feat: revision-qualified annotations root for pinned runtimes (P5)
This commit is contained in:
@@ -240,7 +240,7 @@ function readSnapshotWorkspace(snapshotPath: string): {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function runtimePaths(dataRoot: string, workspaceId: string): RuntimePaths {
|
function runtimePaths(dataRoot: string, workspaceId: string, workspaceRevision?: string): RuntimePaths {
|
||||||
if (!isAbsolute(dataRoot)) throw new Error("registry workspace runtime requires an absolute data root");
|
if (!isAbsolute(dataRoot)) throw new Error("registry workspace runtime requires an absolute data root");
|
||||||
const root = join(dataRoot, "sessions", workspaceId);
|
const root = join(dataRoot, "sessions", workspaceId);
|
||||||
return {
|
return {
|
||||||
@@ -248,6 +248,9 @@ function runtimePaths(dataRoot: string, workspaceId: string): RuntimePaths {
|
|||||||
artifacts: join(root, "artifacts"),
|
artifacts: join(root, "artifacts"),
|
||||||
indexes: join(root, "indexes"),
|
indexes: join(root, "indexes"),
|
||||||
memory: join(root, "memory"),
|
memory: join(root, "memory"),
|
||||||
|
...(workspaceRevision === undefined
|
||||||
|
? {}
|
||||||
|
: { annotations_root: join(dataRoot, "sessions", workspaceId, "revisions", workspaceRevision, "artifacts") }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,7 +311,7 @@ function renderWorkspaceRuntimeFromWorkspace(options: {
|
|||||||
workspaceId: options.workspaceId,
|
workspaceId: options.workspaceId,
|
||||||
workspaceRevision: options.workspaceRevision,
|
workspaceRevision: options.workspaceRevision,
|
||||||
revisionContentRoot: options.revisionContentRoot,
|
revisionContentRoot: options.revisionContentRoot,
|
||||||
runtimePaths: runtimePaths(options.dataRoot, options.workspaceId),
|
runtimePaths: runtimePaths(options.dataRoot, options.workspaceId, options.workspaceRevision),
|
||||||
installationOverlay: overlay,
|
installationOverlay: overlay,
|
||||||
bindings,
|
bindings,
|
||||||
bindingDigest: stableBindingDigest(bindings),
|
bindingDigest: stableBindingDigest(bindings),
|
||||||
@@ -316,7 +319,7 @@ function renderWorkspaceRuntimeFromWorkspace(options: {
|
|||||||
renderedConfig: renderRuntimeConfig(
|
renderedConfig: renderRuntimeConfig(
|
||||||
options.workspace,
|
options.workspace,
|
||||||
bindings,
|
bindings,
|
||||||
runtimePaths(options.dataRoot, options.workspaceId),
|
runtimePaths(options.dataRoot, options.workspaceId, options.workspaceRevision),
|
||||||
context,
|
context,
|
||||||
overlay,
|
overlay,
|
||||||
options.semanticRuntime,
|
options.semanticRuntime,
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export interface RuntimePaths {
|
|||||||
artifacts: string;
|
artifacts: string;
|
||||||
indexes: string;
|
indexes: string;
|
||||||
memory: string;
|
memory: string;
|
||||||
|
/** Revision-qualified root for curated FK annotations (P5); optional for legacy callers. */
|
||||||
|
annotations_root?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RuntimeIdentity {
|
export interface RuntimeIdentity {
|
||||||
|
|||||||
@@ -170,6 +170,27 @@ test("active workspace rendering is byte-identical to direct snapshot rendering"
|
|||||||
expect(active.catalogBlob).toMatch(/^sha256:[0-9a-f]{64}$/);
|
expect(active.catalogBlob).toMatch(/^sha256:[0-9a-f]{64}$/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("renders a revision-qualified annotations root for the active revision", async () => {
|
||||||
|
const f = await fixture();
|
||||||
|
const active = await renderActiveWorkspaceRuntime({
|
||||||
|
workspaceId: "psd-clinical",
|
||||||
|
registry: f.registry,
|
||||||
|
registryConfig: f.registryConfig,
|
||||||
|
harnessDir: f.harnessDir,
|
||||||
|
configPath: "config/tht.yaml",
|
||||||
|
dataRoot: f.dataRoot,
|
||||||
|
secretRoots: f.registryConfig.secretRoots,
|
||||||
|
semanticRuntime,
|
||||||
|
});
|
||||||
|
const rendered = parse(active.renderedConfig) as Record<string, any>;
|
||||||
|
|
||||||
|
expect(rendered.paths.annotations_root).toBe(
|
||||||
|
join(f.dataRoot, "sessions", "psd-clinical", "revisions", f.revision.commit, "artifacts"),
|
||||||
|
);
|
||||||
|
expect(rendered.roots.annotations_root).toBe(rendered.paths.annotations_root);
|
||||||
|
expect(rendered.paths.artifacts).toBe(join(f.dataRoot, "sessions", "psd-clinical", "artifacts"));
|
||||||
|
});
|
||||||
|
|
||||||
test("deterministic operator leases are keyed by logical identity and stable across calls", async () => {
|
test("deterministic operator leases are keyed by logical identity and stable across calls", async () => {
|
||||||
const f = await fixture();
|
const f = await fixture();
|
||||||
const first = await publishDeterministicRuntimeConfigLease({
|
const first = await publishDeterministicRuntimeConfigLease({
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from tht.cli.schema_cmd import annotations_path
|
||||||
|
from tht.config import load_config
|
||||||
|
|
||||||
|
REVISION = "a" * 40
|
||||||
|
|
||||||
|
|
||||||
|
def _config(tmp_path: Path, *, with_annotations_root: bool = True) -> Path:
|
||||||
|
annotations = tmp_path / "revisions" / REVISION / "artifacts"
|
||||||
|
roots = f"""
|
||||||
|
roots:
|
||||||
|
sessions: {tmp_path / 'sessions'}
|
||||||
|
artifacts: {tmp_path / 'artifacts'}
|
||||||
|
indexes: {tmp_path / 'indexes'}
|
||||||
|
memory: {tmp_path / 'memory'}
|
||||||
|
"""
|
||||||
|
if with_annotations_root:
|
||||||
|
roots += f" annotations_root: {annotations}\n"
|
||||||
|
path = tmp_path / "runtime.yaml"
|
||||||
|
path.write_text(f"""
|
||||||
|
runtime_identity:
|
||||||
|
workspace_id: demo
|
||||||
|
workspace_revision: {REVISION}
|
||||||
|
source_identity: workspace://demo
|
||||||
|
dwh:
|
||||||
|
type: postgres_direct
|
||||||
|
connection: {{database: warehouse, schema: analytics, user: reader, password: secret}}
|
||||||
|
{roots}
|
||||||
|
""")
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def test_annotations_path_prefers_revision_root(tmp_path, monkeypatch):
|
||||||
|
monkeypatch.delenv("THT_HOME", raising=False)
|
||||||
|
monkeypatch.delenv("THT_DATA_ROOT", raising=False)
|
||||||
|
cfg = load_config(_config(tmp_path))
|
||||||
|
|
||||||
|
assert cfg.paths.annotations_root == tmp_path / "revisions" / REVISION / "artifacts"
|
||||||
|
assert annotations_path(cfg) == (
|
||||||
|
tmp_path / "revisions" / REVISION / "artifacts" / "mschema" / "annotations.yaml"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_annotations_path_falls_back_to_legacy_root(tmp_path, monkeypatch):
|
||||||
|
monkeypatch.delenv("THT_HOME", raising=False)
|
||||||
|
monkeypatch.delenv("THT_DATA_ROOT", raising=False)
|
||||||
|
cfg = load_config(_config(tmp_path, with_annotations_root=False))
|
||||||
|
|
||||||
|
assert cfg.paths.annotations_root is None
|
||||||
|
assert annotations_path(cfg) == tmp_path / "artifacts" / "mschema" / "annotations.yaml"
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_config_preserves_annotations_root_with_data_root(tmp_path, monkeypatch):
|
||||||
|
monkeypatch.setenv("THT_DATA_ROOT", str(tmp_path / "data"))
|
||||||
|
monkeypatch.delenv("THT_HOME", raising=False)
|
||||||
|
cfg = load_config(_config(tmp_path))
|
||||||
|
|
||||||
|
assert cfg.paths.annotations_root == tmp_path / "revisions" / REVISION / "artifacts"
|
||||||
|
assert annotations_path(cfg) == (
|
||||||
|
tmp_path / "revisions" / REVISION / "artifacts" / "mschema" / "annotations.yaml"
|
||||||
|
)
|
||||||
@@ -49,6 +49,8 @@ def physical_path(cfg) -> Path:
|
|||||||
|
|
||||||
|
|
||||||
def annotations_path(cfg) -> Path:
|
def annotations_path(cfg) -> Path:
|
||||||
|
if cfg.paths.annotations_root is not None:
|
||||||
|
return cfg.paths.annotations_root / "mschema" / "annotations.yaml"
|
||||||
return cfg.paths.artifacts / "mschema" / "annotations.yaml"
|
return cfg.paths.artifacts / "mschema" / "annotations.yaml"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -324,6 +324,9 @@ class PathsConfig(BaseModel):
|
|||||||
# Explicit workspace-global memory root (P3). When absent, legacy `artifacts/memory` is used
|
# Explicit workspace-global memory root (P3). When absent, legacy `artifacts/memory` is used
|
||||||
# only through the documented migration path.
|
# only through the documented migration path.
|
||||||
memory: Path | None = None
|
memory: Path | None = None
|
||||||
|
# Revision-qualified curated FK annotations root (P5). When absent, legacy
|
||||||
|
# `artifacts/mschema/annotations.yaml` remains the annotations source.
|
||||||
|
annotations_root: Path | None = None
|
||||||
|
|
||||||
|
|
||||||
class RuntimeIdentityConfig(BaseModel):
|
class RuntimeIdentityConfig(BaseModel):
|
||||||
@@ -669,6 +672,8 @@ def load_config(path: Path) -> Config:
|
|||||||
sessions=resolved.sessions,
|
sessions=resolved.sessions,
|
||||||
artifacts=resolved.artifacts,
|
artifacts=resolved.artifacts,
|
||||||
indexes=resolved.indexes,
|
indexes=resolved.indexes,
|
||||||
|
memory=cfg.paths.memory,
|
||||||
|
annotations_root=cfg.paths.annotations_root,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user