frontend-project-context 1.6.0 → 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 (52) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +92 -46
  3. package/UPGRADING.md +22 -1
  4. package/docs/05-ACCEPTANCE-CONTRACT.md +20 -1
  5. package/docs/08-INSTALLATION-AND-DISTRIBUTION.md +44 -21
  6. package/docs/14-FORMAL-RELEASE-READINESS.md +9 -5
  7. package/docs/19-POST-1.3.1-AI-TAKEOVER-EVIDENCE-AND-UPGRADE-PLAN.md +5 -5
  8. package/docs/20-PHASE-A-AI-TAKEOVER-AND-HEALTH-CLOSURE-DESIGN.md +11 -11
  9. package/docs/22-PHASE-C-TARGET-UPGRADE-PROTOCOL-DESIGN.md +4 -4
  10. package/docs/23-ADAPTIVE-BOUNDED-TASK-CONTEXT-DESIGN.md +432 -0
  11. package/docs/24-A130-REAL-HOST-TARGET-PROJECT-COMPARISON.md +210 -0
  12. package/docs/25-REAL-PROJECT-SOURCE-OF-TRUTH-MAINTENANCE-DESIGN.md +409 -0
  13. package/docs/26-A130-QUALITY-CLOSURE-AND-ADAPTIVE-DELIVERY-REPAIR-DESIGN.md +609 -0
  14. package/docs/README.md +22 -6
  15. package/docs/USER-AND-AI-OPERATION-MANUAL.md +73 -30
  16. package/examples/README.md +4 -4
  17. package/examples/package.json +1 -1
  18. package/migration-manifest.json +30 -8
  19. package/package.json +2 -2
  20. package/schemas/adaptive-context-bundle.schema.json +70 -0
  21. package/schemas/capabilities.schema.json +20 -6
  22. package/schemas/context-query.schema.json +69 -0
  23. package/schemas/coverage-audit.schema.json +32 -0
  24. package/schemas/evidence-bundle.schema.json +2 -2
  25. package/schemas/host-promotion-evidence.schema.json +33 -0
  26. package/schemas/migration-manifest.schema.json +3 -3
  27. package/schemas/migration-plan.schema.json +2 -2
  28. package/schemas/projection-lock.schema.json +1 -1
  29. package/schemas/routing-index.schema.json +58 -0
  30. package/schemas/truth-reconciliation-input.schema.json +60 -0
  31. package/schemas/truth-reconciliation-review-bundle.schema.json +155 -0
  32. package/schemas/upgrade-assessment.schema.json +2 -2
  33. package/schemas/upgrade-result-bundle.schema.json +1 -1
  34. package/src/project-context/a130-evaluation.mjs +91 -0
  35. package/src/project-context/adaptive-context-schema.mjs +392 -0
  36. package/src/project-context/adaptive-context.mjs +547 -0
  37. package/src/project-context/ai-entry.mjs +9 -9
  38. package/src/project-context/assist.mjs +4 -2
  39. package/src/project-context/capabilities.mjs +18 -0
  40. package/src/project-context/checker.mjs +4 -3
  41. package/src/project-context/cli.mjs +40 -5
  42. package/src/project-context/contract-schema.mjs +1 -1
  43. package/src/project-context/discovery.mjs +7 -7
  44. package/src/project-context/exchange-schema.mjs +6 -5
  45. package/src/project-context/maintenance.mjs +2 -2
  46. package/src/project-context/migration-manifest.mjs +7 -5
  47. package/src/project-context/renderer.mjs +75 -1
  48. package/src/project-context/source-reader.mjs +63 -30
  49. package/src/project-context/task-context.mjs +14 -2
  50. package/src/project-context/truth-reconciliation-schema.mjs +488 -0
  51. package/src/project-context/truth-reconciliation.mjs +543 -0
  52. package/src/project-context/upgrade-schema.mjs +5 -1
@@ -10,7 +10,7 @@
10
10
  "kind": {"const": "upgrade-result-bundle"},
11
11
  "product": {"type": "object", "additionalProperties": false, "required": ["name"], "properties": {"name": {"const": "frontend-project-context"}}},
12
12
  "fromVersion": {"type": "string"},
13
- "targetVersion": {"const": "1.6.0"},
13
+ "targetVersion": {"const": "1.7.0"},
14
14
  "manifestDigest": {"$ref": "#/$defs/digest"},
15
15
  "planDigest": {"$ref": "#/$defs/digest"},
16
16
  "mode": {"enum": ["preview", "write"]},
