frontend-project-context 1.6.0 → 1.8.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 +31 -0
- package/README.md +101 -48
- package/UPGRADING.md +30 -1
- package/docs/05-ACCEPTANCE-CONTRACT.md +20 -1
- package/docs/08-INSTALLATION-AND-DISTRIBUTION.md +62 -22
- package/docs/14-FORMAL-RELEASE-READINESS.md +9 -5
- package/docs/19-POST-1.3.1-AI-TAKEOVER-EVIDENCE-AND-UPGRADE-PLAN.md +5 -5
- package/docs/20-PHASE-A-AI-TAKEOVER-AND-HEALTH-CLOSURE-DESIGN.md +11 -11
- package/docs/22-PHASE-C-TARGET-UPGRADE-PROTOCOL-DESIGN.md +4 -4
- package/docs/23-ADAPTIVE-BOUNDED-TASK-CONTEXT-DESIGN.md +432 -0
- package/docs/24-A130-REAL-HOST-TARGET-PROJECT-COMPARISON.md +210 -0
- package/docs/25-REAL-PROJECT-SOURCE-OF-TRUTH-MAINTENANCE-DESIGN.md +409 -0
- package/docs/26-A130-QUALITY-CLOSURE-AND-ADAPTIVE-DELIVERY-REPAIR-DESIGN.md +609 -0
- package/docs/27-TEAM-SHARED-CONTEXT-DIRECTION-DISCUSSION.md +30 -0
- package/docs/28-REAL-PROJECT-ONBOARDING-CLOSURE-DESIGN.md +534 -0
- package/docs/AI-PROJECT-INITIALIZATION.md +89 -0
- package/docs/README.md +34 -6
- package/docs/USER-AND-AI-OPERATION-MANUAL.md +87 -40
- package/examples/README.md +6 -6
- package/examples/package.json +1 -1
- package/migration-manifest.json +42 -8
- package/package.json +2 -2
- package/schemas/adaptive-context-bundle.schema.json +70 -0
- package/schemas/capabilities.schema.json +37 -8
- package/schemas/context-query.schema.json +69 -0
- package/schemas/coverage-audit.schema.json +32 -0
- package/schemas/evidence-bundle.schema.json +2 -2
- package/schemas/host-promotion-evidence.schema.json +33 -0
- package/schemas/initialization-instruction.schema.json +60 -0
- package/schemas/migration-manifest.schema.json +3 -3
- package/schemas/migration-plan.schema.json +2 -2
- package/schemas/project-status.schema.json +5 -4
- package/schemas/projection-lock.schema.json +1 -1
- package/schemas/routing-index.schema.json +58 -0
- package/schemas/truth-reconciliation-input.schema.json +60 -0
- package/schemas/truth-reconciliation-review-bundle.schema.json +155 -0
- package/schemas/upgrade-assessment.schema.json +2 -2
- package/schemas/upgrade-result-bundle.schema.json +1 -1
- package/src/project-context/a130-evaluation.mjs +91 -0
- package/src/project-context/adaptive-context-schema.mjs +392 -0
- package/src/project-context/adaptive-context.mjs +547 -0
- package/src/project-context/ai-entry.mjs +11 -9
- package/src/project-context/assist.mjs +4 -2
- package/src/project-context/capabilities.mjs +32 -0
- package/src/project-context/checker.mjs +4 -3
- package/src/project-context/cli.mjs +55 -5
- package/src/project-context/contract-schema.mjs +1 -1
- package/src/project-context/discovery.mjs +7 -7
- package/src/project-context/exchange-schema.mjs +6 -5
- package/src/project-context/initialization-instruction.mjs +42 -0
- package/src/project-context/maintenance.mjs +2 -2
- package/src/project-context/migration-manifest.mjs +7 -5
- package/src/project-context/project-status.mjs +14 -3
- package/src/project-context/project-store.mjs +27 -2
- package/src/project-context/renderer.mjs +75 -1
- package/src/project-context/source-reader.mjs +63 -30
- package/src/project-context/task-context.mjs +14 -2
- package/src/project-context/truth-reconciliation-schema.mjs +488 -0
- package/src/project-context/truth-reconciliation.mjs +543 -0
- package/src/project-context/upgrade-schema.mjs +5 -1
|
@@ -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); }
|