frontend-project-context 1.3.1 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -2
- package/README.md +84 -14
- package/UPGRADING.md +34 -1
- package/docs/04-PROGRAM-DESIGN.md +34 -4
- package/docs/05-ACCEPTANCE-CONTRACT.md +21 -3
- package/docs/08-INSTALLATION-AND-DISTRIBUTION.md +29 -7
- package/docs/14-FORMAL-RELEASE-READINESS.md +26 -1
- package/docs/18-BRANCH-AWARE-STAGED-CONTEXT-DESIGN.md +2 -2
- package/docs/19-POST-1.3.1-AI-TAKEOVER-EVIDENCE-AND-UPGRADE-PLAN.md +579 -0
- package/docs/20-PHASE-A-AI-TAKEOVER-AND-HEALTH-CLOSURE-DESIGN.md +535 -0
- package/docs/21-PHASE-B-EVIDENCE-FEEDBACK-PROTOCOL-DESIGN.md +347 -0
- package/docs/22-PHASE-C-TARGET-UPGRADE-PROTOCOL-DESIGN.md +398 -0
- package/docs/README.md +21 -5
- package/docs/USER-AND-AI-OPERATION-MANUAL.md +797 -0
- package/examples/README.md +27 -0
- package/examples/package.json +6 -2
- package/migration-manifest.json +88 -0
- package/package.json +3 -2
- package/schemas/action-plan.schema.json +31 -3
- package/schemas/capabilities.schema.json +50 -18
- package/schemas/evidence-bundle.schema.json +64 -0
- package/schemas/evidence-input.schema.json +82 -0
- package/schemas/migration-manifest.schema.json +29 -0
- package/schemas/migration-plan.schema.json +32 -0
- package/schemas/project-status.schema.json +75 -0
- package/schemas/projection-lock.schema.json +48 -0
- package/schemas/review-bundle.schema.json +3 -3
- package/schemas/upgrade-assessment.schema.json +48 -0
- package/schemas/upgrade-result-bundle.schema.json +35 -0
- package/src/project-context/ai-entry.mjs +320 -0
- package/src/project-context/capabilities.mjs +44 -17
- package/src/project-context/checker.mjs +20 -3
- package/src/project-context/cli.mjs +77 -2
- package/src/project-context/contract-schema.mjs +30 -16
- package/src/project-context/dashboard-model.mjs +4 -4
- package/src/project-context/dashboard-renderer.mjs +3 -3
- package/src/project-context/discovery.mjs +6 -1
- package/src/project-context/evidence-schema.mjs +209 -0
- package/src/project-context/evidence.mjs +99 -0
- package/src/project-context/exchange-schema.mjs +21 -11
- package/src/project-context/exchange.mjs +26 -4
- package/src/project-context/maintenance.mjs +2 -2
- package/src/project-context/migration-manifest.mjs +166 -0
- package/src/project-context/project-status.mjs +157 -0
- package/src/project-context/projection-store.mjs +8 -1
- package/src/project-context/upgrade-schema.mjs +215 -0
- package/src/project-context/upgrade.mjs +494 -0
|
@@ -11,6 +11,9 @@ import {
|
|
|
11
11
|
} from "./exchange-schema.mjs";
|
|
12
12
|
import { inspectProjectInitialization, loadProject } from "./project-store.mjs";
|
|
13
13
|
import { RENDERER_VERSION } from "./renderer.mjs";
|
|
14
|
+
import { AI_ENTRY_RENDERER_VERSION } from "./ai-entry.mjs";
|
|
15
|
+
import { PROJECT_STATUS_SCHEMA_VERSION } from "./project-status.mjs";
|
|
16
|
+
import { EVIDENCE_BUNDLE_SCHEMA_VERSION, EVIDENCE_INPUT_SCHEMA_VERSION } from "./evidence-schema.mjs";
|
|
14
17
|
import {
|
|
15
18
|
CONTEXT_BUDGET_UNIT,
|
|
16
19
|
INTEGRATION_REVIEW_BUNDLE_SCHEMA_VERSION,
|
|
@@ -18,57 +21,81 @@ import {
|
|
|
18
21
|
STAGE_RECEIPT_SCHEMA_VERSION,
|
|
19
22
|
TASK_CONTEXT_PLAN_SCHEMA_VERSION,
|
|
20
23
|
} from "./task-context-schema.mjs";
|
|
24
|
+
import {
|
|
25
|
+
MIGRATION_PLAN_SCHEMA_VERSION,
|
|
26
|
+
UPGRADE_ASSESSMENT_SCHEMA_VERSION,
|
|
27
|
+
UPGRADE_RESULT_BUNDLE_SCHEMA_VERSION,
|
|
28
|
+
} from "./upgrade-schema.mjs";
|
|
21
29
|
|
|
22
|
-
function schemas() {
|
|
30
|
+
function schemas(projectionLockWritten = 1) {
|
|
23
31
|
return {
|
|
24
32
|
actionPlan: ACTION_PLAN_SCHEMA_VERSION,
|
|
25
33
|
assistBundle: ASSIST_BUNDLE_SCHEMA_VERSION,
|
|
26
34
|
capabilities: CAPABILITIES_SCHEMA_VERSION,
|
|
27
35
|
contract: 2,
|
|
28
36
|
dashboardViewModel: DASHBOARD_SCHEMA_VERSION,
|
|
37
|
+
evidenceBundle: EVIDENCE_BUNDLE_SCHEMA_VERSION,
|
|
38
|
+
evidenceInput: EVIDENCE_INPUT_SCHEMA_VERSION,
|
|
29
39
|
integrationReviewBundle: INTEGRATION_REVIEW_BUNDLE_SCHEMA_VERSION,
|
|
30
|
-
projectionLock:
|
|
40
|
+
projectionLock: 2,
|
|
41
|
+
projectionLockReadable: [1, 2],
|
|
42
|
+
projectionLockWritten,
|
|
31
43
|
projectionRenderer: RENDERER_VERSION,
|
|
32
44
|
proposal: 1,
|
|
45
|
+
projectStatus: PROJECT_STATUS_SCHEMA_VERSION,
|
|
46
|
+
aiEntryRenderer: AI_ENTRY_RENDERER_VERSION,
|
|
47
|
+
migrationManifest: 2,
|
|
48
|
+
migrationPlan: MIGRATION_PLAN_SCHEMA_VERSION,
|
|
33
49
|
reviewBundle: REVIEW_BUNDLE_SCHEMA_VERSION,
|
|
34
50
|
sourceLock: 1,
|
|
35
51
|
stageContextBundle: STAGE_CONTEXT_BUNDLE_SCHEMA_VERSION,
|
|
36
52
|
stageReceipt: STAGE_RECEIPT_SCHEMA_VERSION,
|
|
37
53
|
taskContextPlan: TASK_CONTEXT_PLAN_SCHEMA_VERSION,
|
|
54
|
+
upgradeAssessment: UPGRADE_ASSESSMENT_SCHEMA_VERSION,
|
|
55
|
+
upgradeResultBundle: UPGRADE_RESULT_BUNDLE_SCHEMA_VERSION,
|
|
38
56
|
};
|
|
39
57
|
}
|
|
40
58
|
|
|
59
|
+
export const PERMANENT_BOUNDARIES = Object.freeze({
|
|
60
|
+
provider: false,
|
|
61
|
+
agentRuntime: false,
|
|
62
|
+
git: false,
|
|
63
|
+
network: false,
|
|
64
|
+
dependencyInstallation: false,
|
|
65
|
+
automaticApproval: false,
|
|
66
|
+
businessCodeWrites: false,
|
|
67
|
+
taskExecution: false,
|
|
68
|
+
stagePathBodyReads: false,
|
|
69
|
+
applyPlan: false,
|
|
70
|
+
scheduler: false,
|
|
71
|
+
daemon: false,
|
|
72
|
+
telemetry: false,
|
|
73
|
+
selfUpdate: false,
|
|
74
|
+
automaticEvidenceUpload: false,
|
|
75
|
+
packageManager: false,
|
|
76
|
+
automaticUpgrade: false,
|
|
77
|
+
});
|
|
78
|
+
|
|
41
79
|
export async function buildCapabilities(root) {
|
|
42
80
|
const initialization = await inspectProjectInitialization(root);
|
|
43
81
|
let project = null;
|
|
82
|
+
let projectionLockWritten = 1;
|
|
44
83
|
if (initialization.status === "initialized") {
|
|
45
84
|
const loaded = await loadProject(root);
|
|
46
85
|
project = { id: loaded.contract.project.id, name: loaded.contract.project.name };
|
|
86
|
+
projectionLockWritten = loaded.projectionsLock.schemaVersion;
|
|
47
87
|
}
|
|
48
88
|
return {
|
|
49
89
|
schemaVersion: CAPABILITIES_SCHEMA_VERSION,
|
|
50
90
|
package: { name: "frontend-project-context", version: PACKAGE_VERSION },
|
|
51
91
|
exchangeProtocolVersion: EXCHANGE_PROTOCOL_VERSION,
|
|
52
|
-
schemas: schemas(),
|
|
92
|
+
schemas: schemas(projectionLockWritten),
|
|
53
93
|
commands: [...COMMANDS],
|
|
54
94
|
actionKinds: [...ACTION_KINDS],
|
|
55
95
|
contextBudget: { unit: CONTEXT_BUDGET_UNIT, modelTokens: false, callerMustProvideLimit: true },
|
|
56
96
|
initialization: initialization.status,
|
|
57
97
|
initialized: initialization.status === "initialized",
|
|
58
98
|
project,
|
|
59
|
-
boundaries: {
|
|
60
|
-
provider: false,
|
|
61
|
-
agentRuntime: false,
|
|
62
|
-
git: false,
|
|
63
|
-
network: false,
|
|
64
|
-
dependencyInstallation: false,
|
|
65
|
-
automaticApproval: false,
|
|
66
|
-
businessCodeWrites: false,
|
|
67
|
-
taskExecution: false,
|
|
68
|
-
stagePathBodyReads: false,
|
|
69
|
-
applyPlan: false,
|
|
70
|
-
scheduler: false,
|
|
71
|
-
daemon: false,
|
|
72
|
-
},
|
|
99
|
+
boundaries: { ...PERMANENT_BOUNDARIES },
|
|
73
100
|
};
|
|
74
101
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { AI_ENTRY_RENDERER_VERSION, parseAiEntryRegion } from "./ai-entry.mjs";
|
|
3
4
|
import { sha256 } from "./canonical-json.mjs";
|
|
4
5
|
import { sourceStatus } from "./contract-schema.mjs";
|
|
5
6
|
import { readSourceDigest, verifyItem } from "./source-reader.mjs";
|
|
@@ -74,7 +75,23 @@ export async function checkProject(root, project) {
|
|
|
74
75
|
try {
|
|
75
76
|
content = await readFile(resolved.absolute, "utf8");
|
|
76
77
|
} catch (error) {
|
|
77
|
-
findings.push({
|
|
78
|
+
findings.push({
|
|
79
|
+
code: entry.ownership === "region"
|
|
80
|
+
? error?.code === "ENOENT" ? "ai-entry-missing" : "projection-unreadable"
|
|
81
|
+
: error?.code === "ENOENT" ? "projection-missing" : "projection-unreadable",
|
|
82
|
+
path: entry.path,
|
|
83
|
+
});
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (entry.ownership === "region") {
|
|
87
|
+
const parsed = parseAiEntryRegion(content);
|
|
88
|
+
if (parsed.state !== "present" || sha256(parsed.region) !== entry.regionDigest) {
|
|
89
|
+
findings.push({ code: "ai-entry-ownership-conflict", path: entry.path });
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (entry.rendererVersion !== AI_ENTRY_RENDERER_VERSION) {
|
|
93
|
+
findings.push({ code: "ai-entry-renderer-stale", path: entry.path, expected: AI_ENTRY_RENDERER_VERSION, actual: entry.rendererVersion });
|
|
94
|
+
}
|
|
78
95
|
continue;
|
|
79
96
|
}
|
|
80
97
|
const marker = parseProjectionMarker(content);
|
|
@@ -113,7 +130,7 @@ export async function checkProject(root, project) {
|
|
|
113
130
|
}
|
|
114
131
|
|
|
115
132
|
export function checkExitCode(findings) {
|
|
116
|
-
return findings.some((finding) =>
|
|
133
|
+
return findings.some((finding) => ["projection-ownership-conflict", "ai-entry-ownership-conflict", "ai-entry-path-conflict"].includes(finding.code)) ? 3 : findings.length > 0 ? 1 : 0;
|
|
117
134
|
}
|
|
118
135
|
|
|
119
136
|
export function blockingContextFindings(findings) {
|
|
@@ -127,6 +144,6 @@ export function blockingContextFindings(findings) {
|
|
|
127
144
|
}
|
|
128
145
|
|
|
129
146
|
export function findingSeverity(finding) {
|
|
130
|
-
if (
|
|
147
|
+
if (["projection-ownership-conflict", "ai-entry-ownership-conflict", "ai-entry-path-conflict"].includes(finding.code)) return "conflict";
|
|
131
148
|
return blockingContextFindings([finding]).length > 0 ? "blocked" : "attention";
|
|
132
149
|
}
|
|
@@ -8,21 +8,31 @@ import { buildDashboardModel } from "./dashboard-model.mjs";
|
|
|
8
8
|
import { renderDashboardHtml } from "./dashboard-renderer.mjs";
|
|
9
9
|
import { discoverProject } from "./discovery.mjs";
|
|
10
10
|
import { ProjectContextError, fail } from "./errors.mjs";
|
|
11
|
+
import { buildEvidenceBundleFile } from "./evidence.mjs";
|
|
11
12
|
import { buildCapabilities, preflightActionPlanFile } from "./exchange.mjs";
|
|
13
|
+
import { PERMANENT_BOUNDARIES } from "./capabilities.mjs";
|
|
12
14
|
import { normalizeProposalPath } from "./exchange-schema.mjs";
|
|
13
15
|
import { atomicCreateFileOrSame, atomicWriteFile, readJsonFile } from "./io.mjs";
|
|
14
16
|
import { acceptSourceChange, deprecateItem, deprecateSource, reviewSource, reviseItem } from "./maintenance.mjs";
|
|
15
17
|
import { normalizeRelativePath, resolveExistingInside, resolveProjectRoot, resolveWritableInside } from "./path-policy.mjs";
|
|
16
18
|
import { initializeProject, inspectProjectInitialization, loadProject } from "./project-store.mjs";
|
|
17
19
|
import { publishProjection } from "./projection-store.mjs";
|
|
20
|
+
import { publishAiEntry, removeAiEntry } from "./ai-entry.mjs";
|
|
21
|
+
import { buildProjectStatus } from "./project-status.mjs";
|
|
18
22
|
import { renderContextBundle } from "./renderer.mjs";
|
|
19
23
|
import { buildIntegrationReviewBundleFiles, buildStageContextBundleFiles } from "./task-context.mjs";
|
|
24
|
+
import { applyMigrationPlanFile, buildMigrationPlanFile, buildUpgradeAssessment } from "./upgrade.mjs";
|
|
20
25
|
|
|
21
26
|
const HELP = `project-context — model-neutral project contract compiler
|
|
22
27
|
|
|
23
28
|
Usage:
|
|
24
29
|
project-context init --project PATH --id ID --name NAME [--write] [--json]
|
|
25
30
|
project-context capabilities --project PATH [--json]
|
|
31
|
+
project-context status --project PATH [--json]
|
|
32
|
+
project-context evidence --project PATH --input FILE [--json]
|
|
33
|
+
project-context upgrade-check --project PATH --from-version VERSION [--json]
|
|
34
|
+
project-context upgrade-plan --project PATH --assessment FILE [--json]
|
|
35
|
+
project-context upgrade-apply --project PATH --plan FILE [--write] [--json]
|
|
26
36
|
project-context setup --project PATH --id ID --name NAME [--output FILE] [--write] [--json]
|
|
27
37
|
project-context register --project PATH --id SOURCE_ID --kind KIND [--path PATH] [--pointer POINTER] [--reference TEXT] [--write] [--json]
|
|
28
38
|
project-context propose --project PATH --id ITEM_ID --kind KIND --subject SUBJECT (--value TEXT | --value-json JSON) --statement TEXT --sources SOURCE_ID... --scope SCOPE [--scope-path PATH] [--overrides ITEM_ID...] [--verification KIND] [--verification-source SOURCE_ID] [--verification-expected-json JSON] [--output FILE --write] [--json]
|
|
@@ -35,6 +45,8 @@ Usage:
|
|
|
35
45
|
project-context approve --project PATH (--proposal FILE | --pending) --ids ID... --by NAME [--rationale TEXT] [--write] [--json | --full-json]
|
|
36
46
|
project-context context --project PATH --path RELATIVE_PATH... [--task TEXT] [--locale zh-CN|en|all] [--json]
|
|
37
47
|
project-context publish --project PATH --target agents|ruler --output FILE [--path RELATIVE_PATH...] [--write] [--json]
|
|
48
|
+
project-context publish-entry --project PATH --output AGENTS.md [--write] [--json]
|
|
49
|
+
project-context remove-entry --project PATH --output AGENTS.md [--write] [--json]
|
|
38
50
|
project-context check --project PATH [--json]
|
|
39
51
|
project-context dashboard --project PATH [--json]
|
|
40
52
|
project-context sync --project PATH [--changed-path RELATIVE_PATH...] [--json]
|
|
@@ -45,10 +57,10 @@ Usage:
|
|
|
45
57
|
All commands are read-only unless their own --write flag is present.
|
|
46
58
|
`;
|
|
47
59
|
const VALUE_FLAGS = new Set([
|
|
48
|
-
"project", "id", "name", "output", "proposal", "by", "task", "target", "rationale",
|
|
60
|
+
"project", "id", "name", "input", "output", "proposal", "by", "task", "target", "rationale",
|
|
49
61
|
"kind", "pointer", "reference", "subject", "value", "value-json", "statement", "scope", "scope-path",
|
|
50
62
|
"verification", "verification-source", "verification-expected-json",
|
|
51
|
-
"expected-digest", "expected-item-digest", "expected-source-digest", "locale", "plan", "stage",
|
|
63
|
+
"expected-digest", "expected-item-digest", "expected-source-digest", "locale", "plan", "stage", "from-version", "assessment",
|
|
52
64
|
]);
|
|
53
65
|
const LIST_FLAGS = new Set([
|
|
54
66
|
"ids", "path", "changed-path", "sources", "overrides", "affected-items", "receipt", "receipt-bundle", "main-changed-path", "branch-changed-path",
|
|
@@ -57,6 +69,11 @@ const BOOLEAN_FLAGS = new Set(["write", "json", "full-json", "help", "pending"])
|
|
|
57
69
|
const COMMAND_OPTIONS = new Map([
|
|
58
70
|
["init", new Set(["project", "id", "name", "write", "json", "help"])],
|
|
59
71
|
["capabilities", new Set(["project", "json", "help"])],
|
|
72
|
+
["status", new Set(["project", "json", "help"])],
|
|
73
|
+
["evidence", new Set(["project", "input", "json", "help"])],
|
|
74
|
+
["upgrade-check", new Set(["project", "from-version", "json", "help"])],
|
|
75
|
+
["upgrade-plan", new Set(["project", "assessment", "json", "help"])],
|
|
76
|
+
["upgrade-apply", new Set(["project", "plan", "write", "json", "help"])],
|
|
60
77
|
["setup", new Set(["project", "id", "name", "output", "write", "json", "help"])],
|
|
61
78
|
["register", new Set(["project", "id", "kind", "path", "pointer", "reference", "write", "json", "help"])],
|
|
62
79
|
["propose", new Set([
|
|
@@ -82,6 +99,8 @@ const COMMAND_OPTIONS = new Map([
|
|
|
82
99
|
["approve", new Set(["project", "proposal", "pending", "ids", "by", "rationale", "write", "json", "full-json", "help"])],
|
|
83
100
|
["context", new Set(["project", "path", "task", "locale", "json", "help"])],
|
|
84
101
|
["publish", new Set(["project", "target", "output", "path", "write", "json", "help"])],
|
|
102
|
+
["publish-entry", new Set(["project", "output", "write", "json", "help"])],
|
|
103
|
+
["remove-entry", new Set(["project", "output", "write", "json", "help"])],
|
|
85
104
|
["check", new Set(["project", "json", "help"])],
|
|
86
105
|
["dashboard", new Set(["project", "json", "help"])],
|
|
87
106
|
["sync", new Set(["project", "changed-path", "json", "help"])],
|
|
@@ -323,6 +342,51 @@ async function runCommand(command, options) {
|
|
|
323
342
|
].join("\n") + "\n";
|
|
324
343
|
return { exitCode: 0, stdout: jsonOrText(options, capabilities, summary), stderr: "" };
|
|
325
344
|
}
|
|
345
|
+
if (command === "status") {
|
|
346
|
+
const result = await buildProjectStatus(root, PERMANENT_BOUNDARIES);
|
|
347
|
+
const summary = `Project Context: ${result.status.initialization.state}; health ${result.status.health}; AI Entry ${result.status.entry.state}.\n`;
|
|
348
|
+
return { exitCode: result.exitCode, stdout: jsonOrText(options, result.status, summary), stderr: "" };
|
|
349
|
+
}
|
|
350
|
+
if (command === "evidence") {
|
|
351
|
+
const bundle = await buildEvidenceBundleFile(root, required(options, "input"));
|
|
352
|
+
const summary = [
|
|
353
|
+
`Evidence ${bundle.result}; project health ${bundle.projectContext.health}.`,
|
|
354
|
+
`Finding codes: ${bundle.projectContext.findingCodes.join(", ") || "none"}.`,
|
|
355
|
+
`Bundle digest: ${bundle.bundleDigest}.`,
|
|
356
|
+
"Human review is required before transfer.",
|
|
357
|
+
].join("\n") + "\n";
|
|
358
|
+
return { exitCode: 0, stdout: jsonOrText(options, bundle, summary), stderr: "" };
|
|
359
|
+
}
|
|
360
|
+
if (command === "upgrade-check") {
|
|
361
|
+
const assessment = await buildUpgradeAssessment(root, required(options, "from-version"));
|
|
362
|
+
const summary = [
|
|
363
|
+
`Upgrade ${assessment.fromVersion} -> ${assessment.targetVersion}: ${assessment.state}.`,
|
|
364
|
+
`Health: ${assessment.health}; rollback: ${assessment.rollbackClass}.`,
|
|
365
|
+
`Finding codes: ${assessment.findingCodes.join(", ") || "none"}.`,
|
|
366
|
+
`Assessment digest: ${assessment.assessmentDigest}.`,
|
|
367
|
+
].join("\n") + "\n";
|
|
368
|
+
return { exitCode: assessment.state === "blocked" || assessment.state === "not-applicable" ? 1 : 0, stdout: jsonOrText(options, assessment, summary), stderr: "" };
|
|
369
|
+
}
|
|
370
|
+
if (command === "upgrade-plan") {
|
|
371
|
+
const plan = await buildMigrationPlanFile(root, required(options, "assessment"));
|
|
372
|
+
const summary = [
|
|
373
|
+
`Upgrade plan: ${plan.nextAction.kind}.`,
|
|
374
|
+
`Targets: ${plan.nextAction.targets.join(", ") || "none"}.`,
|
|
375
|
+
`Writes: ${plan.nextAction.writes}; human review: ${plan.requiresHumanReview}.`,
|
|
376
|
+
`Plan digest: ${plan.planDigest}.`,
|
|
377
|
+
].join("\n") + "\n";
|
|
378
|
+
return { exitCode: 0, stdout: jsonOrText(options, plan, summary), stderr: "" };
|
|
379
|
+
}
|
|
380
|
+
if (command === "upgrade-apply") {
|
|
381
|
+
const applied = await applyMigrationPlanFile(root, required(options, "plan"), { write: options.write });
|
|
382
|
+
const summary = [
|
|
383
|
+
`Upgrade ${applied.result.mode}: ${applied.result.coreMigration}.`,
|
|
384
|
+
`Action: ${applied.result.action.kind}; written: ${applied.result.written}.`,
|
|
385
|
+
`Overall upgrade: ${applied.result.overallUpgrade}.`,
|
|
386
|
+
`Result digest: ${applied.result.resultDigest}.`,
|
|
387
|
+
].join("\n") + "\n";
|
|
388
|
+
return { exitCode: applied.exitCode, stdout: jsonOrText(options, applied.result, summary), stderr: "" };
|
|
389
|
+
}
|
|
326
390
|
if (command === "init") {
|
|
327
391
|
const result = await initializeProject(root, required(options, "id"), required(options, "name"), options.write);
|
|
328
392
|
return {
|
|
@@ -539,6 +603,17 @@ async function runCommand(command, options) {
|
|
|
539
603
|
const summary = options.write ? `${result.action}: ${result.entry.path}\n` : `Preview ${result.action}: ${result.entry.path}\n\n${result.content}`;
|
|
540
604
|
return { exitCode: 0, stdout: options.json ? prettyCanonicalJson(result) : summary, stderr: "" };
|
|
541
605
|
}
|
|
606
|
+
if (command === "publish-entry" || command === "remove-entry") {
|
|
607
|
+
const operation = command === "publish-entry" ? publishAiEntry : removeAiEntry;
|
|
608
|
+
const result = await operation(root, project, {
|
|
609
|
+
output: required(options, "output"),
|
|
610
|
+
write: options.write,
|
|
611
|
+
});
|
|
612
|
+
const summary = options.write
|
|
613
|
+
? `${result.action}: ${result.impact.paths[0]}\n`
|
|
614
|
+
: `Preview ${result.action}: ${result.impact.paths[0]}\n\n${result.proposed.region ?? ""}`;
|
|
615
|
+
return { exitCode: 0, stdout: options.json ? prettyCanonicalJson(result) : summary, stderr: "" };
|
|
616
|
+
}
|
|
542
617
|
fail("command-unknown", `unknown command: ${command}`);
|
|
543
618
|
}
|
|
544
619
|
|
|
@@ -288,26 +288,40 @@ export function validateSourceLock(lock) {
|
|
|
288
288
|
export function validateProjectionLock(lock) {
|
|
289
289
|
object(lock, "projections lock");
|
|
290
290
|
exactKeys(lock, new Set(["schemaVersion", "projections"]), "projections lock");
|
|
291
|
-
if (lock.schemaVersion
|
|
291
|
+
if (![1, 2].includes(lock.schemaVersion) || !Array.isArray(lock.projections)) fail("schema-invalid", "projections lock is invalid");
|
|
292
292
|
const seen = new Set();
|
|
293
293
|
for (const [index, entry] of lock.projections.entries()) {
|
|
294
294
|
object(entry, `projection entry ${index}`);
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
new Set(["path", "target", "paths", "contractDigest", "bundleDigest", "contentDigest", "itemIds", "rendererVersion"]),
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
for (const key of ["contractDigest", "bundleDigest", "contentDigest"]) {
|
|
305
|
-
string(entry[key], `projection entry ${index}.${key}`);
|
|
306
|
-
if (!SHA256.test(entry[key])) fail("schema-invalid", `projection ${key} must be sha256`);
|
|
295
|
+
const label = `projection entry ${index}`;
|
|
296
|
+
if (lock.schemaVersion === 1) {
|
|
297
|
+
exactKeys(entry, new Set(["path", "target", "paths", "contractDigest", "bundleDigest", "contentDigest", "itemIds", "rendererVersion"]), label);
|
|
298
|
+
} else if (entry.ownership === "file") {
|
|
299
|
+
exactKeys(entry, new Set(["path", "target", "ownership", "paths", "contractDigest", "bundleDigest", "contentDigest", "itemIds", "rendererVersion"]), label);
|
|
300
|
+
} else if (entry.ownership === "region") {
|
|
301
|
+
exactKeys(entry, new Set(["path", "target", "ownership", "regionId", "regionDigest", "rendererVersion", "createdFile"]), label);
|
|
302
|
+
} else {
|
|
303
|
+
fail("schema-invalid-enum", `${label}.ownership must be file or region`);
|
|
307
304
|
}
|
|
308
|
-
|
|
309
|
-
if (
|
|
310
|
-
fail("schema-
|
|
305
|
+
entry.path = normalizeRelativePath(entry.path, { label: `projection entry ${index}.path` });
|
|
306
|
+
if (lock.schemaVersion === 2 && entry.ownership === "region") {
|
|
307
|
+
if (entry.target !== "ai-entry") fail("schema-invalid-enum", "region projection target must be ai-entry");
|
|
308
|
+
if (entry.regionId !== "project-context-ai-entry") fail("schema-invalid-enum", "AI Entry regionId is invalid");
|
|
309
|
+
string(entry.regionDigest, `${label}.regionDigest`);
|
|
310
|
+
if (!SHA256.test(entry.regionDigest)) fail("schema-invalid", "AI Entry regionDigest must be sha256");
|
|
311
|
+
if (entry.rendererVersion !== 1) fail("schema-version-unsupported", "AI Entry rendererVersion must be 1");
|
|
312
|
+
if (typeof entry.createdFile !== "boolean") fail("schema-invalid", `${label}.createdFile must be boolean`);
|
|
313
|
+
} else {
|
|
314
|
+
if (entry.target !== "agents" && entry.target !== "ruler") fail("schema-invalid-enum", "projection target is invalid");
|
|
315
|
+
uniqueStrings(entry.paths, `${label}.paths`);
|
|
316
|
+
entry.paths = entry.paths.map((value) => normalizeRelativePath(value, { allowRoot: true, label: "projection scope path" }));
|
|
317
|
+
for (const key of ["contractDigest", "bundleDigest", "contentDigest"]) {
|
|
318
|
+
string(entry[key], `${label}.${key}`);
|
|
319
|
+
if (!SHA256.test(entry[key])) fail("schema-invalid", `projection ${key} must be sha256`);
|
|
320
|
+
}
|
|
321
|
+
uniqueStrings(entry.itemIds, `${label}.itemIds`, { empty: true });
|
|
322
|
+
if (![1, 2, 3].includes(entry.rendererVersion)) {
|
|
323
|
+
fail("schema-version-unsupported", "projection rendererVersion must be 1, 2, or 3");
|
|
324
|
+
}
|
|
311
325
|
}
|
|
312
326
|
if (seen.has(entry.path)) fail("schema-duplicate", `projections lock contains duplicate path: ${entry.path}`);
|
|
313
327
|
seen.add(entry.path);
|
|
@@ -201,11 +201,11 @@ function buildItems(contract) {
|
|
|
201
201
|
}
|
|
202
202
|
|
|
203
203
|
function projectionStatus(codes) {
|
|
204
|
-
if (codes.
|
|
205
|
-
if (codes.
|
|
204
|
+
if (codes.some((code) => code.endsWith("ownership-conflict"))) return "conflict";
|
|
205
|
+
if (codes.some((code) => code.endsWith("missing"))) return "missing";
|
|
206
206
|
if (codes.includes("projection-unreadable") || codes.includes("projection-path-invalid")) return "unreadable";
|
|
207
207
|
if (codes.includes("projection-diverged")) return "diverged";
|
|
208
|
-
if (codes.some((code) => code === "projection-stale" || code
|
|
208
|
+
if (codes.some((code) => code === "projection-stale" || code.endsWith("renderer-stale") || code === "projection-item-missing")) {
|
|
209
209
|
return "stale";
|
|
210
210
|
}
|
|
211
211
|
return codes.length > 0 ? "attention" : "healthy";
|
|
@@ -214,7 +214,7 @@ function projectionStatus(codes) {
|
|
|
214
214
|
function buildProjections(project, findings) {
|
|
215
215
|
return project.projectionsLock.projections.map((entry) => {
|
|
216
216
|
const findingCodes = findings
|
|
217
|
-
.filter((finding) => finding.path === entry.path && finding.code.startsWith("projection-"))
|
|
217
|
+
.filter((finding) => finding.path === entry.path && (finding.code.startsWith("projection-") || finding.code.startsWith("ai-entry-")))
|
|
218
218
|
.map((finding) => finding.code);
|
|
219
219
|
return {
|
|
220
220
|
...structuredClone(entry),
|
|
@@ -567,10 +567,10 @@ function renderScopes(model) {
|
|
|
567
567
|
|
|
568
568
|
function renderProjection(projection) {
|
|
569
569
|
return `<details class="record projection"><summary><strong class="mono">${escapeHtml(projection.path)}</strong><span>${escapeHtml(projection.target)} · ${bi(`渲染器 ${projection.rendererVersion}`, `Renderer ${projection.rendererVersion}`)}</span>${statusBadge(projection.status)}</summary><div class="detail-body"><dl class="audit-grid">
|
|
570
|
-
${auditField(bi("范围路径", "Scope paths"), tokenList(projection.paths), { htmlTerm: true })}
|
|
571
|
-
${auditField(bi("知识项 ID", "Item IDs"), tokenList(projection.itemIds), { htmlTerm: true })}
|
|
570
|
+
${auditField(bi("范围路径", "Scope paths"), tokenList(projection.paths ?? []), { htmlTerm: true })}
|
|
571
|
+
${auditField(bi("知识项 ID", "Item IDs"), tokenList(projection.itemIds ?? []), { htmlTerm: true })}
|
|
572
572
|
${auditField(bi("问题代码", "Finding codes"), tokenList(projection.findingCodes), { htmlTerm: true })}
|
|
573
|
-
${auditField(bi("合同指纹", "Contract digest"), escapeHtml(projection.contractDigest), { mono: true, htmlTerm: true })}
|
|
573
|
+
${auditField(bi("合同指纹", "Contract digest"), escapeHtml(projection.contractDigest ?? projection.regionDigest ?? "not-applicable"), { mono: true, htmlTerm: true })}
|
|
574
574
|
</dl></div></details>`;
|
|
575
575
|
}
|
|
576
576
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readdir, readFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { sha256 } from "./canonical-json.mjs";
|
|
4
|
+
import { parseAiEntryRegion } from "./ai-entry.mjs";
|
|
4
5
|
import { sourceRegistrationShape, sourceStatus } from "./contract-schema.mjs";
|
|
5
6
|
import { DEFAULT_IGNORES, digestPath, digestPathIdentity, readSourceDigest } from "./source-reader.mjs";
|
|
6
7
|
|
|
@@ -199,7 +200,11 @@ export async function discoverProject(root, contract) {
|
|
|
199
200
|
const ruleContents = new Map();
|
|
200
201
|
for (const relative of rulePaths) {
|
|
201
202
|
const contents = await readFile(path.join(root, relative), "utf8");
|
|
202
|
-
|
|
203
|
+
const aiEntry = parseAiEntryRegion(contents);
|
|
204
|
+
const outsideEntry = aiEntry.state === "present"
|
|
205
|
+
? `${contents.slice(0, aiEntry.start)}${contents.slice(aiEntry.end)}`.replace(/^\ufeff/u, "").trim()
|
|
206
|
+
: null;
|
|
207
|
+
if (!contents.startsWith("<!-- managed-by: project-context;") && outsideEntry !== "") ruleContents.set(relative, contents);
|
|
203
208
|
}
|
|
204
209
|
const knownRulePaths = new Set(ruleContents.keys());
|
|
205
210
|
const aliasTargets = new Map();
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { canonicalJson, canonicalValue, digestJson, validateJsonValue } from "./canonical-json.mjs";
|
|
2
|
+
import { fail } from "./errors.mjs";
|
|
3
|
+
|
|
4
|
+
export const EVIDENCE_INPUT_SCHEMA_VERSION = 1;
|
|
5
|
+
export const EVIDENCE_BUNDLE_SCHEMA_VERSION = 1;
|
|
6
|
+
export const EVIDENCE_INPUT_MAX_UTF8_BYTES = 32 * 1024;
|
|
7
|
+
export const EVIDENCE_OUTPUT_MAX_UTF8_BYTES = 48 * 1024;
|
|
8
|
+
|
|
9
|
+
const ID = /^[a-z0-9]+(?:[.-][a-z0-9]+)*$/u;
|
|
10
|
+
const SHA256 = /^sha256:[a-f0-9]{64}$/u;
|
|
11
|
+
const FIELD_PATH = /^[a-z][A-Za-z0-9]*(?:\.[a-z][A-Za-z0-9]*)*$/u;
|
|
12
|
+
const CAPABILITIES = new Set(["setup", "takeover", "maintenance", "staged-context", "upgrade", "other-protocol"]);
|
|
13
|
+
const RESULTS = new Set(["passed", "degraded", "blocked", "failed"]);
|
|
14
|
+
const RUNTIMES = new Set(["node", "other", "redacted"]);
|
|
15
|
+
const PACKAGE_MANAGERS = new Set(["npm", "pnpm", "yarn", "bun", "other", "redacted"]);
|
|
16
|
+
const PROJECT_SHAPES = new Set(["single", "monorepo", "multi-context", "redacted"]);
|
|
17
|
+
const OPERATIONS = new Set([
|
|
18
|
+
"capabilities", "status", "setup-preview", "sync", "check", "context", "preflight", "stage-context",
|
|
19
|
+
"integration-review", "publish-entry-preview", "remove-entry-preview", "upgrade-baseline", "other-protocol",
|
|
20
|
+
]);
|
|
21
|
+
const REDACTION_METHODS = new Set(["removed", "generalized", "hashed", "redacted", "not-collected"]);
|
|
22
|
+
const STRUCTURALLY_FORBIDDEN_FIELDS = new Set([
|
|
23
|
+
"approval", "approve", "authorization", "base64", "body", "branch", "branchName", "businessCode", "chat", "codeBody",
|
|
24
|
+
"cookie", "credential", "credentials", "cwd", "decision", "destination", "diff", "environmentVariables", "externalId", "fileName", "filename",
|
|
25
|
+
"generatedAt", "hostName", "hostname", "itemIds", "items", "locator", "log", "logs", "name", "output", "path",
|
|
26
|
+
"paths", "priority", "projectId", "projectName", "release", "repository", "repositoryUrl", "reviewer", "sourceBody",
|
|
27
|
+
"password", "secret", "sourceIds", "sources", "targetVersion", "timestamp", "token", "upload", "url", "userName", "username", "value",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
function invalid(message, details) {
|
|
31
|
+
fail("evidence-input-invalid", message, { details });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function object(value, label) {
|
|
35
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) invalid(`${label} must be an object`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function exactKeys(value, allowed, label) {
|
|
39
|
+
for (const key of Object.keys(value)) {
|
|
40
|
+
if (STRUCTURALLY_FORBIDDEN_FIELDS.has(key)) {
|
|
41
|
+
fail("evidence-redaction-blocked", `${label} contains a structurally forbidden field: ${key}`, { details: { field: key } });
|
|
42
|
+
}
|
|
43
|
+
if (!allowed.has(key)) invalid(`${label} contains unknown field: ${key}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function requiredKeys(value, required, label) {
|
|
48
|
+
for (const key of required) if (!Object.hasOwn(value, key)) invalid(`${label}.${key} is required`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function string(value, label) {
|
|
52
|
+
if (typeof value !== "string" || value.length === 0) invalid(`${label} must be a non-empty string`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function stableId(value, label) {
|
|
56
|
+
string(value, label);
|
|
57
|
+
if (!ID.test(value)) invalid(`${label} must use stable lowercase dot/kebab naming`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function summary(value, label) {
|
|
61
|
+
string(value, label);
|
|
62
|
+
if (/\r|\n|\u2028|\u2029/u.test(value)) invalid(`${label} must be a single line`);
|
|
63
|
+
if (Buffer.byteLength(value, "utf8") > 500) invalid(`${label} exceeds 500 UTF-8 bytes`);
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function enumValue(value, allowed, label) {
|
|
68
|
+
if (!allowed.has(value)) invalid(`${label} is invalid`);
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function array(value, label, max) {
|
|
73
|
+
if (!Array.isArray(value)) invalid(`${label} must be an array`);
|
|
74
|
+
if (value.length > max) invalid(`${label} exceeds ${max} entries`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function uniqueSorted(values, identity = (value) => canonicalJson(value)) {
|
|
78
|
+
return [...new Map(values.map((value) => [identity(value), value])).values()]
|
|
79
|
+
.sort((left, right) => identity(left).localeCompare(identity(right)));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizeCodes(value, label) {
|
|
83
|
+
array(value, label, 32);
|
|
84
|
+
const normalized = value.map((entry, index) => {
|
|
85
|
+
stableId(entry, `${label}[${index}]`);
|
|
86
|
+
return entry;
|
|
87
|
+
});
|
|
88
|
+
return uniqueSorted(normalized, (entry) => entry);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function normalizeEnvironment(value) {
|
|
92
|
+
object(value, "environment");
|
|
93
|
+
const keys = new Set(["runtime", "runtimeMajor", "packageManager", "projectShape"]);
|
|
94
|
+
exactKeys(value, keys, "environment");
|
|
95
|
+
requiredKeys(value, keys, "environment");
|
|
96
|
+
if (!Number.isInteger(value.runtimeMajor) || value.runtimeMajor < 0 || value.runtimeMajor > 999) {
|
|
97
|
+
invalid("environment.runtimeMajor must be an integer from 0 through 999");
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
runtime: enumValue(value.runtime, RUNTIMES, "environment.runtime"),
|
|
101
|
+
runtimeMajor: value.runtimeMajor,
|
|
102
|
+
packageManager: enumValue(value.packageManager, PACKAGE_MANAGERS, "environment.packageManager"),
|
|
103
|
+
projectShape: enumValue(value.projectShape, PROJECT_SHAPES, "environment.projectShape"),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function normalizeObservation(value, label) {
|
|
108
|
+
object(value, label);
|
|
109
|
+
const keys = new Set(["code", "summary"]);
|
|
110
|
+
exactKeys(value, keys, label);
|
|
111
|
+
requiredKeys(value, keys, label);
|
|
112
|
+
stableId(value.code, `${label}.code`);
|
|
113
|
+
return { code: value.code, summary: summary(value.summary, `${label}.summary`) };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function normalizeReproduction(value) {
|
|
117
|
+
array(value, "reproduction", 20);
|
|
118
|
+
return value.map((entry, index) => {
|
|
119
|
+
const label = `reproduction[${index}]`;
|
|
120
|
+
object(entry, label);
|
|
121
|
+
const keys = new Set(["operation", "outcome", "findingCodes"]);
|
|
122
|
+
exactKeys(entry, keys, label);
|
|
123
|
+
requiredKeys(entry, keys, label);
|
|
124
|
+
return {
|
|
125
|
+
operation: enumValue(entry.operation, OPERATIONS, `${label}.operation`),
|
|
126
|
+
outcome: enumValue(entry.outcome, RESULTS, `${label}.outcome`),
|
|
127
|
+
findingCodes: normalizeCodes(entry.findingCodes, `${label}.findingCodes`),
|
|
128
|
+
};
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function normalizeArtifacts(value) {
|
|
133
|
+
array(value, "artifacts", 32);
|
|
134
|
+
const normalized = value.map((entry, index) => {
|
|
135
|
+
const label = `artifacts[${index}]`;
|
|
136
|
+
object(entry, label);
|
|
137
|
+
const keys = new Set(["kind", "digest"]);
|
|
138
|
+
exactKeys(entry, keys, label);
|
|
139
|
+
requiredKeys(entry, keys, label);
|
|
140
|
+
stableId(entry.kind, `${label}.kind`);
|
|
141
|
+
if (typeof entry.digest !== "string" || !SHA256.test(entry.digest)) invalid(`${label}.digest must be sha256`);
|
|
142
|
+
return { kind: entry.kind, digest: entry.digest };
|
|
143
|
+
});
|
|
144
|
+
return uniqueSorted(normalized);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function normalizeRedactions(value) {
|
|
148
|
+
array(value, "redactions", 32);
|
|
149
|
+
const normalized = value.map((entry, index) => {
|
|
150
|
+
const label = `redactions[${index}]`;
|
|
151
|
+
object(entry, label);
|
|
152
|
+
const keys = new Set(["field", "method"]);
|
|
153
|
+
exactKeys(entry, keys, label);
|
|
154
|
+
requiredKeys(entry, keys, label);
|
|
155
|
+
string(entry.field, `${label}.field`);
|
|
156
|
+
if (!FIELD_PATH.test(entry.field)) invalid(`${label}.field must be a dotted input field path`);
|
|
157
|
+
return { field: entry.field, method: enumValue(entry.method, REDACTION_METHODS, `${label}.method`) };
|
|
158
|
+
});
|
|
159
|
+
return uniqueSorted(normalized);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function normalizeEvidenceInput(input) {
|
|
163
|
+
try {
|
|
164
|
+
validateJsonValue(input);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
invalid("evidence input must be JSON-compatible", { reason: error.message });
|
|
167
|
+
}
|
|
168
|
+
object(input, "evidence input");
|
|
169
|
+
const keys = new Set([
|
|
170
|
+
"schemaVersion", "capability", "result", "environment", "expected", "observed", "errorCodes",
|
|
171
|
+
"reproduction", "artifacts", "redactions",
|
|
172
|
+
]);
|
|
173
|
+
exactKeys(input, keys, "evidence input");
|
|
174
|
+
requiredKeys(input, keys, "evidence input");
|
|
175
|
+
if (input.schemaVersion !== EVIDENCE_INPUT_SCHEMA_VERSION) invalid("evidence input schemaVersion must be 1");
|
|
176
|
+
const normalized = {
|
|
177
|
+
schemaVersion: EVIDENCE_INPUT_SCHEMA_VERSION,
|
|
178
|
+
capability: enumValue(input.capability, CAPABILITIES, "capability"),
|
|
179
|
+
result: enumValue(input.result, RESULTS, "result"),
|
|
180
|
+
environment: normalizeEnvironment(input.environment),
|
|
181
|
+
expected: normalizeObservation(input.expected, "expected"),
|
|
182
|
+
observed: normalizeObservation(input.observed, "observed"),
|
|
183
|
+
errorCodes: normalizeCodes(input.errorCodes, "errorCodes"),
|
|
184
|
+
reproduction: normalizeReproduction(input.reproduction),
|
|
185
|
+
artifacts: normalizeArtifacts(input.artifacts),
|
|
186
|
+
redactions: normalizeRedactions(input.redactions),
|
|
187
|
+
};
|
|
188
|
+
if (Buffer.byteLength(canonicalJson(normalized), "utf8") > EVIDENCE_INPUT_MAX_UTF8_BYTES) {
|
|
189
|
+
fail("evidence-input-budget-exceeded", `canonical evidence input exceeds ${EVIDENCE_INPUT_MAX_UTF8_BYTES} UTF-8 bytes`);
|
|
190
|
+
}
|
|
191
|
+
return canonicalValue(normalized);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function finalizeEvidenceBundle(bundleWithoutDigest) {
|
|
195
|
+
const canonical = canonicalValue(bundleWithoutDigest);
|
|
196
|
+
const bundle = canonicalValue({ ...canonical, bundleDigest: digestJson(canonical) });
|
|
197
|
+
if (Buffer.byteLength(canonicalJson(bundle), "utf8") > EVIDENCE_OUTPUT_MAX_UTF8_BYTES) {
|
|
198
|
+
fail("evidence-output-budget-exceeded", `canonical evidence output exceeds ${EVIDENCE_OUTPUT_MAX_UTF8_BYTES} UTF-8 bytes`);
|
|
199
|
+
}
|
|
200
|
+
return bundle;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function verifyEvidenceBundleDigest(bundle) {
|
|
204
|
+
object(bundle, "evidence bundle");
|
|
205
|
+
const copy = structuredClone(bundle);
|
|
206
|
+
const actual = copy.bundleDigest;
|
|
207
|
+
delete copy.bundleDigest;
|
|
208
|
+
return typeof actual === "string" && actual === digestJson(canonicalValue(copy));
|
|
209
|
+
}
|