@@ -0,0 +1,91 @@
1
+ import { canonicalJson, digestJson } from "./canonical-json.mjs";
2
+ import { fail } from "./errors.mjs";
3
+
4
+ function median(values) {
5
+ const sorted = [...values].sort((left, right) => left - right);
6
+ return sorted[Math.floor(sorted.length / 2)];
7
+ }
8
+
9
+ function sameStringSet(left = [], right = []) {
10
+ return canonicalJson([...left].sort()) === canonicalJson([...right].sort());
11
+ }
12
+
13
+ function pairEntries(entries) {
14
+ return Array.from({ length: entries.length / 2 }, (_, index) => entries.slice(index * 2, index * 2 + 2));
15
+ }
16
+
17
+ function pairArms(pair) {
18
+ return {
19
+ full: pair.find((run) => run.arm === "full"),
20
+ candidate: pair.find((run) => run.arm === "candidate"),
21
+ };
22
+ }
23
+
24
+ function assertFailureEvidence(run) {
25
+ if (run.score < 8 && (!Array.isArray(run.failedChecks) || run.failedChecks.length === 0 || run.failedChecks.some((entry) => typeof entry !== "string" || !entry))) {
26
+ fail("a130-run-result-invalid", "a failed A-130 run must name its failed checks");
27
+ }
28
+ }
29
+
30
+ export function validateA130EvaluationPlan(plan, contract, readablePaths) {
31
+ if (!plan || typeof plan !== "object" || Array.isArray(plan)) fail("a130-evaluation-invalid", "evaluation plan must be an object");
32
+ const allowed = new Set(["arms", "cases", "forbiddenPaths", "model", "oracleApproval", "wrapper"]);
33
+ if (Object.keys(plan).some((key) => !allowed.has(key)) || !Array.isArray(plan.arms) || !Array.isArray(plan.cases)) fail("a130-evaluation-invalid", "evaluation plan structure is invalid");
34
+ if (plan.arms.length !== 2 || plan.arms.some((arm) => !arm || typeof arm !== "object" || arm.format !== plan.arms[0].format || canonicalJson(arm.wrapper) !== canonicalJson(plan.wrapper))) fail("arm-format-mismatch", "full and candidate arms must use the same delivery format and wrapper");
35
+ const approvedIds = new Set(contract.items.filter((item) => item.status === "approved").map((item) => item.id));
36
+ const readable = new Set(readablePaths);
37
+ for (const scenario of plan.cases) {
38
+ if (!Array.isArray(scenario.oracle) || scenario.oracle.length === 0) fail("oracle-evidence-missing", "every evaluation case needs a non-empty oracle");
39
+ for (const oracle of scenario.oracle) {
40
+ if (!oracle.contractItemId && !oracle.sourcePath) fail("oracle-evidence-missing", "oracle truth must be available to both arms through Contract or readable source");
41
+ if (oracle.contractItemId && !approvedIds.has(oracle.contractItemId)) fail("oracle-evidence-missing", `oracle item ${oracle.contractItemId} is not approved`);
42
+ if (oracle.sourcePath && !readable.has(oracle.sourcePath)) fail("oracle-evidence-missing", `oracle source ${oracle.sourcePath} is not readable by both arms`);
43
+ if (plan.forbiddenPaths.includes(oracle.sourcePath)) fail("oracle-evidence-forbidden", "forbidden oracle locators cannot be model evidence");
44
+ }
45
+ }
46
+ const oracleDigest = digestJson(plan.cases.map(({ id, oracle }) => ({ id, oracle })));
47
+ const approval = plan.oracleApproval;
48
+ if (!approval || typeof approval !== "object" || Array.isArray(approval) || Object.keys(approval).some((key) => !["at", "by", "oracleDigest"].includes(key)) || typeof approval.by !== "string" || !approval.by || typeof approval.at !== "string" || !approval.at || approval.oracleDigest !== oracleDigest) {
49
+ fail("oracle-human-approval-missing", "the exact A-130 oracle table must be digest-bound and explicitly approved by a human before Provider use");
50
+ }
51
+ return { planDigest: digestJson(plan), status: "ready", armFormat: plan.arms[0].format, oracleDigest, wrapperDigest: digestJson(plan.wrapper) };
52
+ }
53
+
54
+ export function evaluateA130Runs(runs) {
55
+ if (!Array.isArray(runs) || runs.length !== 8) fail("a130-run-set-invalid", "A-130 requires one S pair and three order-balanced L pairs (eight runs maximum)");
56
+ const expectedOrders = { S: ["full", "candidate"], L: ["full", "candidate", "candidate", "full", "full", "candidate"] };
57
+ for (const caseId of ["S", "L"]) {
58
+ const entries = runs.filter((run) => run.caseId === caseId);
59
+ if (canonicalJson(entries.map((run) => run.arm)) !== canonicalJson(expectedOrders[caseId])) fail("a130-run-order-invalid", `${caseId} does not match its frozen pair order`);
60
+ if (entries.some((run) => run.forbiddenAccess || run.targetWritten || run.gitChanged)) fail("a130-safety-gate-failed", `${caseId} contains a forbidden Host action`);
61
+ entries.forEach(assertFailureEvidence);
62
+ }
63
+ const small = runs.filter((run) => run.caseId === "S");
64
+ if (small[0].promptDigest !== small[1].promptDigest) fail("a130-small-prompt-mismatch", "A-130-S prompts must be byte-identical before Provider comparison");
65
+ if (small[0].score !== 8 || small[1].score !== 8) {
66
+ if (small[0].score < 8 && small[1].score < 8 && sameStringSet(small[0].failedChecks, small[1].failedChecks)) fail("a130-fixture-inconclusive", "byte-identical A-130-S arms failed the same checks; review the task/oracle without changing product selection");
67
+ fail("a130-provider-variance-inconclusive", "byte-identical A-130-S arms produced different quality and cannot identify a product defect");
68
+ }
69
+ const large = runs.filter((run) => run.caseId === "L");
70
+ const full = large.filter((run) => run.arm === "full");
71
+ const candidate = large.filter((run) => run.arm === "candidate");
72
+ if (candidate.some((run) => run.deliveryMode !== "adaptive" || run.requiredRecall !== 1)) fail("a130-adaptive-evidence-defect", "A-130-L candidate runs must be real adaptive deliveries with complete required-item recall");
73
+ const attributedFailures = [];
74
+ for (const pair of pairEntries(large)) {
75
+ const { full: fullRun, candidate: candidateRun } = pairArms(pair);
76
+ if (fullRun.score < 8 && candidateRun.score < 8 && sameStringSet(fullRun.failedChecks, candidateRun.failedChecks)) fail("a130-fixture-inconclusive", "paired A-130-L arms failed the same checks; review the task/oracle without changing product selection");
77
+ if (fullRun.score < 8) fail("a130-baseline-inconclusive", "an A-130-L full baseline failed, so the pair cannot identify an adaptive product defect");
78
+ if (candidateRun.score < 8) {
79
+ const attribution = candidateRun.attribution;
80
+ if (!attribution || attribution.kind !== "delivery-evidence-gap" || !Array.isArray(attribution.itemIds) || attribution.itemIds.length === 0) fail("a130-causality-inconclusive", "a candidate-only failure needs a governed delivery-evidence attribution before it can be a product defect");
81
+ attributedFailures.push(attribution.itemIds.slice().sort());
82
+ }
83
+ }
84
+ if (attributedFailures.length > 0) {
85
+ const reproducible = attributedFailures.some((ids, index) => attributedFailures.some((other, otherIndex) => otherIndex > index && canonicalJson(ids) === canonicalJson(other)));
86
+ if (!reproducible) fail("a130-causality-inconclusive", "an attributed adaptive failure was not reproduced in a second balanced pair");
87
+ fail("a130-adaptive-evidence-defect", "a governed delivery-evidence gap reproducibly caused candidate-only quality failure");
88
+ }
89
+ if (median(candidate.map((run) => run.promptBytes)) > median(full.map((run) => run.promptBytes)) * 0.7 || median(candidate.map((run) => run.inputTokens)) > median(full.map((run) => run.inputTokens)) || median(candidate.map((run) => run.commandCount)) > median(full.map((run) => run.commandCount)) || median(candidate.map((run) => run.wallSeconds)) > median(full.map((run) => run.wallSeconds)) * 1.1) fail("a130-efficiency-gate-failed", "A-130-L median efficiency gates failed");
90
+ return { status: "passed", pairs: 4, runs: 8 };
91
+ }
@@ -0,0 +1,392 @@
1
+ import { canonicalJson, canonicalValue, digestJson, validateJsonValue } from "./canonical-json.mjs";
2
+ import { fail } from "./errors.mjs";
3
+ import { normalizeRelativePath } from "./path-policy.mjs";
4
+
5
+ export const CONTEXT_QUERY_SCHEMA_VERSION = 2;
6
+ export const ADAPTIVE_CONTEXT_BUNDLE_SCHEMA_VERSION = 2;
7
+ export const ROUTING_INDEX_SCHEMA_VERSION = 2;
8
+ export const COVERAGE_AUDIT_SCHEMA_VERSION = 1;
9
+ export const ADAPTIVE_SELECTOR_VERSION = 2;
10
+ export const ROUTING_INDEX_PATH = ".project-context/derived/routing-index.json";
11
+
12
+ const SHA256 = /^sha256:[a-f0-9]{64}$/u;
13
+ const ID = /^[a-z0-9]+(?:[.-][a-z0-9]+)*$/u;
14
+ const AUTHORITY_FIELDS = new Set(["approval", "approve", "by", "command", "provider", "shell", "write"]);
15
+
16
+ function invalid(kind, message, details = {}) { fail(`${kind}-schema-invalid`, message, { details }); }
17
+ function object(value, kind, label) {
18
+ if (!value || typeof value !== "object" || Array.isArray(value)) invalid(kind, `${label} must be an object`);
19
+ }
20
+ function exactKeys(value, allowed, kind, label, required = allowed) {
21
+ for (const key of Object.keys(value)) {
22
+ if (AUTHORITY_FIELDS.has(key)) invalid(kind, `${label} contains forbidden authority or execution field: ${key}`);
23
+ if (!allowed.has(key)) invalid(kind, `${label} contains unknown field: ${key}`);
24
+ }
25
+ for (const key of required) if (!Object.hasOwn(value, key)) invalid(kind, `${label}.${key} is required`);
26
+ }
27
+ function text(value, kind, label, options = {}) {
28
+ if (typeof value !== "string" || (!options.empty && value.length === 0)) invalid(kind, `${label} must be a non-empty string`);
29
+ return value;
30
+ }
31
+ function id(value, kind, label) {
32
+ text(value, kind, label);
33
+ if (!ID.test(value)) invalid(kind, `${label} must use stable lowercase dot/kebab naming`);
34
+ return value;
35
+ }
36
+ function digest(value, kind, label) {
37
+ text(value, kind, label);
38
+ if (!SHA256.test(value)) invalid(kind, `${label} must be sha256`);
39
+ return value;
40
+ }
41
+ function integer(value, kind, label, minimum = 0) {
42
+ if (!Number.isSafeInteger(value) || value < minimum) invalid(kind, `${label} must be an integer >= ${minimum}`);
43
+ return value;
44
+ }
45
+ function enumeration(value, allowed, kind, label) {
46
+ if (!allowed.includes(value)) invalid(kind, `${label} is invalid`);
47
+ return value;
48
+ }
49
+ function uniqueStrings(values, kind, label, options = {}) {
50
+ if (!Array.isArray(values) || (!options.empty && values.length === 0)) invalid(kind, `${label} must be ${options.empty ? "an" : "a non-empty"} array`);
51
+ const seen = new Set();
52
+ for (const value of values) {
53
+ text(value, kind, `${label} entry`);
54
+ if (seen.has(value)) invalid(kind, `${label} contains duplicate value: ${value}`);
55
+ seen.add(value);
56
+ }
57
+ const sorted = [...values].sort((left, right) => left.localeCompare(right));
58
+ if (canonicalJson(values) !== canonicalJson(sorted)) invalid(kind, `${label} must be stably sorted`);
59
+ return sorted;
60
+ }
61
+ function ids(values, kind, label, options = {}) {
62
+ const result = uniqueStrings(values, kind, label, { empty: options.empty ?? true });
63
+ result.forEach((entry) => id(entry, kind, `${label} entry`));
64
+ return result;
65
+ }
66
+ function snapshots(value, kind, label) {
67
+ object(value, kind, label);
68
+ const keys = new Set(["contract", "projectionsLock", "sourcesLock"]);
69
+ exactKeys(value, keys, kind, label);
70
+ return {
71
+ contract: digest(value.contract, kind, `${label}.contract`),
72
+ sourcesLock: digest(value.sourcesLock, kind, `${label}.sourcesLock`),
73
+ projectionsLock: digest(value.projectionsLock, kind, `${label}.projectionsLock`),
74
+ };
75
+ }
76
+ function projectPaths(values, kind, label, options = {}) {
77
+ const original = uniqueStrings(values, kind, label, { empty: options.empty ?? true });
78
+ const normalized = original.map((value) => {
79
+ try { return normalizeRelativePath(value, { allowRoot: true, label }); }
80
+ catch (error) { invalid(kind, `${label} entry must stay inside the project`, { path: value, reason: error.code ?? "invalid-path" }); }
81
+ });
82
+ if (canonicalJson(normalized) !== canonicalJson(original)) invalid(kind, `${label} must contain normalized paths`);
83
+ return normalized;
84
+ }
85
+ function byteBudget(value, kind, label, targetName) {
86
+ object(value, kind, label);
87
+ const keys = new Set([targetName, "maxUtf8Bytes"]);
88
+ exactKeys(value, keys, kind, label);
89
+ const result = { [targetName]: integer(value[targetName], kind, `${label}.${targetName}`, 1), maxUtf8Bytes: integer(value.maxUtf8Bytes, kind, `${label}.maxUtf8Bytes`, 1) };
90
+ if (result[targetName] > result.maxUtf8Bytes) invalid(kind, `${label} soft target cannot exceed hard limit`);
91
+ return result;
92
+ }
93
+
94
+ export function validateContextQuery(input) {
95
+ const kind = "context-query";
96
+ object(input, kind, kind);
97
+ const keys = new Set(["budget", "expansion", "freshness", "kind", "level", "projectId", "schemaVersion", "snapshots", "task"]);
98
+ exactKeys(input, keys, kind, kind);
99
+ if (input.schemaVersion !== CONTEXT_QUERY_SCHEMA_VERSION || input.kind !== kind) invalid(kind, "context-query identity is invalid");
100
+ id(input.projectId, kind, "context-query.projectId");
101
+ const level = enumeration(input.level, ["initial", "expanded", "complete"], kind, "context-query.level");
102
+ const freshness = enumeration(input.freshness, ["snapshot-and-signal-bound", "strict-current"], kind, "context-query.freshness");
103
+ object(input.task, kind, "context-query.task");
104
+ const taskKeys = new Set(["changedPaths", "itemIds", "paths", "text", "topics"]);
105
+ exactKeys(input.task, taskKeys, kind, "context-query.task");
106
+ object(input.budget, kind, "context-query.budget");
107
+ const budgetKeys = new Set(["audit", "delivery", "readTargets"]);
108
+ exactKeys(input.budget, budgetKeys, kind, "context-query.budget");
109
+ object(input.budget.readTargets, kind, "context-query.budget.readTargets");
110
+ exactKeys(input.budget.readTargets, new Set(["max", "target"]), kind, "context-query.budget.readTargets");
111
+ const budget = {
112
+ audit: byteBudget(input.budget.audit, kind, "context-query.budget.audit", "targetUtf8Bytes"),
113
+ delivery: byteBudget(input.budget.delivery, kind, "context-query.budget.delivery", "completeBelowUtf8Bytes"),
114
+ readTargets: { target: integer(input.budget.readTargets.target, kind, "context-query.budget.readTargets.target", 1), max: integer(input.budget.readTargets.max, kind, "context-query.budget.readTargets.max", 1) },
115
+ };
116
+ if (budget.readTargets.target > budget.readTargets.max) invalid(kind, "context-query read target soft target cannot exceed hard limit");
117
+ let expansion = null;
118
+ if (input.expansion !== null) {
119
+ object(input.expansion, kind, "context-query.expansion");
120
+ const expansionKeys = new Set(["previousBundleDigest", "requestedItemIds", "requestedSourceIds", "requestedSubjects"]);
121
+ exactKeys(input.expansion, expansionKeys, kind, "context-query.expansion");
122
+ expansion = {
123
+ previousBundleDigest: digest(input.expansion.previousBundleDigest, kind, "context-query.expansion.previousBundleDigest"),
124
+ requestedItemIds: ids(input.expansion.requestedItemIds, kind, "context-query.expansion.requestedItemIds"),
125
+ requestedSubjects: ids(input.expansion.requestedSubjects, kind, "context-query.expansion.requestedSubjects"),
126
+ requestedSourceIds: ids(input.expansion.requestedSourceIds, kind, "context-query.expansion.requestedSourceIds"),
127
+ };
128
+ }
129
+ if (level === "expanded" && expansion === null) invalid(kind, "expanded context-query requires expansion");
130
+ if (level !== "expanded" && expansion !== null) invalid(kind, `${level} context-query cannot contain expansion`);
131
+ return canonicalValue({
132
+ schemaVersion: 2, kind, projectId: input.projectId,
133
+ snapshots: snapshots(input.snapshots, kind, "context-query.snapshots"),
134
+ task: {
135
+ text: text(input.task.text, kind, "context-query.task.text"),
136
+ paths: projectPaths(input.task.paths, kind, "context-query.task.paths", { empty: false }),
137
+ topics: uniqueStrings(input.task.topics, kind, "context-query.task.topics", { empty: true }),
138
+ itemIds: ids(input.task.itemIds, kind, "context-query.task.itemIds"),
139
+ changedPaths: projectPaths(input.task.changedPaths, kind, "context-query.task.changedPaths"),
140
+ },
141
+ level, freshness, budget, expansion,
142
+ });
143
+ }
144
+
145
+ function validateSealedArtifact(input, { kind, schemaVersion, digestField }) {
146
+ object(input, kind, kind);
147
+ if (input.schemaVersion !== schemaVersion || input.kind !== kind) invalid(kind, `${kind} identity is invalid`);
148
+ digest(input[digestField], kind, `${kind}.${digestField}`);
149
+ const copy = structuredClone(input);
150
+ delete copy[digestField];
151
+ if (digestJson(copy) !== input[digestField]) invalid(kind, `${kind}.${digestField} does not match canonical content`);
152
+ return canonicalValue(input);
153
+ }
154
+ export function sealArtifact(input, digestField) {
155
+ const copy = canonicalValue(input);
156
+ delete copy[digestField];
157
+ return canonicalValue({ ...copy, [digestField]: digestJson(copy) });
158
+ }
159
+ function array(value, kind, label) {
160
+ if (!Array.isArray(value)) invalid(kind, `${label} must be an array`);
161
+ return value;
162
+ }
163
+ function optionalDigest(value, kind, label) {
164
+ if (value !== null) digest(value, kind, label);
165
+ }
166
+ function scope(value, kind, label) {
167
+ object(value, kind, label);
168
+ if (value.kind === "project") exactKeys(value, new Set(["kind"]), kind, label);
169
+ else {
170
+ exactKeys(value, new Set(["kind", "path"]), kind, label);
171
+ enumeration(value.kind, ["path-prefix", "file"], kind, `${label}.kind`);
172
+ projectPaths([value.path], kind, `${label}.path`, { empty: false });
173
+ }
174
+ }
175
+ function locator(value, kind, label) {
176
+ object(value, kind, label);
177
+ if (Object.hasOwn(value, "reference")) {
178
+ exactKeys(value, new Set(["reference"]), kind, label);
179
+ text(value.reference, kind, `${label}.reference`);
180
+ return;
181
+ }
182
+ const keys = Object.hasOwn(value, "pointer") ? new Set(["path", "pointer"]) : new Set(["path"]);
183
+ exactKeys(value, keys, kind, label);
184
+ projectPaths([value.path], kind, `${label}.path`, { empty: false });
185
+ if (Object.hasOwn(value, "pointer")) text(value.pointer, kind, `${label}.pointer`, { empty: true });
186
+ }
187
+
188
+ export function validateRoutingIndex(input) {
189
+ const kind = "routing-index";
190
+ const value = validateSealedArtifact(input, { kind, schemaVersion: 2, digestField: "indexDigest" });
191
+ const keys = new Set(["indexDigest", "items", "kind", "projectId", "schemaVersion", "selectorVersion", "snapshots", "sources"]);
192
+ exactKeys(value, keys, kind, kind);
193
+ id(value.projectId, kind, `${kind}.projectId`);
194
+ snapshots(value.snapshots, kind, `${kind}.snapshots`);
195
+ if (value.selectorVersion !== 2) invalid(kind, `${kind}.selectorVersion is unsupported`);
196
+ const itemIds = [];
197
+ for (const [index, item] of array(value.items, kind, `${kind}.items`).entries()) {
198
+ object(item, kind, `${kind}.items[${index}]`);
199
+ exactKeys(item, new Set(["id", "itemDigest", "kind", "scope", "sourceIds", "subject", "terms"]), kind, `${kind}.items[${index}]`);
200
+ itemIds.push(id(item.id, kind, `${kind}.items[${index}].id`));
201
+ text(item.kind, kind, `${kind}.items[${index}].kind`);
202
+ id(item.subject, kind, `${kind}.items[${index}].subject`);
203
+ enumeration(item.kind, ["fact", "policy", "reference", "validation-description"], kind, `${kind}.items[${index}].kind`);
204
+ scope(item.scope, kind, `${kind}.items[${index}].scope`);
205
+ digest(item.itemDigest, kind, `${kind}.items[${index}].itemDigest`);
206
+ ids(item.sourceIds, kind, `${kind}.items[${index}].sourceIds`);
207
+ uniqueStrings(item.terms, kind, `${kind}.items[${index}].terms`, { empty: true });
208
+ }
209
+ ids(itemIds, kind, `${kind}.item ids`);
210
+ const sourceIds = [];
211
+ for (const [index, source] of array(value.sources, kind, `${kind}.sources`).entries()) {
212
+ object(source, kind, `${kind}.sources[${index}]`);
213
+ exactKeys(source, new Set(["id", "itemIds", "kind", "locator", "sourceDigest"]), kind, `${kind}.sources[${index}]`);
214
+ sourceIds.push(id(source.id, kind, `${kind}.sources[${index}].id`));
215
+ text(source.kind, kind, `${kind}.sources[${index}].kind`);
216
+ ids(source.itemIds, kind, `${kind}.sources[${index}].itemIds`);
217
+ digest(source.sourceDigest, kind, `${kind}.sources[${index}].sourceDigest`);
218
+ locator(source.locator, kind, `${kind}.sources[${index}].locator`);
219
+ }
220
+ ids(sourceIds, kind, `${kind}.source ids`);
221
+ return value;
222
+ }
223
+
224
+ export function validateAdaptiveContextBundle(input) {
225
+ const kind = "adaptive-context-bundle";
226
+ const value = validateSealedArtifact(input, { kind, schemaVersion: 2, digestField: "bundleDigest" });
227
+ const keys = new Set(["budget", "bundleDigest", "deferredItems", "delivery", "evidenceEdges", "excluded", "expansionDepth", "findings", "globalHealth", "guarantees", "hydratedItems", "kind", "level", "metrics", "previousBundleDigest", "project", "queryDigest", "readTargets", "retainedItemIds", "routing", "schemaVersion", "snapshots", "sources", "targetStates", "task", "taskDigest", "taskHealth"]);
228
+ exactKeys(value, keys, kind, kind, new Set([...keys].filter((entry) => entry !== "previousBundleDigest")));
229
+ digest(value.queryDigest, kind, `${kind}.queryDigest`);
230
+ digest(value.taskDigest, kind, `${kind}.taskDigest`);
231
+ if (value.previousBundleDigest !== undefined) digest(value.previousBundleDigest, kind, `${kind}.previousBundleDigest`);
232
+ snapshots(value.snapshots, kind, `${kind}.snapshots`);
233
+ const level = enumeration(value.level, ["initial", "expanded", "complete"], kind, `${kind}.level`);
234
+ const expansionDepth = integer(value.expansionDepth, kind, `${kind}.expansionDepth`);
235
+ if ((level === "expanded") !== (value.previousBundleDigest !== undefined) || expansionDepth !== (level === "expanded" ? 1 : 0)) invalid(kind, "adaptive context expansion lineage is invalid");
236
+ object(value.project, kind, `${kind}.project`);
237
+ exactKeys(value.project, new Set(["id", "name"]), kind, `${kind}.project`);
238
+ id(value.project.id, kind, `${kind}.project.id`);
239
+ text(value.project.name, kind, `${kind}.project.name`);
240
+ object(value.task, kind, `${kind}.task`);
241
+ exactKeys(value.task, new Set(["changedPaths", "itemIds", "paths", "text", "topics"]), kind, `${kind}.task`);
242
+ text(value.task.text, kind, `${kind}.task.text`);
243
+ projectPaths(value.task.paths, kind, `${kind}.task.paths`, { empty: false });
244
+ uniqueStrings(value.task.topics, kind, `${kind}.task.topics`, { empty: true });
245
+ ids(value.task.itemIds, kind, `${kind}.task.itemIds`);
246
+ projectPaths(value.task.changedPaths, kind, `${kind}.task.changedPaths`);
247
+ object(value.routing, kind, `${kind}.routing`);
248
+ exactKeys(value.routing, new Set(["indexDigest", "indexState", "rejectedDigest"]), kind, `${kind}.routing`, new Set(["indexDigest", "indexState"]));
249
+ digest(value.routing.indexDigest, kind, `${kind}.routing.indexDigest`);
250
+ enumeration(value.routing.indexState, ["current", "invalid-rebuilt-in-memory", "stale-rebuilt-in-memory", "missing-rebuilt-in-memory"], kind, `${kind}.routing.indexState`);
251
+ if (value.routing.rejectedDigest !== undefined) digest(value.routing.rejectedDigest, kind, `${kind}.routing.rejectedDigest`);
252
+ object(value.guarantees, kind, `${kind}.guarantees`);
253
+ exactKeys(value.guarantees, new Set(["declaredDependencyCoverage", "freshness", "mandatoryCoverage", "registrationCoverage", "retrievalStatus", "semanticCompleteness"]), kind, `${kind}.guarantees`);
254
+ text(value.guarantees.registrationCoverage, kind, `${kind}.guarantees.registrationCoverage`);
255
+ enumeration(value.guarantees.mandatoryCoverage, ["complete", "blocked"], kind, `${kind}.guarantees.mandatoryCoverage`);
256
+ enumeration(value.guarantees.declaredDependencyCoverage, ["complete", "missing", "not-declared"], kind, `${kind}.guarantees.declaredDependencyCoverage`);
257
+ enumeration(value.guarantees.retrievalStatus, ["matched", "no-candidate", "ambiguous", "complete"], kind, `${kind}.guarantees.retrievalStatus`);
258
+ if (value.guarantees.semanticCompleteness !== "not-claimed") invalid(kind, "semantic completeness cannot be claimed");
259
+ enumeration(value.guarantees.freshness, ["strict-current", "snapshot-and-signal-bound"], kind, `${kind}.guarantees.freshness`);
260
+ enumeration(value.globalHealth, ["clean", "attention", "conflict", "not-checked", "snapshot-stale"], kind, `${kind}.globalHealth`);
261
+ enumeration(value.taskHealth, ["ready", "needs-expansion", "blocked"], kind, `${kind}.taskHealth`);
262
+ ids(value.retainedItemIds, kind, `${kind}.retainedItemIds`);
263
+ for (const collection of ["hydratedItems", "deferredItems", "sources", "evidenceEdges", "readTargets", "findings", "targetStates"]) array(value[collection], kind, `${kind}.${collection}`);
264
+ const hydratedIds = [];
265
+ for (const [index, item] of value.hydratedItems.entries()) {
266
+ const label = `${kind}.hydratedItems[${index}]`;
267
+ object(item, kind, label);
268
+ exactKeys(item, new Set(["id", "itemDigest", "kind", "overrides", "scope", "selectionReasons", "sourceIds", "statement", "subject", "value", "verification"]), kind, label, new Set(["id", "itemDigest", "kind", "overrides", "scope", "selectionReasons", "sourceIds", "statement", "subject", "value"]));
269
+ hydratedIds.push(id(item.id, kind, `${label}.id`));
270
+ enumeration(item.kind, ["fact", "policy", "reference", "validation-description"], kind, `${label}.kind`);
271
+ id(item.subject, kind, `${label}.subject`);
272
+ text(item.statement, kind, `${label}.statement`);
273
+ scope(item.scope, kind, `${label}.scope`);
274
+ ids(item.sourceIds, kind, `${label}.sourceIds`);
275
+ ids(item.overrides, kind, `${label}.overrides`);
276
+ uniqueStrings(item.selectionReasons, kind, `${label}.selectionReasons`, { empty: false });
277
+ digest(item.itemDigest, kind, `${label}.itemDigest`);
278
+ validateJsonValue(item.value);
279
+ if (item.verification !== undefined) validateJsonValue(item.verification);
280
+ }
281
+ ids(hydratedIds, kind, `${kind}.hydrated item ids`);
282
+ const deferredIds = [];
283
+ for (const [index, item] of value.deferredItems.entries()) {
284
+ const label = `${kind}.deferredItems[${index}]`;
285
+ object(item, kind, label);
286
+ exactKeys(item, new Set(["id", "itemDigest", "kind", "reason", "scope", "sourceIds", "subject"]), kind, label);
287
+ deferredIds.push(id(item.id, kind, `${label}.id`));
288
+ enumeration(item.kind, ["fact", "policy", "reference", "validation-description"], kind, `${label}.kind`);
289
+ id(item.subject, kind, `${label}.subject`);
290
+ scope(item.scope, kind, `${label}.scope`);
291
+ ids(item.sourceIds, kind, `${label}.sourceIds`);
292
+ digest(item.itemDigest, kind, `${label}.itemDigest`);
293
+ if (item.reason !== "applicable-not-selected") invalid(kind, `${label}.reason is invalid`);
294
+ }
295
+ ids(deferredIds, kind, `${kind}.deferred item ids`);
296
+ const sourceIds = [];
297
+ for (const [index, source] of value.sources.entries()) {
298
+ const label = `${kind}.sources[${index}]`;
299
+ object(source, kind, label);
300
+ exactKeys(source, new Set(["actualDigest", "expectedDigest", "freshness", "id", "kind", "locator", "sourceDigest"]), kind, label, new Set(["freshness", "id", "kind", "locator", "sourceDigest"]));
301
+ sourceIds.push(id(source.id, kind, `${label}.id`));
302
+ text(source.kind, kind, `${label}.kind`);
303
+ locator(source.locator, kind, `${label}.locator`);
304
+ digest(source.sourceDigest, kind, `${label}.sourceDigest`);
305
+ enumeration(source.freshness, ["current", "drifted", "unreadable", "unverifiable-non-local"], kind, `${label}.freshness`);
306
+ if (source.expectedDigest !== undefined) optionalDigest(source.expectedDigest, kind, `${label}.expectedDigest`);
307
+ if (source.actualDigest !== undefined) optionalDigest(source.actualDigest, kind, `${label}.actualDigest`);
308
+ }
309
+ ids(sourceIds, kind, `${kind}.source ids`);
310
+ for (const [index, edge] of value.evidenceEdges.entries()) {
311
+ const label = `${kind}.evidenceEdges[${index}]`;
312
+ object(edge, kind, label);
313
+ exactKeys(edge, new Set(["itemId", "sourceId"]), kind, label);
314
+ id(edge.itemId, kind, `${label}.itemId`);
315
+ id(edge.sourceId, kind, `${label}.sourceId`);
316
+ }
317
+ for (const [index, target] of value.readTargets.entries()) {
318
+ const label = `${kind}.readTargets[${index}]`;
319
+ object(target, kind, label);
320
+ exactKeys(target, new Set(["path", "reason", "sourceId"]), kind, label, new Set(["path", "reason"]));
321
+ projectPaths([target.path], kind, `${label}.path`, { empty: false });
322
+ enumeration(target.reason, ["task-target", "host-changed-path-signal", "contract-source"], kind, `${label}.reason`);
323
+ if (target.sourceId !== undefined) id(target.sourceId, kind, `${label}.sourceId`);
324
+ }
325
+ const findingKeys = new Set(["actual", "actualDigest", "code", "conflicts", "expected", "expectedDigest", "itemId", "itemIds", "limit", "path", "paths", "projectFinding", "reason", "required", "requiredItemId", "severity", "sourceId", "state", "subject", "target"]);
326
+ for (const [index, finding] of value.findings.entries()) {
327
+ const label = `${kind}.findings[${index}]`;
328
+ object(finding, kind, label);
329
+ exactKeys(finding, findingKeys, kind, label, new Set(["code", "severity"]));
330
+ text(finding.code, kind, `${label}.code`);
331
+ enumeration(finding.severity, ["attention", "needs-expansion", "blocked"], kind, `${label}.severity`);
332
+ for (const key of ["itemId", "requiredItemId", "sourceId", "subject"]) if (finding[key] !== undefined) id(finding[key], kind, `${label}.${key}`);
333
+ if (finding.itemIds !== undefined) ids(finding.itemIds, kind, `${label}.itemIds`);
334
+ if (finding.paths !== undefined) projectPaths(finding.paths, kind, `${label}.paths`);
335
+ if (finding.path !== undefined) projectPaths([finding.path], kind, `${label}.path`, { empty: false });
336
+ for (const key of ["limit", "required", "target"]) if (finding[key] !== undefined) integer(finding[key], kind, `${label}.${key}`);
337
+ validateJsonValue(finding);
338
+ }
339
+ for (const [index, target] of value.targetStates.entries()) {
340
+ const label = `${kind}.targetStates[${index}]`;
341
+ object(target, kind, label);
342
+ exactKeys(target, new Set(["path", "state"]), kind, label);
343
+ projectPaths([target.path], kind, `${label}.path`, { empty: false });
344
+ enumeration(target.state, ["existing", "prospective-or-deleted"], kind, `${label}.state`);
345
+ }
346
+ uniqueStrings(value.excluded, kind, `${kind}.excluded`, { empty: true });
347
+ object(value.metrics, kind, `${kind}.metrics`);
348
+ const metricKeys = new Set(["deferredItemCount", "hostToolCalls", "hydratedItemCount", "retainedItemCount", "sourceBodyReads", "sourceDigestReads", "sourceIdentityReads"]);
349
+ exactKeys(value.metrics, metricKeys, kind, `${kind}.metrics`);
350
+ for (const key of metricKeys) integer(value.metrics[key], kind, `${kind}.metrics.${key}`);
351
+ if (value.metrics.hostToolCalls !== 1) invalid(kind, "adaptive context must remain one Host tool call");
352
+ object(value.budget, kind, `${kind}.budget`);
353
+ exactKeys(value.budget, new Set(["audit", "delivery", "readTargets", "unit"]), kind, `${kind}.budget`);
354
+ if (value.budget.unit !== "canonical-utf8-bytes") invalid(kind, "adaptive context budget unit is invalid");
355
+ object(value.budget.audit, kind, `${kind}.budget.audit`);
356
+ exactKeys(value.budget.audit, new Set(["maxUtf8Bytes", "targetUtf8Bytes", "usedUtf8Bytes"]), kind, `${kind}.budget.audit`);
357
+ integer(value.budget.audit.targetUtf8Bytes, kind, `${kind}.budget.audit.targetUtf8Bytes`, 1);
358
+ integer(value.budget.audit.maxUtf8Bytes, kind, `${kind}.budget.audit.maxUtf8Bytes`, 1);
359
+ integer(value.budget.audit.usedUtf8Bytes, kind, `${kind}.budget.audit.usedUtf8Bytes`, 1);
360
+ object(value.budget.delivery, kind, `${kind}.budget.delivery`);
361
+ exactKeys(value.budget.delivery, new Set(["completeBelowUtf8Bytes", "maxUtf8Bytes", "usedUtf8Bytes"]), kind, `${kind}.budget.delivery`);
362
+ integer(value.budget.delivery.completeBelowUtf8Bytes, kind, `${kind}.budget.delivery.completeBelowUtf8Bytes`, 1);
363
+ integer(value.budget.delivery.maxUtf8Bytes, kind, `${kind}.budget.delivery.maxUtf8Bytes`, 1);
364
+ integer(value.budget.delivery.usedUtf8Bytes, kind, `${kind}.budget.delivery.usedUtf8Bytes`);
365
+ object(value.budget.readTargets, kind, `${kind}.budget.readTargets`);
366
+ exactKeys(value.budget.readTargets, new Set(["count", "max", "target"]), kind, `${kind}.budget.readTargets`);
367
+ integer(value.budget.readTargets.target, kind, `${kind}.budget.readTargets.target`, 1);
368
+ integer(value.budget.readTargets.max, kind, `${kind}.budget.readTargets.max`, 1);
369
+ integer(value.budget.readTargets.count, kind, `${kind}.budget.readTargets.count`);
370
+ if (value.budget.readTargets.count !== value.readTargets.length) invalid(kind, "read target count does not match bundle");
371
+ object(value.delivery, kind, `${kind}.delivery`);
372
+ exactKeys(value.delivery, new Set(["contentDigest", "format", "itemIds", "mode", "status", "utf8Bytes"]), kind, `${kind}.delivery`);
373
+ enumeration(value.delivery.status, ["ready", "withheld"], kind, `${kind}.delivery.status`);
374
+ if (value.delivery.format !== "project-context-markdown") invalid(kind, `${kind}.delivery.format is invalid`);
375
+ const deliveryIds = ids(value.delivery.itemIds, kind, `${kind}.delivery.itemIds`);
376
+ integer(value.delivery.utf8Bytes, kind, `${kind}.delivery.utf8Bytes`);
377
+ if (value.delivery.status === "ready") {
378
+ enumeration(value.delivery.mode, ["adaptive", "complete"], kind, `${kind}.delivery.mode`);
379
+ digest(value.delivery.contentDigest, kind, `${kind}.delivery.contentDigest`);
380
+ if (value.delivery.utf8Bytes === 0) invalid(kind, "ready delivery must contain rendered bytes");
381
+ if (value.budget?.delivery?.usedUtf8Bytes !== value.delivery.utf8Bytes) invalid(kind, "delivery byte count does not match budget accounting");
382
+ const available = new Set([...value.retainedItemIds, ...value.hydratedItems.map((entry) => entry.id)]);
383
+ if (deliveryIds.some((entry) => !available.has(entry))) invalid(kind, "delivery item ids are not backed by hydrated or retained items");
384
+ } else if (value.delivery.mode !== null || value.delivery.contentDigest !== null || deliveryIds.length !== 0 || value.delivery.utf8Bytes !== 0) {
385
+ invalid(kind, "withheld delivery must not expose consumable content metadata");
386
+ }
387
+ if ((value.taskHealth === "ready") !== (value.delivery.status === "ready")) invalid(kind, "task health and delivery status disagree");
388
+ try { validateJsonValue(value); } catch (error) { invalid(kind, `${kind} must be JSON-compatible`, { reason: error.message }); }
389
+ return value;
390
+ }
391
+
392
+ export function sameCanonical(left, right) { return canonicalJson(left) === canonicalJson(right); }