frontend-project-context 1.3.1 → 1.7.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.
Files changed (67) hide show
  1. package/CHANGELOG.md +51 -2
  2. package/README.md +156 -40
  3. package/UPGRADING.md +55 -1
  4. package/docs/04-PROGRAM-DESIGN.md +34 -4
  5. package/docs/05-ACCEPTANCE-CONTRACT.md +40 -3
  6. package/docs/08-INSTALLATION-AND-DISTRIBUTION.md +67 -22
  7. package/docs/14-FORMAL-RELEASE-READINESS.md +30 -1
  8. package/docs/18-BRANCH-AWARE-STAGED-CONTEXT-DESIGN.md +2 -2
  9. package/docs/19-POST-1.3.1-AI-TAKEOVER-EVIDENCE-AND-UPGRADE-PLAN.md +579 -0
  10. package/docs/20-PHASE-A-AI-TAKEOVER-AND-HEALTH-CLOSURE-DESIGN.md +535 -0
  11. package/docs/21-PHASE-B-EVIDENCE-FEEDBACK-PROTOCOL-DESIGN.md +347 -0
  12. package/docs/22-PHASE-C-TARGET-UPGRADE-PROTOCOL-DESIGN.md +398 -0
  13. package/docs/23-ADAPTIVE-BOUNDED-TASK-CONTEXT-DESIGN.md +432 -0
  14. package/docs/24-A130-REAL-HOST-TARGET-PROJECT-COMPARISON.md +210 -0
  15. package/docs/25-REAL-PROJECT-SOURCE-OF-TRUTH-MAINTENANCE-DESIGN.md +409 -0
  16. package/docs/26-A130-QUALITY-CLOSURE-AND-ADAPTIVE-DELIVERY-REPAIR-DESIGN.md +609 -0
  17. package/docs/README.md +38 -6
  18. package/docs/USER-AND-AI-OPERATION-MANUAL.md +840 -0
  19. package/examples/README.md +29 -2
  20. package/examples/package.json +6 -2
  21. package/migration-manifest.json +110 -0
  22. package/package.json +3 -2
  23. package/schemas/action-plan.schema.json +31 -3
  24. package/schemas/adaptive-context-bundle.schema.json +70 -0
  25. package/schemas/capabilities.schema.json +64 -18
  26. package/schemas/context-query.schema.json +69 -0
  27. package/schemas/coverage-audit.schema.json +32 -0
  28. package/schemas/evidence-bundle.schema.json +64 -0
  29. package/schemas/evidence-input.schema.json +82 -0
  30. package/schemas/host-promotion-evidence.schema.json +33 -0
  31. package/schemas/migration-manifest.schema.json +29 -0
  32. package/schemas/migration-plan.schema.json +32 -0
  33. package/schemas/project-status.schema.json +75 -0
  34. package/schemas/projection-lock.schema.json +48 -0
  35. package/schemas/review-bundle.schema.json +3 -3
  36. package/schemas/routing-index.schema.json +58 -0
  37. package/schemas/truth-reconciliation-input.schema.json +60 -0
  38. package/schemas/truth-reconciliation-review-bundle.schema.json +155 -0
  39. package/schemas/upgrade-assessment.schema.json +48 -0
  40. package/schemas/upgrade-result-bundle.schema.json +35 -0
  41. package/src/project-context/a130-evaluation.mjs +91 -0
  42. package/src/project-context/adaptive-context-schema.mjs +392 -0
  43. package/src/project-context/adaptive-context.mjs +547 -0
  44. package/src/project-context/ai-entry.mjs +320 -0
  45. package/src/project-context/assist.mjs +4 -2
  46. package/src/project-context/capabilities.mjs +62 -17
  47. package/src/project-context/checker.mjs +24 -6
  48. package/src/project-context/cli.mjs +113 -3
  49. package/src/project-context/contract-schema.mjs +30 -16
  50. package/src/project-context/dashboard-model.mjs +4 -4
  51. package/src/project-context/dashboard-renderer.mjs +3 -3
  52. package/src/project-context/discovery.mjs +13 -8
  53. package/src/project-context/evidence-schema.mjs +209 -0
  54. package/src/project-context/evidence.mjs +99 -0
  55. package/src/project-context/exchange-schema.mjs +23 -12
  56. package/src/project-context/exchange.mjs +26 -4
  57. package/src/project-context/maintenance.mjs +4 -4
  58. package/src/project-context/migration-manifest.mjs +168 -0
  59. package/src/project-context/project-status.mjs +157 -0
  60. package/src/project-context/projection-store.mjs +8 -1
  61. package/src/project-context/renderer.mjs +75 -1
  62. package/src/project-context/source-reader.mjs +63 -30
  63. package/src/project-context/task-context.mjs +14 -2
  64. package/src/project-context/truth-reconciliation-schema.mjs +488 -0
  65. package/src/project-context/truth-reconciliation.mjs +543 -0
  66. package/src/project-context/upgrade-schema.mjs +219 -0
  67. package/src/project-context/upgrade.mjs +494 -0
