feat: reconcile generated docs on explicit registry pull
This commit is contained in:
@@ -136,14 +136,66 @@ export class WorkspaceRegistry {
|
||||
return await this.lock.run(async () => {
|
||||
try {
|
||||
const status = await this.repository.pull();
|
||||
await this.activate(status.head!);
|
||||
return status;
|
||||
const head = await this.reconcileGeneratedDocs(status.head!);
|
||||
await this.activate(head);
|
||||
return head === status.head ? status : { ...status, head };
|
||||
} catch (error) {
|
||||
return await this.gitFallback(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconcile API-owned generated documentation against the active catalog/descriptors at an
|
||||
* exact commit. Startup/status paths never push; only an explicit operator pull may produce a
|
||||
* single deterministic docs-only follow-up commit. Curator catalog/descriptor/Evidence bytes
|
||||
* are never modified.
|
||||
*/
|
||||
private async reconcileGeneratedDocs(commit: string): Promise<string> {
|
||||
const safeHead = safeCommit(commit);
|
||||
const catalog = parseWorkspaceCatalogYaml(await this.repository.readCatalog(safeHead));
|
||||
const catalogById = new Map(catalog.workspaces.map((entry) => [entry.id, entry]));
|
||||
const expected = new Map<string, { envExample: string; markdown: string }>();
|
||||
for (const id of catalogById.keys()) {
|
||||
const path = workspacePath(id);
|
||||
const type = await this.repository.gitObjectType(safeHead, path);
|
||||
if (type !== "blob") continue;
|
||||
const workspace = parseWorkspaceYaml(await this.repository.readWorkspace(path, safeHead));
|
||||
assertCatalogMatchesDescriptor(catalogById.get(id)!, workspace);
|
||||
expected.set(id, renderWorkspaceDocs(workspace));
|
||||
}
|
||||
|
||||
const docPaths = this.documentationPaths;
|
||||
const writes: string[] = [];
|
||||
const removals: string[] = [];
|
||||
for (const [id, docs] of expected) {
|
||||
for (const [kind, contents] of [["contract", docs.envExample], ["readme", docs.markdown]] as const) {
|
||||
const path = docPaths(id)[kind === "contract" ? "contract" : "readme"];
|
||||
const current = await this.repository.readObjectOrAbsent(safeHead, path);
|
||||
if (current !== contents) {
|
||||
await this.repository.writeRegistryFile(path, contents);
|
||||
writes.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
const presentDocs = new Set<string>();
|
||||
for (const path of await this.repository.workspaceDocsPaths(safeHead)) {
|
||||
const id = path.slice("workspace-docs/".length, path.lastIndexOf("/"));
|
||||
if (!expected.has(id)) {
|
||||
await this.repository.removeRegistryFile(path);
|
||||
removals.push(path);
|
||||
} else {
|
||||
presentDocs.add(path);
|
||||
}
|
||||
}
|
||||
if (writes.length === 0 && removals.length === 0) return safeHead;
|
||||
const next = await this.repository.commitAndPush(
|
||||
[...writes, ...removals],
|
||||
"Synchronize generated workspace documentation",
|
||||
);
|
||||
return next.head!;
|
||||
}
|
||||
|
||||
async listCatalog(): Promise<Array<WorkspaceCatalogEntry & {
|
||||
configurationState: "ready" | "configuration_required";
|
||||
revision?: WorkspaceRevision;
|
||||
|
||||
Reference in New Issue
Block a user