feat: bind evidence trees to registry revisions

This commit is contained in:
2026-08-09 18:39:11 +02:00
parent 033809b1b6
commit 521288eb04
4 changed files with 320 additions and 6 deletions
+20
View File
@@ -387,6 +387,9 @@ export class WorkspaceRegistry {
}
if (request.action === "create" && existing) throw await this.conflictFor(request, status.head!, existing, local);
if (request.action !== "create" && !existing) throw await this.conflictFor(request, status.head!, existing, local);
if (request.action !== "delete") {
await this.assertEvidenceContext(request.workspace, status.head!);
}
const yamlPath = workspacePath(id);
const docPaths = this.documentationPaths(id);
@@ -472,6 +475,20 @@ export class WorkspaceRegistry {
));
}
private async assertEvidenceContext(workspace: WorkspaceDescriptor, revision: string): Promise<void> {
if (!isCanonicalWorkspace(workspace)) return;
if (workspace.evidence?.source.type !== "filesystem") return;
// P6 owns recursive containment. Here we deliberately validate only the declared root object.
await this.repository.assertTreeAtRevision(revision, workspace.evidence.source.uri);
}
private async assertSnapshotEvidenceContexts(state: ActiveState): Promise<void> {
for (const revision of state.revisions) {
const workspace = parseWorkspaceYaml(await readFile(revision.snapshotPath, "utf8"));
await this.assertEvidenceContext(workspace, revision.commit);
}
}
private async activate(commit: string): Promise<void> {
const safeHead = safeCommit(commit);
const files = await this.repository.workspacePaths();
@@ -495,6 +512,7 @@ export class WorkspaceRegistry {
if (workspace.workspace.id !== id) {
throw new WorkspaceRegistryError("workspace_invalid", "Workspace ID does not match its repository path");
}
await this.assertEvidenceContext(workspace, safeHead);
let snapshotSource = source;
const state: WorkspaceRevision["state"] = isCanonicalWorkspace(workspace)
? "operational"
@@ -721,6 +739,7 @@ export class WorkspaceRegistry {
`${revision.id}.yaml`, `${revision.id}.env.example`, `${revision.id}.md`,
]);
await this.assertManifestFiles(directory, manifest.files, legacyExpected);
await this.assertSnapshotEvidenceContexts(state);
const expected = this.expectedSnapshotFiles(state);
const files = Object.fromEntries(expected.map((name) => [name, manifest.files[name]]));
await this.writeSnapshotManifest(directory, { ...state, files });
@@ -757,6 +776,7 @@ export class WorkspaceRegistry {
throw new Error("manifest revisions do not match active state");
}
await this.assertManifestFiles(directory, manifest.files, this.expectedSnapshotFiles(state));
await this.assertSnapshotEvidenceContexts(state);
} catch (error) {
if (error instanceof WorkspaceRegistryError) throw error;
throw new WorkspaceRegistryError("workspace_invalid", "Workspace snapshot integrity check failed");