@@ -1,4 +1,5 @@
1
1
  import { buildSetupAssistBundle, buildSyncAssistBundle } from "./assist.mjs";
2
+ import { buildAdaptiveContextBundleFiles, buildAdaptiveContextDeliveryFiles, buildCoverageAuditFiles, indexContextFiles } from "./adaptive-context.mjs";
2
3
  import { approvePendingItems, approveProposal } from "./approver.mjs";
3
4
  import { buildItemProposal, registerSource } from "./authoring.mjs";
4
5
  import { blockingContextFindings, checkExitCode, checkProject } from "./checker.mjs";
@@ -8,21 +9,32 @@ import { buildDashboardModel } from "./dashboard-model.mjs";
8
9
  import { renderDashboardHtml } from "./dashboard-renderer.mjs";
9
10
  import { discoverProject } from "./discovery.mjs";
10
11
  import { ProjectContextError, fail } from "./errors.mjs";
12
+ import { buildEvidenceBundleFile } from "./evidence.mjs";
11
13
  import { buildCapabilities, preflightActionPlanFile } from "./exchange.mjs";
14
+ import { PERMANENT_BOUNDARIES } from "./capabilities.mjs";
12
15
  import { normalizeProposalPath } from "./exchange-schema.mjs";
13
16
  import { atomicCreateFileOrSame, atomicWriteFile, readJsonFile } from "./io.mjs";
14
17
  import { acceptSourceChange, deprecateItem, deprecateSource, reviewSource, reviseItem } from "./maintenance.mjs";
15
18
  import { normalizeRelativePath, resolveExistingInside, resolveProjectRoot, resolveWritableInside } from "./path-policy.mjs";
16
19
  import { initializeProject, inspectProjectInitialization, loadProject } from "./project-store.mjs";
17
20
  import { publishProjection } from "./projection-store.mjs";
21
+ import { publishAiEntry, removeAiEntry } from "./ai-entry.mjs";
22
+ import { buildProjectStatus } from "./project-status.mjs";
18
23
  import { renderContextBundle } from "./renderer.mjs";
19
24
  import { buildIntegrationReviewBundleFiles, buildStageContextBundleFiles } from "./task-context.mjs";
25
+ import { applyMigrationPlanFile, buildMigrationPlanFile, buildUpgradeAssessment } from "./upgrade.mjs";
26
+ import { buildTruthReconciliationReviewFiles } from "./truth-reconciliation.mjs";
20
27
 
