frontend-project-context 1.7.0 → 1.9.1
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 +26 -0
- package/README.md +17 -10
- package/UPGRADING.md +18 -0
- package/docs/08-INSTALLATION-AND-DISTRIBUTION.md +29 -6
- package/docs/14-FORMAL-RELEASE-READINESS.md +14 -0
- package/docs/24-A130-REAL-HOST-TARGET-PROJECT-COMPARISON.md +1 -1
- package/docs/26-A130-QUALITY-CLOSURE-AND-ADAPTIVE-DELIVERY-REPAIR-DESIGN.md +1 -1
- package/docs/27-TEAM-SHARED-CONTEXT-DIRECTION-DISCUSSION.md +30 -0
- package/docs/28-REAL-PROJECT-ONBOARDING-CLOSURE-DESIGN.md +544 -0
- package/docs/29-REAL-PROJECT-1.8.0-INITIALIZATION-OBSERVATIONS.md +228 -0
- package/docs/30-TASK-CONTEXT-CONSUMPTION-CLOSURE-DESIGN.md +563 -0
- package/docs/31-TASK-CONTEXT-INTEGRITY-REPAIR-DESIGN.md +281 -0
- package/docs/AI-PROJECT-INITIALIZATION.md +90 -0
- package/docs/PRODUCT-SHARING-AND-ADOPTION-GUIDE.md +111 -0
- package/docs/README.md +24 -0
- package/docs/USER-AND-AI-OPERATION-MANUAL.md +17 -13
- package/docs/assets/product-sharing-01-overview.svg +32 -0
- package/docs/assets/product-sharing-02-how-it-works.svg +17 -0
- package/docs/assets/product-sharing-03-example.svg +19 -0
- package/examples/README.md +2 -2
- package/examples/package.json +1 -1
- package/migration-manifest.json +40 -10
- package/package.json +2 -2
- package/schemas/capabilities.schema.json +29 -10
- package/schemas/context-bundle.schema.json +32 -0
- package/schemas/coverage-audit.schema.json +6 -4
- package/schemas/evidence-bundle.schema.json +1 -1
- package/schemas/initialization-instruction.schema.json +72 -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/upgrade-assessment.schema.json +2 -2
- package/schemas/upgrade-result-bundle.schema.json +1 -1
- package/src/project-context/adaptive-context-schema.mjs +1 -1
- package/src/project-context/adaptive-context.mjs +12 -23
- package/src/project-context/ai-entry.mjs +24 -9
- package/src/project-context/approver.mjs +6 -2
- package/src/project-context/authoring.mjs +28 -8
- package/src/project-context/capabilities.mjs +18 -1
- package/src/project-context/checker.mjs +1 -1
- package/src/project-context/cli.mjs +44 -16
- package/src/project-context/context-bundle.mjs +378 -0
- package/src/project-context/contract-schema.mjs +62 -9
- package/src/project-context/coverage-profile.mjs +127 -0
- package/src/project-context/exchange-schema.mjs +24 -7
- package/src/project-context/exchange.mjs +3 -0
- package/src/project-context/initialization-instruction.mjs +60 -0
- package/src/project-context/maintenance.mjs +5 -4
- package/src/project-context/migration-manifest.mjs +5 -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 +11 -4
- package/src/project-context/source-reader.mjs +27 -2
- package/src/project-context/upgrade-schema.mjs +2 -2
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import { canonicalJson, canonicalValue, digestJson } from "./canonical-json.mjs";
|
|
2
|
+
import { sourceStatus } from "./contract-schema.mjs";
|
|
3
|
+
import { coverageDomainForPath, coverageProfile } from "./coverage-profile.mjs";
|
|
4
|
+
import { fail } from "./errors.mjs";
|
|
5
|
+
import { normalizeRelativePath, resolveExistingInside } from "./path-policy.mjs";
|
|
6
|
+
import { renderCollectedContextBundle } from "./renderer.mjs";
|
|
7
|
+
import { effectiveItems } from "./scope-compiler.mjs";
|
|
8
|
+
|
|
9
|
+
export const CONTEXT_BUNDLE_SCHEMA_VERSION = 1;
|
|
10
|
+
|
|
11
|
+
const LOCAL_SOURCE_KINDS = new Set(["file", "path", "json-pointer"]);
|
|
12
|
+
const ROLES = Object.freeze({ "provenance-only": 0, conditional: 1, required: 2 });
|
|
13
|
+
const SHA256 = /^sha256:[a-f0-9]{64}$/u;
|
|
14
|
+
|
|
15
|
+
function uniqueSorted(values) {
|
|
16
|
+
return [...new Set(values)].sort((left, right) => left.localeCompare(right));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function snapshots(project) {
|
|
20
|
+
return {
|
|
21
|
+
contract: project.contractDigest,
|
|
22
|
+
sourcesLock: project.sourcesLockDigest,
|
|
23
|
+
projectionsLock: project.projectionsLockDigest,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function scopeCoverage(items) {
|
|
28
|
+
if (items.length === 0) return "none";
|
|
29
|
+
return items.some((item) => item.scope.kind !== "project") ? "scoped" : "project-only";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function itemRecord(item) {
|
|
33
|
+
return canonicalValue({
|
|
34
|
+
id: item.id,
|
|
35
|
+
kind: item.kind,
|
|
36
|
+
subject: item.subject,
|
|
37
|
+
scope: structuredClone(item.scope),
|
|
38
|
+
sourceIds: [...item.sources].sort(),
|
|
39
|
+
statement: item.statement,
|
|
40
|
+
value: structuredClone(item.value),
|
|
41
|
+
overrides: [...item.overrides].sort(),
|
|
42
|
+
...(item.verification !== undefined ? { verification: structuredClone(item.verification) } : {}),
|
|
43
|
+
...(item.consumption !== undefined ? { consumption: structuredClone(item.consumption) } : {}),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function sourceRole(item, sourceId) {
|
|
48
|
+
if (item.consumption?.requiredSources.includes(sourceId)) return "required";
|
|
49
|
+
if (item.consumption?.conditionalSources.includes(sourceId)) return "conditional";
|
|
50
|
+
if (item.consumption === undefined && item.kind === "reference") return "conditional";
|
|
51
|
+
return "provenance-only";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function sourceLocator(source) {
|
|
55
|
+
if (source.kind === "json-pointer") return { path: source.path, pointer: source.pointer };
|
|
56
|
+
if (LOCAL_SOURCE_KINDS.has(source.kind)) return { path: source.path };
|
|
57
|
+
return { reference: source.reference };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function consumptionRecords(root, contract, items, paths) {
|
|
61
|
+
const sourceMap = new Map(contract.sources.filter((source) => sourceStatus(source) === "active").map((source) => [source.id, source]));
|
|
62
|
+
const records = new Map();
|
|
63
|
+
const merge = (value) => {
|
|
64
|
+
const current = records.get(value.path);
|
|
65
|
+
if (!current) {
|
|
66
|
+
records.set(value.path, value);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (ROLES[value.role] > ROLES[current.role]) current.role = value.role;
|
|
70
|
+
current.itemIds = uniqueSorted([...current.itemIds, ...value.itemIds]);
|
|
71
|
+
current.sourceIds = uniqueSorted([...current.sourceIds, ...value.sourceIds]);
|
|
72
|
+
current.reasons = uniqueSorted([...current.reasons, ...value.reasons]);
|
|
73
|
+
const pointers = uniqueSorted([...(current.pointers ?? []), ...(value.pointers ?? [])]);
|
|
74
|
+
if (pointers.length > 0) current.pointers = pointers;
|
|
75
|
+
else delete current.pointers;
|
|
76
|
+
};
|
|
77
|
+
for (const targetPath of paths) {
|
|
78
|
+
merge({
|
|
79
|
+
path: targetPath,
|
|
80
|
+
role: "required",
|
|
81
|
+
itemIds: [],
|
|
82
|
+
sourceIds: [],
|
|
83
|
+
reasons: ["task-target"],
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
for (const item of items) {
|
|
87
|
+
for (const sourceId of item.sources) {
|
|
88
|
+
const source = sourceMap.get(sourceId);
|
|
89
|
+
const role = sourceRole(item, sourceId);
|
|
90
|
+
if (!source || !LOCAL_SOURCE_KINDS.has(source.kind)) continue;
|
|
91
|
+
merge({
|
|
92
|
+
path: source.path,
|
|
93
|
+
role,
|
|
94
|
+
itemIds: [item.id],
|
|
95
|
+
sourceIds: [sourceId],
|
|
96
|
+
reasons: [`contract-source:${role}`],
|
|
97
|
+
...(source.kind === "json-pointer" ? { pointers: [source.pointer] } : {}),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const all = [...records.values()].sort((left, right) => left.path.localeCompare(right.path));
|
|
102
|
+
const readTargets = all.filter((entry) => entry.role !== "provenance-only");
|
|
103
|
+
const provenancePaths = new Set(all.filter((entry) => entry.role === "provenance-only").map((entry) => entry.path));
|
|
104
|
+
const provenanceSources = [];
|
|
105
|
+
for (const item of items) {
|
|
106
|
+
for (const sourceId of item.sources) {
|
|
107
|
+
const source = sourceMap.get(sourceId);
|
|
108
|
+
if (!source || sourceRole(item, sourceId) !== "provenance-only") continue;
|
|
109
|
+
if (LOCAL_SOURCE_KINDS.has(source.kind) && records.get(source.path)?.role !== "provenance-only") continue;
|
|
110
|
+
provenanceSources.push({ sourceId, itemIds: [item.id], kind: source.kind, locator: sourceLocator(source) });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const bySource = new Map();
|
|
114
|
+
for (const record of provenanceSources) {
|
|
115
|
+
const current = bySource.get(record.sourceId);
|
|
116
|
+
if (current) current.itemIds = uniqueSorted([...current.itemIds, ...record.itemIds]);
|
|
117
|
+
else bySource.set(record.sourceId, record);
|
|
118
|
+
}
|
|
119
|
+
const unavailable = [];
|
|
120
|
+
for (const target of readTargets.filter((entry) => entry.role === "required" && entry.sourceIds.length > 0)) {
|
|
121
|
+
try {
|
|
122
|
+
await resolveExistingInside(root, target.path);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
unavailable.push({ code: "required-read-target-unavailable", path: target.path, sourceIds: target.sourceIds, reason: error.code ?? "unavailable" });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
readTargets: readTargets.map((entry) => canonicalValue(entry)),
|
|
129
|
+
provenanceSources: [...bySource.values()].sort((left, right) => left.sourceId.localeCompare(right.sourceId)).map(canonicalValue),
|
|
130
|
+
unavailable,
|
|
131
|
+
provenancePaths,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function coverageFor(profile, targets) {
|
|
136
|
+
const domains = targets.map((target) => {
|
|
137
|
+
const domain = coverageDomainForPath(profile, target.path);
|
|
138
|
+
return {
|
|
139
|
+
path: target.path,
|
|
140
|
+
disposition: domain?.disposition ?? "outside-declared-coverage",
|
|
141
|
+
domainId: domain?.id ?? null,
|
|
142
|
+
};
|
|
143
|
+
});
|
|
144
|
+
let registrationCoverage = "closed-for-declared-scope";
|
|
145
|
+
if (!profile) registrationCoverage = "not-declared";
|
|
146
|
+
else if (domains.some((entry) => entry.disposition === "unresolved")) registrationCoverage = "unresolved";
|
|
147
|
+
else if (profile.version !== 2) registrationCoverage = "review-required";
|
|
148
|
+
return {
|
|
149
|
+
profileItemId: profile?.itemId ?? null,
|
|
150
|
+
profileVersion: profile?.version ?? null,
|
|
151
|
+
registrationCoverage,
|
|
152
|
+
targets: domains,
|
|
153
|
+
guarantee: "declared-scope-only-never-all-project-truth",
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function stableGaps(gaps) {
|
|
158
|
+
const byJson = new Map();
|
|
159
|
+
for (const gap of gaps) byJson.set(canonicalJson(gap), canonicalValue(gap));
|
|
160
|
+
return [...byJson.values()].sort((left, right) => canonicalJson(left).localeCompare(canonicalJson(right)));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function authority() {
|
|
164
|
+
return {
|
|
165
|
+
productTaskExecution: false,
|
|
166
|
+
productBusinessCodeWrites: false,
|
|
167
|
+
hostTaskAuthority: "external-to-project-context",
|
|
168
|
+
resolveFrom: ["current-user-request", "human-authored-project-instructions", "higher-priority-safety-constraints"],
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function buildContextBundle(root, project, input) {
|
|
173
|
+
const mode = input.mode;
|
|
174
|
+
if (!new Set(["locate", "targeted"]).has(mode)) fail("context-bundle-schema-invalid", "context bundle mode is invalid");
|
|
175
|
+
const paths = mode === "locate"
|
|
176
|
+
? []
|
|
177
|
+
: uniqueSorted(input.paths.map((value) => normalizeRelativePath(value, { allowRoot: true, label: "target path" })));
|
|
178
|
+
const task = input.task ?? "";
|
|
179
|
+
const sections = mode === "locate"
|
|
180
|
+
? [{ path: ".", items: effectiveItems(project.contract.items.filter((item) => item.scope.kind === "project"), ".") }]
|
|
181
|
+
: paths.map((targetPath) => ({ path: targetPath, items: effectiveItems(project.contract.items, targetPath) }));
|
|
182
|
+
const itemMap = new Map();
|
|
183
|
+
for (const section of sections) for (const item of section.items) itemMap.set(item.id, item);
|
|
184
|
+
const items = [...itemMap.values()].sort((left, right) => left.id.localeCompare(right.id));
|
|
185
|
+
const commonIds = sections.length === 0
|
|
186
|
+
? []
|
|
187
|
+
: sections[0].items.map((item) => item.id).filter((id) => sections.every((section) => section.items.some((item) => item.id === id))).sort();
|
|
188
|
+
const targets = mode === "locate" ? [] : sections.map((section) => {
|
|
189
|
+
const ids = section.items.map((item) => item.id).sort();
|
|
190
|
+
return {
|
|
191
|
+
path: section.path,
|
|
192
|
+
scopeCoverage: scopeCoverage(section.items),
|
|
193
|
+
itemIds: ids,
|
|
194
|
+
scopedItemIds: section.items.filter((item) => item.scope.kind !== "project").map((item) => item.id).sort(),
|
|
195
|
+
deltaItemIds: ids.filter((id) => !commonIds.includes(id)),
|
|
196
|
+
};
|
|
197
|
+
});
|
|
198
|
+
const consumption = await consumptionRecords(root, project.contract, items, paths);
|
|
199
|
+
const profile = coverageProfile(project.contract);
|
|
200
|
+
const coverage = coverageFor(profile, targets);
|
|
201
|
+
const gaps = [...consumption.unavailable];
|
|
202
|
+
if (mode === "locate") gaps.push({ code: "target-path-not-yet-known" });
|
|
203
|
+
for (const target of targets) {
|
|
204
|
+
if (target.scopeCoverage === "project-only") gaps.push({ code: "project-scope-only", path: target.path });
|
|
205
|
+
if (target.scopeCoverage === "none") gaps.push({ code: "no-applicable-contract-item", path: target.path });
|
|
206
|
+
}
|
|
207
|
+
if (!profile) gaps.push({ code: "registration-coverage-not-declared" });
|
|
208
|
+
if (coverage.registrationCoverage === "review-required") gaps.push({ code: "registration-coverage-review-required" });
|
|
209
|
+
if (coverage.registrationCoverage === "unresolved") gaps.push({ code: "registration-coverage-review-required", disposition: "unresolved" });
|
|
210
|
+
const nextActions = mode === "locate"
|
|
211
|
+
? ["locate-minimum-candidate-paths-inside-target-root", "recompile-targeted-context-with-all-candidate-paths"]
|
|
212
|
+
: ["consume-required-read-targets", "evaluate-conditional-read-targets", "recompile-if-affected-paths-change"];
|
|
213
|
+
const structured = {
|
|
214
|
+
schemaVersion: 1,
|
|
215
|
+
kind: "context-bundle",
|
|
216
|
+
project: { id: project.contract.project.id, name: project.contract.project.name },
|
|
217
|
+
mode,
|
|
218
|
+
task: { text: task, paths },
|
|
219
|
+
snapshots: snapshots(project),
|
|
220
|
+
semanticCompleteness: "not-claimed",
|
|
221
|
+
commonItemIds: commonIds,
|
|
222
|
+
targets,
|
|
223
|
+
items: items.map(itemRecord),
|
|
224
|
+
readTargets: consumption.readTargets,
|
|
225
|
+
provenanceSources: consumption.provenanceSources,
|
|
226
|
+
coverage,
|
|
227
|
+
authority: authority(),
|
|
228
|
+
gaps: stableGaps(gaps),
|
|
229
|
+
nextActions,
|
|
230
|
+
};
|
|
231
|
+
const content = renderCollectedContextBundle({
|
|
232
|
+
project: project.contract.project,
|
|
233
|
+
contractDigest: project.contractDigest,
|
|
234
|
+
paths: mode === "locate" ? ["."] : paths,
|
|
235
|
+
sections,
|
|
236
|
+
itemIds: items.map((item) => item.id),
|
|
237
|
+
}, project.contract.sources, task || undefined, {
|
|
238
|
+
locale: input.locale,
|
|
239
|
+
mode,
|
|
240
|
+
gaps: structured.gaps,
|
|
241
|
+
nextActions,
|
|
242
|
+
});
|
|
243
|
+
return validateContextBundle({ ...structured, content, bundleDigest: digestJson({ ...structured, content }) });
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function requireObject(value, label) {
|
|
247
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) fail("context-bundle-schema-invalid", `${label} must be an object`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function exactKeys(value, expected, label) {
|
|
251
|
+
requireObject(value, label);
|
|
252
|
+
const actual = Object.keys(value).sort();
|
|
253
|
+
const wanted = [...expected].sort();
|
|
254
|
+
if (actual.length !== wanted.length || actual.some((entry, index) => entry !== wanted[index])) {
|
|
255
|
+
fail("context-bundle-schema-invalid", `${label} must contain exactly: ${wanted.join(", ")}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function allowedKeys(value, allowed, label) {
|
|
260
|
+
requireObject(value, label);
|
|
261
|
+
for (const key of Object.keys(value)) {
|
|
262
|
+
if (!allowed.has(key)) fail("context-bundle-schema-invalid", `${label} contains unknown field: ${key}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function nonEmptyString(value, label) {
|
|
267
|
+
if (typeof value !== "string" || value.length === 0) fail("context-bundle-schema-invalid", `${label} must be a non-empty string`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function stableRecords(values, key, label) {
|
|
271
|
+
const selected = values.map((entry) => entry[key]);
|
|
272
|
+
sortedUnique(selected, `${label} ${key}s`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function sortedUnique(values, label) {
|
|
276
|
+
if (!Array.isArray(values) || values.some((entry) => typeof entry !== "string")) fail("context-bundle-schema-invalid", `${label} must be a string array`);
|
|
277
|
+
const expected = uniqueSorted(values);
|
|
278
|
+
if (canonicalJson(values) !== canonicalJson(expected)) fail("context-bundle-schema-invalid", `${label} must be sorted and unique`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function validateContextBundle(input) {
|
|
282
|
+
const value = canonicalValue(input);
|
|
283
|
+
const keys = new Set(["schemaVersion", "kind", "project", "mode", "task", "snapshots", "semanticCompleteness", "commonItemIds", "targets", "items", "readTargets", "provenanceSources", "coverage", "authority", "gaps", "nextActions", "content", "bundleDigest"]);
|
|
284
|
+
exactKeys(value, keys, "context bundle");
|
|
285
|
+
if (value.schemaVersion !== 1 || value.kind !== "context-bundle") fail("context-bundle-schema-invalid", "context bundle identity is invalid");
|
|
286
|
+
if (!["locate", "targeted"].includes(value.mode)) fail("context-bundle-schema-invalid", "context bundle mode is invalid");
|
|
287
|
+
if (typeof value.content !== "string" || value.content.length === 0) fail("context-bundle-schema-invalid", "context bundle content is required");
|
|
288
|
+
if (!SHA256.test(value.bundleDigest)) fail("context-bundle-schema-invalid", "context bundle digest must be sha256");
|
|
289
|
+
const unsigned = structuredClone(value);
|
|
290
|
+
delete unsigned.bundleDigest;
|
|
291
|
+
if (digestJson(unsigned) !== value.bundleDigest) fail("context-bundle-schema-invalid", "context bundle digest does not match canonical content");
|
|
292
|
+
exactKeys(value.project, new Set(["id", "name"]), "context bundle project");
|
|
293
|
+
nonEmptyString(value.project.id, "context bundle project.id");
|
|
294
|
+
nonEmptyString(value.project.name, "context bundle project.name");
|
|
295
|
+
exactKeys(value.task, new Set(["text", "paths"]), "context bundle task");
|
|
296
|
+
if (typeof value.task.text !== "string") fail("context-bundle-schema-invalid", "context bundle task.text must be a string");
|
|
297
|
+
exactKeys(value.snapshots, new Set(["contract", "sourcesLock", "projectionsLock"]), "context bundle snapshots");
|
|
298
|
+
for (const digest of Object.values(value.snapshots)) if (!SHA256.test(digest)) fail("context-bundle-schema-invalid", "context bundle snapshot must be sha256");
|
|
299
|
+
if (value.semanticCompleteness !== "not-claimed") fail("context-bundle-schema-invalid", "semantic completeness cannot be claimed");
|
|
300
|
+
sortedUnique(value.task.paths, "context bundle task paths");
|
|
301
|
+
sortedUnique(value.commonItemIds, "context bundle common item IDs");
|
|
302
|
+
sortedUnique(value.nextActions, "context bundle next actions");
|
|
303
|
+
for (const collection of ["targets", "items", "readTargets", "provenanceSources", "gaps"]) {
|
|
304
|
+
if (!Array.isArray(value[collection])) fail("context-bundle-schema-invalid", `context bundle ${collection} must be an array`);
|
|
305
|
+
}
|
|
306
|
+
stableRecords(value.targets, "path", "context bundle targets");
|
|
307
|
+
if (canonicalJson(value.targets.map((target) => target.path)) !== canonicalJson(value.task.paths)) {
|
|
308
|
+
fail("context-bundle-schema-invalid", "context bundle targets must correspond exactly to task paths");
|
|
309
|
+
}
|
|
310
|
+
for (const [index, target] of value.targets.entries()) {
|
|
311
|
+
const label = `context bundle targets[${index}]`;
|
|
312
|
+
exactKeys(target, new Set(["path", "scopeCoverage", "itemIds", "scopedItemIds", "deltaItemIds"]), label);
|
|
313
|
+
if (normalizeRelativePath(target.path, { allowRoot: true, label: `${label}.path` }) !== target.path) fail("context-bundle-schema-invalid", `${label}.path must be normalized`);
|
|
314
|
+
if (!["scoped", "project-only", "none"].includes(target.scopeCoverage)) fail("context-bundle-schema-invalid", `${label}.scopeCoverage is invalid`);
|
|
315
|
+
for (const key of ["itemIds", "scopedItemIds", "deltaItemIds"]) sortedUnique(target[key], `${label}.${key}`);
|
|
316
|
+
for (const itemId of [...target.scopedItemIds, ...target.deltaItemIds]) if (!target.itemIds.includes(itemId)) fail("context-bundle-schema-invalid", `${label} contains an item outside itemIds`);
|
|
317
|
+
}
|
|
318
|
+
stableRecords(value.items, "id", "context bundle items");
|
|
319
|
+
const itemIds = new Set(value.items.map((item) => item.id));
|
|
320
|
+
for (const [index, item] of value.items.entries()) {
|
|
321
|
+
const label = `context bundle items[${index}]`;
|
|
322
|
+
allowedKeys(item, new Set(["id", "kind", "subject", "scope", "sourceIds", "statement", "value", "overrides", "verification", "consumption"]), label);
|
|
323
|
+
for (const required of ["id", "kind", "subject", "scope", "sourceIds", "statement", "value", "overrides"]) if (!Object.hasOwn(item, required)) fail("context-bundle-schema-invalid", `${label}.${required} is required`);
|
|
324
|
+
nonEmptyString(item.id, `${label}.id`);
|
|
325
|
+
if (!["fact", "policy", "reference", "validation-description"].includes(item.kind)) fail("context-bundle-schema-invalid", `${label}.kind is invalid`);
|
|
326
|
+
nonEmptyString(item.subject, `${label}.subject`);
|
|
327
|
+
nonEmptyString(item.statement, `${label}.statement`);
|
|
328
|
+
requireObject(item.scope, `${label}.scope`);
|
|
329
|
+
sortedUnique(item.sourceIds, `${label}.sourceIds`);
|
|
330
|
+
sortedUnique(item.overrides, `${label}.overrides`);
|
|
331
|
+
if (item.consumption !== undefined) {
|
|
332
|
+
exactKeys(item.consumption, new Set(["requiredSources", "conditionalSources"]), `${label}.consumption`);
|
|
333
|
+
sortedUnique(item.consumption.requiredSources, `${label}.consumption.requiredSources`);
|
|
334
|
+
sortedUnique(item.consumption.conditionalSources, `${label}.consumption.conditionalSources`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
for (const id of value.commonItemIds) if (!itemIds.has(id)) fail("context-bundle-schema-invalid", `context bundle common item is missing: ${id}`);
|
|
338
|
+
for (const target of value.targets) for (const id of target.itemIds) if (!itemIds.has(id)) fail("context-bundle-schema-invalid", `context bundle target item is missing: ${id}`);
|
|
339
|
+
stableRecords(value.readTargets, "path", "context bundle read targets");
|
|
340
|
+
for (const [index, target] of value.readTargets.entries()) {
|
|
341
|
+
const label = `context bundle readTargets[${index}]`;
|
|
342
|
+
allowedKeys(target, new Set(["path", "role", "itemIds", "sourceIds", "reasons", "pointers"]), label);
|
|
343
|
+
for (const required of ["path", "role", "itemIds", "sourceIds", "reasons"]) if (!Object.hasOwn(target, required)) fail("context-bundle-schema-invalid", `${label}.${required} is required`);
|
|
344
|
+
if (!["required", "conditional"].includes(target.role)) fail("context-bundle-schema-invalid", `${label}.role is invalid`);
|
|
345
|
+
for (const key of ["itemIds", "sourceIds", "reasons", ...(target.pointers === undefined ? [] : ["pointers"])]) sortedUnique(target[key], `${label}.${key}`);
|
|
346
|
+
}
|
|
347
|
+
stableRecords(value.provenanceSources, "sourceId", "context bundle provenance sources");
|
|
348
|
+
for (const [index, source] of value.provenanceSources.entries()) {
|
|
349
|
+
const label = `context bundle provenanceSources[${index}]`;
|
|
350
|
+
exactKeys(source, new Set(["sourceId", "itemIds", "kind", "locator"]), label);
|
|
351
|
+
nonEmptyString(source.sourceId, `${label}.sourceId`);
|
|
352
|
+
nonEmptyString(source.kind, `${label}.kind`);
|
|
353
|
+
sortedUnique(source.itemIds, `${label}.itemIds`);
|
|
354
|
+
requireObject(source.locator, `${label}.locator`);
|
|
355
|
+
}
|
|
356
|
+
exactKeys(value.coverage, new Set(["profileItemId", "profileVersion", "registrationCoverage", "targets", "guarantee"]), "context bundle coverage");
|
|
357
|
+
if (![null, 1, 2].includes(value.coverage.profileVersion)) fail("context-bundle-schema-invalid", "context bundle coverage profileVersion is invalid");
|
|
358
|
+
if (!["not-declared", "review-required", "unresolved", "closed-for-declared-scope"].includes(value.coverage.registrationCoverage)) fail("context-bundle-schema-invalid", "context bundle registrationCoverage is invalid");
|
|
359
|
+
if (value.coverage.guarantee !== "declared-scope-only-never-all-project-truth") fail("context-bundle-schema-invalid", "context bundle coverage guarantee is invalid");
|
|
360
|
+
if (!Array.isArray(value.coverage.targets)) fail("context-bundle-schema-invalid", "context bundle coverage targets must be an array");
|
|
361
|
+
stableRecords(value.coverage.targets, "path", "context bundle coverage targets");
|
|
362
|
+
for (const [index, target] of value.coverage.targets.entries()) exactKeys(target, new Set(["path", "disposition", "domainId"]), `context bundle coverage.targets[${index}]`);
|
|
363
|
+
exactKeys(value.authority, new Set(["productTaskExecution", "productBusinessCodeWrites", "hostTaskAuthority", "resolveFrom"]), "context bundle authority");
|
|
364
|
+
if (value.authority.productTaskExecution !== false || value.authority.productBusinessCodeWrites !== false || value.authority.hostTaskAuthority !== "external-to-project-context") fail("context-bundle-schema-invalid", "context bundle authority is invalid");
|
|
365
|
+
if (canonicalJson(value.authority.resolveFrom) !== canonicalJson(authority().resolveFrom)) fail("context-bundle-schema-invalid", "context bundle authority resolution order is invalid");
|
|
366
|
+
const gapCodes = new Set(["target-path-not-yet-known", "project-scope-only", "no-applicable-contract-item", "registration-coverage-not-declared", "registration-coverage-review-required", "required-read-target-unavailable"]);
|
|
367
|
+
for (const [index, gap] of value.gaps.entries()) {
|
|
368
|
+
const label = `context bundle gaps[${index}]`;
|
|
369
|
+
allowedKeys(gap, new Set(["code", "path", "sourceIds", "reason", "disposition"]), label);
|
|
370
|
+
if (!gapCodes.has(gap.code)) fail("context-bundle-schema-invalid", `${label}.code is invalid`);
|
|
371
|
+
if (gap.sourceIds !== undefined) sortedUnique(gap.sourceIds, `${label}.sourceIds`);
|
|
372
|
+
}
|
|
373
|
+
const stableGapValues = stableGaps(value.gaps);
|
|
374
|
+
if (canonicalJson(stableGapValues) !== canonicalJson(value.gaps)) fail("context-bundle-schema-invalid", "context bundle gaps must be stably sorted and unique");
|
|
375
|
+
if (value.mode === "locate" && (value.task.paths.length > 0 || value.targets.length > 0)) fail("context-bundle-schema-invalid", "locate context cannot contain target paths");
|
|
376
|
+
if (value.mode === "targeted" && value.task.paths.length === 0) fail("context-bundle-schema-invalid", "targeted context requires paths");
|
|
377
|
+
return value;
|
|
378
|
+
}
|
|
@@ -10,6 +10,7 @@ const STATUSES = new Set(["proposed", "approved", "deprecated"]);
|
|
|
10
10
|
const SCOPE_KINDS = new Set(["project", "path-prefix", "file"]);
|
|
11
11
|
const VERIFICATION_KINDS = new Set(["none", "file-exists", "json-value", "path-digest"]);
|
|
12
12
|
const SHA256 = /^sha256:[a-f0-9]{64}$/u;
|
|
13
|
+
const DIGEST_MODES = new Set(["full-file", "outside-owned-ai-entry"]);
|
|
13
14
|
|
|
14
15
|
function object(value, label) {
|
|
15
16
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -64,7 +65,14 @@ export function sourceStatus(source) {
|
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
export function sourceForContract(source, schemaVersion) {
|
|
67
|
-
return schemaVersion
|
|
68
|
+
return schemaVersion >= 2 ? { ...structuredClone(source), status: "active" } : structuredClone(source);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function promoteContractToSchema3(contract) {
|
|
72
|
+
if (contract.schemaVersion === 3) return contract;
|
|
73
|
+
contract.schemaVersion = 3;
|
|
74
|
+
contract.sources = contract.sources.map((source) => source.status === undefined ? { ...source, status: "active" } : source);
|
|
75
|
+
return contract;
|
|
68
76
|
}
|
|
69
77
|
|
|
70
78
|
export function sourceRegistrationShape(source) {
|
|
@@ -76,12 +84,14 @@ export function sourceRegistrationShape(source) {
|
|
|
76
84
|
|
|
77
85
|
export function validateSource(source, label = "source", options = {}) {
|
|
78
86
|
const contractSchemaVersion = options.contractSchemaVersion ?? 1;
|
|
87
|
+
const governedStatus = contractSchemaVersion >= 2 && options.proposal !== true;
|
|
79
88
|
object(source, label);
|
|
80
89
|
const allowed = new Set(["id", "kind", "path", "pointer", "reference", "digest"]);
|
|
81
|
-
if (
|
|
90
|
+
if (governedStatus) {
|
|
82
91
|
allowed.add("status");
|
|
83
92
|
allowed.add("deprecation");
|
|
84
93
|
}
|
|
94
|
+
if (contractSchemaVersion >= 3) allowed.add("digestMode");
|
|
85
95
|
exactKeys(source, allowed, label);
|
|
86
96
|
stableId(source.id, `${label}.id`);
|
|
87
97
|
if (!SOURCE_KINDS.has(source.kind)) fail("schema-invalid-enum", `${label}.kind is invalid`);
|
|
@@ -97,6 +107,10 @@ export function validateSource(source, label = "source", options = {}) {
|
|
|
97
107
|
} else if (source.pointer !== undefined) {
|
|
98
108
|
fail("schema-invalid", `${label}.pointer is only valid for json-pointer sources`);
|
|
99
109
|
}
|
|
110
|
+
if (source.digestMode !== undefined) {
|
|
111
|
+
if (source.kind !== "file") fail("schema-invalid", `${label}.digestMode is only valid for file sources`);
|
|
112
|
+
if (!DIGEST_MODES.has(source.digestMode)) fail("schema-invalid-enum", `${label}.digestMode is invalid`);
|
|
113
|
+
}
|
|
100
114
|
if (source.reference !== undefined) fail("schema-invalid", `${label}.reference is not valid for local sources`);
|
|
101
115
|
} else {
|
|
102
116
|
string(source.reference, `${label}.reference`);
|
|
@@ -107,7 +121,7 @@ export function validateSource(source, label = "source", options = {}) {
|
|
|
107
121
|
fail("schema-invalid", `${label}.digest must be null or omitted for unverifiable sources`);
|
|
108
122
|
}
|
|
109
123
|
}
|
|
110
|
-
if (
|
|
124
|
+
if (governedStatus) {
|
|
111
125
|
if (!SOURCE_STATUSES.has(source.status)) fail("schema-invalid-enum", `${label}.status is invalid`);
|
|
112
126
|
if (source.status === "active" && source.deprecation !== undefined) {
|
|
113
127
|
fail("schema-invalid", `${label}.deprecation is not allowed for active sources`);
|
|
@@ -123,6 +137,23 @@ export function validateSource(source, label = "source", options = {}) {
|
|
|
123
137
|
return source;
|
|
124
138
|
}
|
|
125
139
|
|
|
140
|
+
function validateConsumption(consumption, item, label) {
|
|
141
|
+
object(consumption, label);
|
|
142
|
+
exactKeys(consumption, new Set(["requiredSources", "conditionalSources"]), label);
|
|
143
|
+
uniqueStrings(consumption.requiredSources, `${label}.requiredSources`, { empty: true });
|
|
144
|
+
uniqueStrings(consumption.conditionalSources, `${label}.conditionalSources`, { empty: true });
|
|
145
|
+
for (const [name, values] of Object.entries(consumption)) {
|
|
146
|
+
const sorted = [...values].sort((left, right) => left.localeCompare(right));
|
|
147
|
+
if (JSON.stringify(values) !== JSON.stringify(sorted)) fail("schema-invalid", `${label}.${name} must be stably sorted`);
|
|
148
|
+
for (const source of values) {
|
|
149
|
+
stableId(source, `${label}.${name} entry`);
|
|
150
|
+
if (!item.sources.includes(source)) fail("source-reference-missing", `${label}.${name} must reference item sources: ${source}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const overlap = consumption.requiredSources.find((source) => consumption.conditionalSources.includes(source));
|
|
154
|
+
if (overlap) fail("schema-invalid", `${label} source roles must be mutually exclusive: ${overlap}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
126
157
|
function validateApproval(approval, label) {
|
|
127
158
|
object(approval, label);
|
|
128
159
|
exactKeys(approval, new Set(["by", "at", "rationale"]), label);
|
|
@@ -164,9 +195,12 @@ function validateVerification(verification, label) {
|
|
|
164
195
|
|
|
165
196
|
export function validateItem(item, label = "item", options = {}) {
|
|
166
197
|
object(item, label);
|
|
198
|
+
const contractSchemaVersion = options.contractSchemaVersion ?? 1;
|
|
199
|
+
const allowed = new Set(["id", "kind", "subject", "value", "statement", "scope", "status", "sources", "overrides", "approval", "verification"]);
|
|
200
|
+
if (contractSchemaVersion >= 3) allowed.add("consumption");
|
|
167
201
|
exactKeys(
|
|
168
202
|
item,
|
|
169
|
-
|
|
203
|
+
allowed,
|
|
170
204
|
label,
|
|
171
205
|
);
|
|
172
206
|
stableId(item.id, `${label}.id`);
|
|
@@ -193,6 +227,7 @@ export function validateItem(item, label = "item", options = {}) {
|
|
|
193
227
|
}
|
|
194
228
|
if (item.approval !== undefined && item.status !== "proposed") validateApproval(item.approval, `${label}.approval`);
|
|
195
229
|
if (item.verification != null) validateVerification(item.verification, `${label}.verification`);
|
|
230
|
+
if (item.consumption !== undefined) validateConsumption(item.consumption, item, `${label}.consumption`);
|
|
196
231
|
if (options.proposal && item.status !== "proposed") {
|
|
197
232
|
fail("proposal-not-proposed", `${label} must remain proposed`);
|
|
198
233
|
}
|
|
@@ -210,7 +245,7 @@ function assertUnique(records, label) {
|
|
|
210
245
|
export function validateContract(contract) {
|
|
211
246
|
object(contract, "contract");
|
|
212
247
|
exactKeys(contract, new Set(["schemaVersion", "project", "sources", "items"]), "contract");
|
|
213
|
-
if (![1, 2].includes(contract.schemaVersion)) fail("schema-version-unsupported", "contract.schemaVersion must be 1 or
|
|
248
|
+
if (![1, 2, 3].includes(contract.schemaVersion)) fail("schema-version-unsupported", "contract.schemaVersion must be 1, 2, or 3");
|
|
214
249
|
object(contract.project, "contract.project");
|
|
215
250
|
exactKeys(contract.project, new Set(["id", "name", "root"]), "contract.project");
|
|
216
251
|
stableId(contract.project.id, "contract.project.id");
|
|
@@ -222,7 +257,7 @@ export function validateContract(contract) {
|
|
|
222
257
|
contract.sources.forEach((source, index) => validateSource(source, `contract.sources[${index}]`, {
|
|
223
258
|
contractSchemaVersion: contract.schemaVersion,
|
|
224
259
|
}));
|
|
225
|
-
contract.items.forEach((item, index) => validateItem(item, `contract.items[${index}]
|
|
260
|
+
contract.items.forEach((item, index) => validateItem(item, `contract.items[${index}]`, { contractSchemaVersion: contract.schemaVersion }));
|
|
226
261
|
assertUnique(contract.sources, "contract.sources");
|
|
227
262
|
assertUnique(contract.items, "contract.items");
|
|
228
263
|
const sourcesById = new Map(contract.sources.map((source) => [source.id, source]));
|
|
@@ -234,6 +269,15 @@ export function validateContract(contract) {
|
|
|
234
269
|
fail("source-reference-deprecated", `item ${item.id} references deprecated source ${source}`);
|
|
235
270
|
}
|
|
236
271
|
}
|
|
272
|
+
for (const sourceId of [
|
|
273
|
+
...(item.consumption?.requiredSources ?? []),
|
|
274
|
+
...(item.consumption?.conditionalSources ?? []),
|
|
275
|
+
]) {
|
|
276
|
+
const referenced = sourcesById.get(sourceId);
|
|
277
|
+
if (!referenced || !["file", "path", "json-pointer"].includes(referenced.kind)) {
|
|
278
|
+
fail("schema-invalid", `item ${item.id} consumption source must be a local source: ${sourceId}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
237
281
|
if (item.verification?.kind !== "none" && item.verification?.source) {
|
|
238
282
|
const referenced = sourcesById.get(item.verification.source);
|
|
239
283
|
if (!referenced) {
|
|
@@ -255,8 +299,8 @@ export function validateProposal(proposal) {
|
|
|
255
299
|
if (!Array.isArray(proposal.sources) || !Array.isArray(proposal.items)) {
|
|
256
300
|
fail("schema-invalid", "proposal.sources and proposal.items must be arrays");
|
|
257
301
|
}
|
|
258
|
-
proposal.sources.forEach((source, index) => validateSource(source, `proposal.sources[${index}]
|
|
259
|
-
proposal.items.forEach((item, index) => validateItem(item, `proposal.items[${index}]`, { proposal: true }));
|
|
302
|
+
proposal.sources.forEach((source, index) => validateSource(source, `proposal.sources[${index}]`, { contractSchemaVersion: 3, proposal: true }));
|
|
303
|
+
proposal.items.forEach((item, index) => validateItem(item, `proposal.items[${index}]`, { proposal: true, contractSchemaVersion: 3 }));
|
|
260
304
|
assertUnique(proposal.sources, "proposal.sources");
|
|
261
305
|
assertUnique(proposal.items, "proposal.items");
|
|
262
306
|
const sourceIds = new Set(proposal.sources.map((source) => source.id));
|
|
@@ -264,6 +308,15 @@ export function validateProposal(proposal) {
|
|
|
264
308
|
for (const source of item.sources) {
|
|
265
309
|
if (!sourceIds.has(source)) fail("source-reference-missing", `proposal item ${item.id} references unknown source ${source}`);
|
|
266
310
|
}
|
|
311
|
+
for (const sourceId of [
|
|
312
|
+
...(item.consumption?.requiredSources ?? []),
|
|
313
|
+
...(item.consumption?.conditionalSources ?? []),
|
|
314
|
+
]) {
|
|
315
|
+
const source = proposal.sources.find((entry) => entry.id === sourceId);
|
|
316
|
+
if (!source || !["file", "path", "json-pointer"].includes(source.kind)) {
|
|
317
|
+
fail("schema-invalid", `proposal item ${item.id} consumption source must be local: ${sourceId}`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
267
320
|
}
|
|
268
321
|
return proposal;
|
|
269
322
|
}
|
|
@@ -308,7 +361,7 @@ export function validateProjectionLock(lock) {
|
|
|
308
361
|
if (entry.regionId !== "project-context-ai-entry") fail("schema-invalid-enum", "AI Entry regionId is invalid");
|
|
309
362
|
string(entry.regionDigest, `${label}.regionDigest`);
|
|
310
363
|
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
|
|
364
|
+
if (![1, 2, 3, 4, 5].includes(entry.rendererVersion)) fail("schema-version-unsupported", "AI Entry rendererVersion must be 1, 2, 3, 4, or 5");
|
|
312
365
|
if (typeof entry.createdFile !== "boolean") fail("schema-invalid", `${label}.createdFile must be boolean`);
|
|
313
366
|
} else {
|
|
314
367
|
if (entry.target !== "agents" && entry.target !== "ruler") fail("schema-invalid-enum", "projection target is invalid");
|