fix(metadata): stop schema changes churning generated skill evidence (#1075)

This commit is contained in:
Ysopwnable authored and GitHub committed 2026-10-08 22:21:30 +08:00
1 parent e2f03786f4
commit d52f66d62a
11 files changed
+98 -18

No files matched your search

+9
View File
@@ -76,6 +76,15 @@ the same configuration. Pre-commit formats and lints staged files; pre-push runs
`check:fast`.
`docs:check` checks committed generated metadata. `docs:generate` regenerates
those files, and the docs CI lane checks them.
The committed product catalog contains documented facts and their provider
identity, rather than full runtime schema hashes. The managed skill contains
instructions and inventory metadata; doctor compares its installed files with
the canonical bundle. Schema-only fixes should not change these files or the
skill commitment in the conformance manifest. Runtime schema identity remains
available through doctor and `binary_session`. If a branch conflicts only in
these generated files, merge the source changes first, then run
`npm run docs:generate` to regenerate from the merged sources; keep hand-written
skill instructions from both branches.
Real-provider execution remains uncached; deterministic builds use Turbo.
Local `npm test` runs every deterministic Vitest project without coverage or
-3
View File
@@ -713,9 +713,6 @@
"mcp_prompts": 6
},
"digests": {
"tools_sha256": "0ac2316e6bd007d273c60f69e132341fc06a3dcb6879408ecfdd3271f30a8230",
"prompts_sha256": "ef2de8814390b340623c32001610aa2709d90bed51537b6e562ab3abf144ab4d",
"combined_sha256": "20cc1432b29f11c20206cbaeacafab927dcfd68c81a82cf198ad058f54f06eac",
"providers_sha256": "954b499c76514890062d2b6c936d73cb3a9c45412d522a081125a325e81cdec5"
}
}
@@ -1,5 +1,5 @@
{
"manifest_id": "ecm_bee7251323bddbe561003a45ddee6c43eb9e37839983dc89e438dca6d9e5fdec",
"manifest_id": "ecm_0abe486e63d1ebdeec6487dfd19a5bd92d47a90d84e60df96926fa66602552ea",
"verifier": {
"id": "rea-managed-conformance",
"version": "1"
@@ -13,7 +13,7 @@
"skill_digests": [
{
"skill_id": "reverse-engineer-anything",
"sha256": "62f197bbd05ca97ff21cc69f45ccf776c152004f3c7f1bd589dfea0b8202db57"
"sha256": "0e6130b292e3cd3fe1da214b2b3ca5eba8661c81a5bf64679b7ef29828393f29"
}
],
"claims": [
+6 -7
View File
@@ -19,13 +19,12 @@ const withCount = current.replace(
/^\s{2}tool_count:\s*\d+\s*$/mu,
` tool_count: ${String(CATALOG_IDENTITY.counts.mcp_tools)}`,
);
const digestLine = ` catalog_digest: "${CATALOG_IDENTITY.digests.combined_sha256}"`;
const source = /^\s{2}catalog_digest:/mu.test(withCount)
? withCount.replace(
/^\s{2}catalog_digest:\s*"[a-f0-9]{64}"\s*$/mu,
digestLine,
)
: withCount.replace(/^(\s{2}tool_count:\s*\d+\s*)$/mu, `$1\n${digestLine}`);
// Skill identity follows the installed instruction bundle. Runtime schema identity
// is still reported independently by doctor and binary_session.
const source = withCount.replace(
/^ {2}catalog_digest:[^\r\n]*(?:\r?\n|$)/mu,
"",
);
await ensureGeneratedFile({
path,
+1 -1
View File
@@ -58,7 +58,7 @@ export const createProductCatalog = async (root) => {
runtime_catalog: {
counts: sources.catalogIdentity.CATALOG_IDENTITY.counts,
digests: {
...sources.catalogIdentity.CATALOG_IDENTITY.digests,
// Full runtime schema digests belong to server identity, not this facts projection.
providers_sha256: providerCatalogDigest(providers),
},
},
@@ -4,7 +4,6 @@ description: Reverse engineer native, managed, Electron/JavaScript, packaged, fi
metadata:
version: "33"
tool_count: 139
catalog_digest: "20cc1432b29f11c20206cbaeacafab927dcfd68c81a82cf198ad058f54f06eac"
---
# REA
+2 -1
View File
@@ -26,7 +26,8 @@ export const createDoctorHostFixture = (
Promise.resolve({
version: PRODUCT_IDENTITY.skillVersion,
toolCount: CATALOG_IDENTITY.counts.mcp_tools,
catalogDigest: CATALOG_IDENTITY.digests.combined_sha256,
catalogDigest: null,
canonical: true,
}),
...overrides,
});
+1
View File
@@ -177,6 +177,7 @@ describe("doctor installation identity", () => {
version: "10",
toolCount: null,
catalogDigest: null,
canonical: false,
}),
}),
);
+7 -2
View File
@@ -14,6 +14,7 @@ import {
readLinuxDistribution,
type LinuxDistribution,
} from "./LinuxHopper.js";
import { canonicalSkillNeedsInstall } from "./SetupSkill.js";
import { CATALOG_IDENTITY } from "../catalogIdentity.js";
import { PRODUCT_IDENTITY, SDK_IDENTITY } from "../identity.js";
import {
@@ -95,11 +96,13 @@ export interface DoctorHost {
ilspyCmdVersion?(path: string): Promise<string | undefined>;
}
/** Parsed identity committed by an installed REA skill. */
/** Observed skill metadata and comparison with the packaged instruction bundle. */
interface InstalledSkillIdentity {
readonly version: string | null;
readonly toolCount: number | null;
readonly catalogDigest: string | null;
/** Whether all managed instruction and reference files match this package. */
readonly canonical: boolean;
}
/** Structured result returned by the read-only doctor workflow. */
@@ -139,6 +142,7 @@ interface DoctorIdentity {
readonly skill: {
readonly installed_version: string | null;
readonly installed_tool_count: number | null;
/** Legacy catalog digest observed in older bundles; current bundles omit it. */
readonly installed_catalog_digest: string | null;
readonly state: "aligned" | "stale" | "missing";
readonly remediation: string | null;
@@ -253,7 +257,7 @@ const skillIdentityAligned = (
): boolean =>
identity?.version === PRODUCT_IDENTITY.skillVersion &&
identity.toolCount === CATALOG_IDENTITY.counts.mcp_tools &&
identity.catalogDigest === CATALOG_IDENTITY.digests.combined_sha256;
identity.canonical;
const skillIdentityCheck = (
identity: InstalledSkillIdentity | undefined,
@@ -507,6 +511,7 @@ const installedSkillIdentity = async (
const content = await readInstalledSkill(home);
const countText = /^\s{2}tool_count:\s*(\d+)\s*$/mu.exec(content)?.[1];
return {
canonical: !(await canonicalSkillNeedsInstall(home)),
version: /^\s{2}version:\s*"([^"]+)"\s*$/mu.exec(content)?.[1] ?? null,
toolCount:
countText === undefined ? null : Number.parseInt(countText, 10),
@@ -195,6 +195,11 @@ describe("canonical product catalog", () => {
"project_managed_application_graph",
"verify_managed_native_boundaries",
]);
// Runtime schema commitments must not fan out into checked-in documentation.
// Provider commitments cover the facts actually present in this projection.
expect(Object.keys(catalog.runtime_catalog.digests)).toEqual([
"providers_sha256",
]);
expect(catalog.runtime_catalog.digests.providers_sha256).toBe(
providerCatalogDigest(catalog.providers),
);
+65 -1
View File
@@ -1,4 +1,4 @@
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { describe, expect, it } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
@@ -7,6 +7,11 @@ import {
canonicalSkillNeedsInstall,
installCanonicalSkill,
} from "../../../src/application/SetupSkill.js";
import {
runDoctor,
systemDoctorHost,
} from "../../../src/application/Doctor.js";
import { createDoctorHostFixture } from "../../../src/application/Doctor.fixture.js";
import { TOOL_CONTRACTS } from "../../../src/contracts/toolContracts.js";
import { PRODUCT_IDENTITY } from "../../../src/identity.js";
import { skillReferenceIssues } from "../../../scripts/lib/docs-facts.mjs";
@@ -98,3 +103,62 @@ describe("canonical skill transaction", () => {
expect(await skillReferenceIssues(dirname(destination))).toEqual([]);
});
});
it("doctor verifies installed instructions and references even when metadata is current", async () => {
const home = await createTestTempDirectory("rea-skill-doctor-");
const destination = join(
home,
".agents/skills/reverse-engineer-anything/SKILL.md",
);
const reference = join(
dirname(destination),
"references/evidence-workflows.md",
);
const system = systemDoctorHost({
environment: { HOME: home, USERPROFILE: home },
});
const host = createDoctorHostFixture({
installedSkillIdentity: () =>
system.installedSkillIdentity?.() ?? Promise.resolve(undefined),
});
expect((await runDoctor(undefined, host)).identity?.skill.state).toBe(
"missing",
);
expect(await installCanonicalSkill(home)).toBe("installed");
expect((await runDoctor(undefined, host)).identity?.skill).toMatchObject({
state: "aligned",
installed_catalog_digest: null,
});
const legacyDigest = "0".repeat(64);
await writeFile(
destination,
(await readFile(destination, "utf8")).replace(
` tool_count: ${String(TOOL_CONTRACTS.length)}`,
` tool_count: ${String(TOOL_CONTRACTS.length)}\n catalog_digest: "${legacyDigest}"`,
),
);
expect((await runDoctor(undefined, host)).identity?.skill).toMatchObject({
state: "stale",
installed_catalog_digest: legacyDigest,
});
expect(await installCanonicalSkill(home)).toBe("installed");
for (const path of [destination, reference]) {
const canonical = await readFile(path, "utf8");
await writeFile(path, `${canonical}\nLocally changed instructions.\n`);
expect((await runDoctor(undefined, host)).identity?.skill.state).toBe(
"stale",
);
expect(await installCanonicalSkill(home)).toBe("installed");
expect((await runDoctor(undefined, host)).identity?.skill.state).toBe(
"aligned",
);
}
await rm(reference);
expect((await runDoctor(undefined, host)).identity?.skill.state).toBe(
"stale",
);
expect(await installCanonicalSkill(home)).toBe("installed");
expect((await runDoctor(undefined, host)).identity?.skill.state).toBe(
"aligned",
);
});