21
28
  const HELP = `project-context — model-neutral project contract compiler
22
29
 
23
30
  Usage:
24
31
  project-context init --project PATH --id ID --name NAME [--write] [--json]
25
32
  project-context capabilities --project PATH [--json]
33
+ project-context status --project PATH [--json]
34
+ project-context evidence --project PATH --input FILE [--json]
35
+ project-context upgrade-check --project PATH --from-version VERSION [--json]
36
+ project-context upgrade-plan --project PATH --assessment FILE [--json]
37
+ project-context upgrade-apply --project PATH --plan FILE [--write] [--json]
26
38
  project-context setup --project PATH --id ID --name NAME [--output FILE] [--write] [--json]
27
39
  project-context register --project PATH --id SOURCE_ID --kind KIND [--path PATH] [--pointer POINTER] [--reference TEXT] [--write] [--json]
28
40
  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]
@@ -34,13 +46,19 @@ Usage:
34
46
  project-context discover --project PATH [--output FILE --write] [--json]
35
47
  project-context approve --project PATH (--proposal FILE | --pending) --ids ID... --by NAME [--rationale TEXT] [--write] [--json | --full-json]
36
48
  project-context context --project PATH --path RELATIVE_PATH... [--task TEXT] [--locale zh-CN|en|all] [--json]
49
+ project-context context-query --project PATH --input FILE [--previous FILE] [--json | --prompt]
50
+ project-context coverage-audit --project PATH [--changed-path RELATIVE_PATH...] [--json]
51
+ project-context index-context --project PATH [--write] [--json]
37
52
  project-context publish --project PATH --target agents|ruler --output FILE [--path RELATIVE_PATH...] [--write] [--json]
53
+ project-context publish-entry --project PATH --output AGENTS.md [--write] [--json]
54
+ project-context remove-entry --project PATH --output AGENTS.md [--write] [--json]
38
55
  project-context check --project PATH [--json]
39
56
  project-context dashboard --project PATH [--json]
40
57
  project-context sync --project PATH [--changed-path RELATIVE_PATH...] [--json]
41
58
  project-context preflight --project PATH --plan FILE [--json]
42
59
  project-context stage-context --project PATH --plan FILE --stage STAGE_ID [--receipt FILE...] [--receipt-bundle FILE...] [--changed-path RELATIVE_PATH...] [--json]
43
60
  project-context integration-review --project PATH --plan FILE [--receipt FILE...] [--receipt-bundle FILE...] [--main-changed-path RELATIVE_PATH...] [--branch-changed-path RELATIVE_PATH...] [--json]
61
+ project-context reconcile-truth --project PATH --input FILE... [--previous-review FILE] [--json]
44
62
 
45
63
  All commands are read-only unless their own --write flag is present.
46
64
  `;
@@ -48,15 +66,20 @@ const VALUE_FLAGS = new Set([
48
66
  "project", "id", "name", "output", "proposal", "by", "task", "target", "rationale",
49
67
  "kind", "pointer", "reference", "subject", "value", "value-json", "statement", "scope", "scope-path",
50
68
  "verification", "verification-source", "verification-expected-json",
51
- "expected-digest", "expected-item-digest", "expected-source-digest", "locale", "plan", "stage",
69
+ "expected-digest", "expected-item-digest", "expected-source-digest", "locale", "plan", "stage", "from-version", "assessment", "previous", "previous-review",
52
70
  ]);
53
71
  const LIST_FLAGS = new Set([
54
- "ids", "path", "changed-path", "sources", "overrides", "affected-items", "receipt", "receipt-bundle", "main-changed-path", "branch-changed-path",
72
+ "ids", "path", "input", "changed-path", "sources", "overrides", "affected-items", "receipt", "receipt-bundle", "main-changed-path", "branch-changed-path",
55
73
  ]);
56
- const BOOLEAN_FLAGS = new Set(["write", "json", "full-json", "help", "pending"]);
74
+ const BOOLEAN_FLAGS = new Set(["write", "json", "full-json", "help", "pending", "prompt"]);
57
75
  const COMMAND_OPTIONS = new Map([
58
76
  ["init", new Set(["project", "id", "name", "write", "json", "help"])],
59
77
  ["capabilities", new Set(["project", "json", "help"])],
78
+ ["status", new Set(["project", "json", "help"])],
79
+ ["evidence", new Set(["project", "input", "json", "help"])],
80
+ ["upgrade-check", new Set(["project", "from-version", "json", "help"])],
81
+ ["upgrade-plan", new Set(["project", "assessment", "json", "help"])],
82
+ ["upgrade-apply", new Set(["project", "plan", "write", "json", "help"])],
60
83
  ["setup", new Set(["project", "id", "name", "output", "write", "json", "help"])],
61
84
  ["register", new Set(["project", "id", "kind", "path", "pointer", "reference", "write", "json", "help"])],
62
85
  ["propose", new Set([
@@ -81,13 +104,19 @@ const COMMAND_OPTIONS = new Map([
81
104
  ["discover", new Set(["project", "output", "write", "json", "help"])],
82
105
  ["approve", new Set(["project", "proposal", "pending", "ids", "by", "rationale", "write", "json", "full-json", "help"])],
83
106
  ["context", new Set(["project", "path", "task", "locale", "json", "help"])],
107
+ ["context-query", new Set(["project", "input", "previous", "json", "prompt", "help"])],
108
+ ["coverage-audit", new Set(["project", "changed-path", "json", "help"])],
109
+ ["index-context", new Set(["project", "write", "json", "help"])],
84
110
  ["publish", new Set(["project", "target", "output", "path", "write", "json", "help"])],
111
+ ["publish-entry", new Set(["project", "output", "write", "json", "help"])],
112
+ ["remove-entry", new Set(["project", "output", "write", "json", "help"])],
85
113
  ["check", new Set(["project", "json", "help"])],
86
114
  ["dashboard", new Set(["project", "json", "help"])],
87
115
  ["sync", new Set(["project", "changed-path", "json", "help"])],
88
116
  ["preflight", new Set(["project", "plan", "json", "help"])],
89
117
  ["stage-context", new Set(["project", "plan", "stage", "receipt", "receipt-bundle", "changed-path", "json", "help"])],
90
118
  ["integration-review", new Set(["project", "plan", "receipt", "receipt-bundle", "main-changed-path", "branch-changed-path", "json", "help"])],
119
+ ["reconcile-truth", new Set(["project", "input", "previous-review", "json", "help"])],
91
120
  ]);
92
121
 
93
122
  export function parseArgs(argv) {
@@ -323,6 +352,51 @@ async function runCommand(command, options) {
323
352
  ].join("\n") + "\n";
324
353
  return { exitCode: 0, stdout: jsonOrText(options, capabilities, summary), stderr: "" };
325
354
  }
355
+ if (command === "status") {
356
+ const result = await buildProjectStatus(root, PERMANENT_BOUNDARIES);
357
+ const summary = `Project Context: ${result.status.initialization.state}; health ${result.status.health}; AI Entry ${result.status.entry.state}.\n`;
358
+ return { exitCode: result.exitCode, stdout: jsonOrText(options, result.status, summary), stderr: "" };
359
+ }
360
+ if (command === "evidence") {
361
+ const bundle = await buildEvidenceBundleFile(root, oneListValue(options, "input"));
362
+ const summary = [
363
+ `Evidence ${bundle.result}; project health ${bundle.projectContext.health}.`,
364
+ `Finding codes: ${bundle.projectContext.findingCodes.join(", ") || "none"}.`,
365
+ `Bundle digest: ${bundle.bundleDigest}.`,
366
+ "Human review is required before transfer.",
367
+ ].join("\n") + "\n";
368
+ return { exitCode: 0, stdout: jsonOrText(options, bundle, summary), stderr: "" };
369
+ }
370
+ if (command === "upgrade-check") {
371
+ const assessment = await buildUpgradeAssessment(root, required(options, "from-version"));
372
+ const summary = [
373
+ `Upgrade ${assessment.fromVersion} -> ${assessment.targetVersion}: ${assessment.state}.`,
374
+ `Health: ${assessment.health}; rollback: ${assessment.rollbackClass}.`,
375
+ `Finding codes: ${assessment.findingCodes.join(", ") || "none"}.`,
376
+ `Assessment digest: ${assessment.assessmentDigest}.`,
377
+ ].join("\n") + "\n";
378
+ return { exitCode: assessment.state === "blocked" || assessment.state === "not-applicable" ? 1 : 0, stdout: jsonOrText(options, assessment, summary), stderr: "" };
379
+ }
380
+ if (command === "upgrade-plan") {
381
+ const plan = await buildMigrationPlanFile(root, required(options, "assessment"));
382
+ const summary = [
383
+ `Upgrade plan: ${plan.nextAction.kind}.`,
384
+ `Targets: ${plan.nextAction.targets.join(", ") || "none"}.`,
385
+ `Writes: ${plan.nextAction.writes}; human review: ${plan.requiresHumanReview}.`,
386
+ `Plan digest: ${plan.planDigest}.`,
387
+ ].join("\n") + "\n";
388
+ return { exitCode: 0, stdout: jsonOrText(options, plan, summary), stderr: "" };
389
+ }
390
+ if (command === "upgrade-apply") {
391
+ const applied = await applyMigrationPlanFile(root, required(options, "plan"), { write: options.write });
392
+ const summary = [
393
+ `Upgrade ${applied.result.mode}: ${applied.result.coreMigration}.`,
394
+ `Action: ${applied.result.action.kind}; written: ${applied.result.written}.`,
395
+ `Overall upgrade: ${applied.result.overallUpgrade}.`,
396
+ `Result digest: ${applied.result.resultDigest}.`,
397
+ ].join("\n") + "\n";
398
+ return { exitCode: applied.exitCode, stdout: jsonOrText(options, applied.result, summary), stderr: "" };
399
+ }
326
400
  if (command === "init") {
327
401
  const result = await initializeProject(root, required(options, "id"), required(options, "name"), options.write);
328
402
  return {
@@ -347,6 +421,26 @@ async function runCommand(command, options) {
347
421
  const summary = `Stage context ${bundle.status}: ${bundle.stage.id}; ${bundle.contractItems.length} contract item(s), ${bundle.readTargets.length} read target(s), ${bundle.budget.usedUtf8Bytes}/${bundle.budget.maxUtf8Bytes} UTF-8 bytes.\n`;
348
422
  return { exitCode: bundle.status === "ready" ? 0 : 1, stdout: jsonOrText(options, bundle, summary), stderr: "" };
349
423
  }
424
+ if (command === "context-query") {
425
+ if (options.json && options.prompt) fail("argument-conflict", "context-query accepts only one of --json or --prompt");
426
+ if (options.prompt) {
427
+ const result = await buildAdaptiveContextDeliveryFiles(root, oneListValue(options, "input"), { previousPath: options.previous });
428
+ return { exitCode: result.bundle.taskHealth === "ready" ? 0 : 1, stdout: result.content, stderr: "" };
429
+ }
430
+ const bundle = await buildAdaptiveContextBundleFiles(root, oneListValue(options, "input"), { previousPath: options.previous });
431
+ const summary = `Adaptive context ${bundle.taskHealth}: ${bundle.hydratedItems.length} hydrated, ${bundle.retainedItemIds.length} retained, ${bundle.deferredItems.length} deferred item(s); audit ${bundle.budget.audit.usedUtf8Bytes}/${bundle.budget.audit.maxUtf8Bytes}, delivery ${bundle.budget.delivery.usedUtf8Bytes}/${bundle.budget.delivery.maxUtf8Bytes} UTF-8 bytes.\n`;
432
+ return { exitCode: bundle.taskHealth === "ready" ? 0 : 1, stdout: jsonOrText(options, bundle, summary), stderr: "" };
433
+ }
434
+ if (command === "coverage-audit") {
435
+ const audit = await buildCoverageAuditFiles(root, options["changed-path"] ?? []);
436
+ const summary = `Registration coverage ${audit.registrationCoverage}: ${audit.categories["review-required"].length} review-required candidate(s).\n`;
437
+ return { exitCode: audit.registrationCoverage === "review-required" ? 1 : 0, stdout: jsonOrText(options, audit, summary), stderr: "" };
438
+ }
439
+ if (command === "index-context") {
440
+ const index = await indexContextFiles(root, { write: options.write });
441
+ const summary = `Routing index ${options.write ? "written" : "preview"}: ${index.items.length} item(s), ${index.sources.length} source(s); ${index.indexDigest}.\n`;
442
+ return { exitCode: 0, stdout: jsonOrText(options, index, summary), stderr: "" };
443
+ }
350
444
  if (command === "integration-review") {
351
445
  const bundle = await buildIntegrationReviewBundleFiles(root, required(options, "plan"), {
352
446
  receiptPaths: options.receipt ?? [],
@@ -357,6 +451,11 @@ async function runCommand(command, options) {
357
451
  const summary = `Integration review ${bundle.status}: ${bundle.findings.length} finding(s), ${bundle.contractOverlapItemIds.length} contract overlap(s), ${bundle.decisionCandidates.length} decision candidate(s).\n`;
358
452
  return { exitCode: bundle.status === "reviewable" ? 0 : 1, stdout: jsonOrText(options, bundle, summary), stderr: "" };
359
453
  }
454
+ if (command === "reconcile-truth") {
455
+ const bundle = await buildTruthReconciliationReviewFiles(root, required(options, "input"), { previousReviewPath: options["previous-review"] });
456
+ const summary = `Truth reconciliation ${bundle.summary.taskHealth}: ${bundle.summary.collisionGroups} collision group(s), ${bundle.summary.findings} finding(s); existing delivery ${bundle.summary.existingDelivery}.\n`;
457
+ return { exitCode: bundle.summary.taskHealth === "ready" ? 0 : 1, stdout: jsonOrText(options, bundle, summary), stderr: "" };
458
+ }
360
459
  const project = await loadProject(root);
361
460
  if (command === "register") {
362
461
  const result = await registerSource(root, project, {
@@ -539,6 +638,17 @@ async function runCommand(command, options) {
539
638
  const summary = options.write ? `${result.action}: ${result.entry.path}\n` : `Preview ${result.action}: ${result.entry.path}\n\n${result.content}`;
540
639
  return { exitCode: 0, stdout: options.json ? prettyCanonicalJson(result) : summary, stderr: "" };
541
640
  }
641
+ if (command === "publish-entry" || command === "remove-entry") {
642
+ const operation = command === "publish-entry" ? publishAiEntry : removeAiEntry;
643
+ const result = await operation(root, project, {
644
+ output: required(options, "output"),
645
+ write: options.write,
646
+ });
647
+ const summary = options.write
648
+ ? `${result.action}: ${result.impact.paths[0]}\n`
649
+ : `Preview ${result.action}: ${result.impact.paths[0]}\n\n${result.proposed.region ?? ""}`;
650
+ return { exitCode: 0, stdout: options.json ? prettyCanonicalJson(result) : summary, stderr: "" };
651
+ }
542
652
  fail("command-unknown", `unknown command: ${command}`);
543
653
  }
544
654
 
@@ -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 !== 1 || !Array.isArray(lock.projections)) fail("schema-invalid", "projections lock is invalid");
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
- exactKeys(
296
- entry,
297
- new Set(["path", "target", "paths", "contractDigest", "bundleDigest", "contentDigest", "itemIds", "rendererVersion"]),
298
- `projection entry ${index}`,
299
- );
300
- entry.path = normalizeRelativePath(entry.path, { label: `projection entry ${index}.path` });
301
- if (entry.target !== "agents" && entry.target !== "ruler") fail("schema-invalid-enum", "projection target is invalid");
302
- uniqueStrings(entry.paths, `projection entry ${index}.paths`);
303
- entry.paths = entry.paths.map((value) => normalizeRelativePath(value, { allowRoot: true, label: "projection scope path" }));
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
- uniqueStrings(entry.itemIds, `projection entry ${index}.itemIds`, { empty: true });
309
- if (![1, 2, 3].includes(entry.rendererVersion)) {
310
- fail("schema-version-unsupported", "projection rendererVersion must be 1, 2, or 3");
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 (![1, 2, 3].includes(entry.rendererVersion)) fail("schema-version-unsupported", "AI Entry rendererVersion must be 1, 2, or 3");
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.includes("projection-ownership-conflict")) return "conflict";
205
- if (codes.includes("projection-missing")) return "missing";
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 === "projection-renderer-stale" || code === "projection-item-missing")) {
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
 
@@ -144,7 +145,7 @@ export async function discoverProject(root, contract) {
144
145
  id: "fact-package-manager",
145
146
  subject: "project.package-manager",
146
147
  value: packageJson.packageManager,
147
- statement: `The project declares package manager ${packageJson.packageManager}.`,
148
+ statement: `项目声明使用包管理器 ${packageJson.packageManager}。`,
148
149
  source,
149
150
  }));
150
151
  }
@@ -154,7 +155,7 @@ export async function discoverProject(root, contract) {
154
155
  id: `fact-${scriptId}`,
155
156
  subject: `project.${scriptId.replaceAll("-", ".")}`,
156
157
  value: packageJson.scripts[name],
157
- statement: `The project declares the ${name} script.`,
158
+ statement: `项目声明了 ${name} 脚本。`,
158
159
  source,
159
160
  }));
160
161
  }
@@ -166,7 +167,7 @@ export async function discoverProject(root, contract) {
166
167
  id: `fact-${dependencyId}`,
167
168
  subject: `project.${dependencyId.replaceAll("-", ".")}`,
168
169
  value: dependencies[name],
169
- statement: `The project declares ${name} as a dependency.`,
170
+ statement: `项目声明 ${name} 为依赖。`,
170
171
  source,
171
172
  }));
172
173
  }
@@ -180,7 +181,7 @@ export async function discoverProject(root, contract) {
180
181
  id: `fact-${configId}`,
181
182
  subject: `project.${configId.replaceAll("-", ".")}`,
182
183
  value: true,
183
- statement: `The project contains ${relative}.`,
184
+ statement: `项目包含 ${relative}。`,
184
185
  source,
185
186
  }));
186
187
  }
@@ -190,7 +191,7 @@ export async function discoverProject(root, contract) {
190
191
  id: `fact-${generatedId("top-level-directory", relative)}`,
191
192
  subject: "project.structure.top-level-directory",
192
193
  value: relative,
193
- statement: `The project contains the top-level directory ${relative}.`,
194
+ statement: `项目包含顶层目录 ${relative}。`,
194
195
  source,
195
196
  scope: { kind: "path-prefix", path: relative },
196
197
  }));
@@ -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
- if (!contents.startsWith("<!-- managed-by: project-context;")) ruleContents.set(relative, contents);
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();
@@ -216,7 +221,7 @@ export async function discoverProject(root, contract) {
216
221
  kind: "reference",
217
222
  subject: `agent-rules.${slug(relative)}`,
218
223
  value: relative,
219
- statement: `Review the existing Agent instruction source at ${relative}.`,
224
+ statement: `阅读现有 AI Agent 指令源 ${relative}。`,
220
225
  source,
221
226
  scope: relative.includes("/") && !relative.startsWith(".")
222
227
  ? { kind: "path-prefix", path: path.posix.dirname(relative) }
@@ -238,7 +243,7 @@ export async function discoverProject(root, contract) {
238
243
  kind: "reference",
239
244
  subject: `registered-source.${source.id.replaceAll("-", ".")}`,
240
245
  value: source.path ?? source.reference,
241
- statement: `Review the explicitly registered project source ${source.id}.`,
246
+ statement: `阅读显式登记的项目源 ${source.id}。`,
242
247
  source: source.id,
243
248
  }));
244
249
  } catch {
@@ -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
+ }