frontend-project-context 1.8.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 +18 -1
- package/README.md +7 -7
- package/UPGRADING.md +10 -0
- package/docs/08-INSTALLATION-AND-DISTRIBUTION.md +9 -3
- package/docs/14-FORMAL-RELEASE-READINESS.md +14 -0
- package/docs/28-REAL-PROJECT-ONBOARDING-CLOSURE-DESIGN.md +10 -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 +3 -2
- package/docs/PRODUCT-SHARING-AND-ADOPTION-GUIDE.md +111 -0
- package/docs/README.md +14 -2
- package/docs/USER-AND-AI-OPERATION-MANUAL.md +3 -3
- 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/package.json +1 -1
- package/migration-manifest.json +30 -12
- package/package.json +2 -2
- package/schemas/capabilities.schema.json +14 -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 +17 -5
- package/schemas/migration-manifest.schema.json +3 -3
- package/schemas/migration-plan.schema.json +1 -1
- package/schemas/projection-lock.schema.json +1 -1
- package/schemas/upgrade-assessment.schema.json +1 -1
- 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 +22 -9
- package/src/project-context/approver.mjs +6 -2
- package/src/project-context/authoring.mjs +28 -8
- package/src/project-context/capabilities.mjs +4 -1
- package/src/project-context/checker.mjs +1 -1
- package/src/project-context/cli.mjs +29 -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 +23 -6
- package/src/project-context/exchange.mjs +3 -0
- package/src/project-context/initialization-instruction.mjs +20 -2
- package/src/project-context/maintenance.mjs +5 -4
- package/src/project-context/migration-manifest.mjs +3 -3
- package/src/project-context/renderer.mjs +11 -4
- package/src/project-context/source-reader.mjs +27 -2
- package/src/project-context/upgrade-schema.mjs +1 -1
|
@@ -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, 4].includes(entry.rendererVersion)) fail("schema-version-unsupported", "AI Entry rendererVersion must be 1, 2, 3, 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");
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { canonicalJson, digestJson } from "./canonical-json.mjs";
|
|
2
|
+
import { fail } from "./errors.mjs";
|
|
3
|
+
import { normalizeRelativePath } from "./path-policy.mjs";
|
|
4
|
+
import { scopeApplies } from "./scope-compiler.mjs";
|
|
5
|
+
|
|
6
|
+
const COVERAGE_SUBJECT = "project.registration-coverage";
|
|
7
|
+
const DISPOSITIONS = new Set(["contract-covered", "dynamic-investigation", "excluded-approved", "unresolved"]);
|
|
8
|
+
|
|
9
|
+
function uniqueSorted(values) {
|
|
10
|
+
return [...new Set(values)].sort((left, right) => left.localeCompare(right));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function object(value, label) {
|
|
14
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) fail("coverage-profile-invalid", `${label} must be an object`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function exactKeys(value, required, optional, label) {
|
|
18
|
+
object(value, label);
|
|
19
|
+
const allowed = new Set([...required, ...optional]);
|
|
20
|
+
for (const key of Object.keys(value)) if (!allowed.has(key)) fail("coverage-profile-invalid", `${label} contains unknown field: ${key}`);
|
|
21
|
+
for (const key of required) if (!Object.hasOwn(value, key)) fail("coverage-profile-invalid", `${label}.${key} is required`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function strings(values, label) {
|
|
25
|
+
if (!Array.isArray(values) || values.some((value) => typeof value !== "string")) fail("coverage-profile-invalid", `${label} must be a string array`);
|
|
26
|
+
return values;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizePath(value, label) {
|
|
30
|
+
if (typeof value !== "string") fail("coverage-profile-invalid", `${label} must be a string`);
|
|
31
|
+
try {
|
|
32
|
+
return normalizeRelativePath(value, { allowRoot: true, label });
|
|
33
|
+
} catch {
|
|
34
|
+
fail("coverage-profile-invalid", `${label} must be a valid project-relative path`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizedPaths(values, label) {
|
|
39
|
+
const normalized = strings(values, label).map((value) => normalizePath(value, `${label} entry`));
|
|
40
|
+
return uniqueSorted(normalized);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function coverageProfile(contract) {
|
|
44
|
+
const candidates = contract.items.filter((item) => item.status === "approved" && item.kind === "policy" && item.subject === COVERAGE_SUBJECT);
|
|
45
|
+
if (candidates.length === 0) return null;
|
|
46
|
+
if (candidates.length > 1) fail("coverage-profile-conflict", `multiple approved ${COVERAGE_SUBJECT} items are active`);
|
|
47
|
+
const item = candidates[0];
|
|
48
|
+
const value = item.value;
|
|
49
|
+
object(value, `${item.id} coverage value`);
|
|
50
|
+
if (value.version === 2) {
|
|
51
|
+
exactKeys(value, ["version", "domains"], [], `${item.id} coverage v2 value`);
|
|
52
|
+
if (!Array.isArray(value.domains)) fail("coverage-profile-invalid", `${item.id} coverage v2 domains must be an array`);
|
|
53
|
+
const approved = new Map(contract.items.filter((entry) => entry.status === "approved").map((entry) => [entry.id, entry]));
|
|
54
|
+
const ids = new Set();
|
|
55
|
+
const paths = new Set();
|
|
56
|
+
const domains = value.domains.map((domain, index) => {
|
|
57
|
+
const label = `${item.id} domains[${index}]`;
|
|
58
|
+
exactKeys(domain, ["id", "path", "disposition", "itemIds"], ["rationale"], label);
|
|
59
|
+
if (typeof domain.id !== "string" || domain.id.length === 0 || ids.has(domain.id)) fail("coverage-profile-invalid", `${label}.id must be unique`);
|
|
60
|
+
ids.add(domain.id);
|
|
61
|
+
const normalizedPath = normalizePath(domain.path, `${label}.path`);
|
|
62
|
+
if (normalizedPath !== domain.path || paths.has(normalizedPath)) fail("coverage-profile-invalid", `${label}.path must be normalized and unique`);
|
|
63
|
+
paths.add(normalizedPath);
|
|
64
|
+
if (!DISPOSITIONS.has(domain.disposition)) fail("coverage-profile-invalid", `${label}.disposition is invalid`);
|
|
65
|
+
const itemIds = uniqueSorted(strings(domain.itemIds, `${label}.itemIds`));
|
|
66
|
+
if (canonicalJson(itemIds) !== canonicalJson(domain.itemIds)) fail("coverage-profile-invalid", `${label}.itemIds must be sorted and unique`);
|
|
67
|
+
if (domain.disposition === "contract-covered") {
|
|
68
|
+
if (itemIds.length === 0) fail("coverage-profile-invalid", `${label} contract-covered domain requires itemIds`);
|
|
69
|
+
for (const itemId of itemIds) {
|
|
70
|
+
const covered = approved.get(itemId);
|
|
71
|
+
if (!covered || !scopeApplies(covered.scope, normalizedPath)) fail("coverage-profile-invalid", `${label} references an item that is not effective for its path: ${itemId}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (domain.disposition === "excluded-approved" && (typeof domain.rationale !== "string" || domain.rationale.length === 0)) {
|
|
75
|
+
fail("coverage-profile-invalid", `${label} excluded-approved domain requires rationale`);
|
|
76
|
+
}
|
|
77
|
+
if (domain.rationale !== undefined && typeof domain.rationale !== "string") fail("coverage-profile-invalid", `${label}.rationale must be a string`);
|
|
78
|
+
return {
|
|
79
|
+
id: domain.id,
|
|
80
|
+
path: normalizedPath,
|
|
81
|
+
disposition: domain.disposition,
|
|
82
|
+
itemIds,
|
|
83
|
+
...(domain.rationale !== undefined ? { rationale: domain.rationale } : {}),
|
|
84
|
+
};
|
|
85
|
+
}).sort((left, right) => left.id.localeCompare(right.id));
|
|
86
|
+
if (canonicalJson(domains) !== canonicalJson(value.domains)) fail("coverage-profile-invalid", `${item.id} domains must be stably sorted by id`);
|
|
87
|
+
return { itemId: item.id, itemDigest: digestJson(item), sourceIds: [...item.sources].sort(), version: 2, domains };
|
|
88
|
+
}
|
|
89
|
+
exactKeys(value, ["roots", "excludedPaths"], [], `${item.id} coverage value`);
|
|
90
|
+
return {
|
|
91
|
+
itemId: item.id,
|
|
92
|
+
itemDigest: digestJson(item),
|
|
93
|
+
sourceIds: [...item.sources].sort(),
|
|
94
|
+
version: 1,
|
|
95
|
+
roots: normalizedPaths(value.roots, "coverage roots"),
|
|
96
|
+
excludedPaths: normalizedPaths(value.excludedPaths, "coverage excluded paths"),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function pathWithin(target, parent) {
|
|
101
|
+
return parent === "." || target === parent || target.startsWith(`${parent}/`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function coverageDomainForPath(profile, candidatePath) {
|
|
105
|
+
if (!profile || profile.version !== 2) return null;
|
|
106
|
+
return profile.domains
|
|
107
|
+
.filter((entry) => pathWithin(candidatePath, entry.path))
|
|
108
|
+
.sort((left, right) => right.path.length - left.path.length || left.id.localeCompare(right.id))[0] ?? null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function coverageClass(profile, registeredPaths, candidatePath) {
|
|
112
|
+
if (registeredPaths.has(candidatePath)) return "registered";
|
|
113
|
+
if (!profile) return "outside-declared-coverage";
|
|
114
|
+
if (profile.version === 2) {
|
|
115
|
+
const domain = coverageDomainForPath(profile, candidatePath);
|
|
116
|
+
if (!domain) return "outside-declared-coverage";
|
|
117
|
+
return {
|
|
118
|
+
"contract-covered": "registered",
|
|
119
|
+
"dynamic-investigation": "dynamic-investigation",
|
|
120
|
+
"excluded-approved": "excluded-approved",
|
|
121
|
+
unresolved: "unresolved",
|
|
122
|
+
}[domain.disposition];
|
|
123
|
+
}
|
|
124
|
+
if (profile.excludedPaths.some((entry) => pathWithin(candidatePath, entry))) return "excluded-approved";
|
|
125
|
+
if (profile.roots.some((entry) => pathWithin(candidatePath, entry))) return "review-required";
|
|
126
|
+
return "outside-declared-coverage";
|
|
127
|
+
}
|
|
@@ -2,11 +2,11 @@ import { canonicalValue, digestJson, validateJsonValue } from "./canonical-json.
|
|
|
2
2
|
import { fail } from "./errors.mjs";
|
|
3
3
|
import { normalizeRelativePath } from "./path-policy.mjs";
|
|
4
4
|
|
|
5
|
-
export const PACKAGE_VERSION = "1.
|
|
6
|
-
export const EXCHANGE_PROTOCOL_VERSION =
|
|
5
|
+
export const PACKAGE_VERSION = "1.9.1";
|
|
6
|
+
export const EXCHANGE_PROTOCOL_VERSION = 9;
|
|
7
7
|
export const ACTION_PLAN_SCHEMA_VERSION = 2;
|
|
8
8
|
export const REVIEW_BUNDLE_SCHEMA_VERSION = 2;
|
|
9
|
-
export const CAPABILITIES_SCHEMA_VERSION =
|
|
9
|
+
export const CAPABILITIES_SCHEMA_VERSION = 9;
|
|
10
10
|
|
|
11
11
|
export const ACTION_KINDS = Object.freeze([
|
|
12
12
|
"accept-source-change",
|
|
@@ -143,7 +143,7 @@ function normalizeScope(value, label) {
|
|
|
143
143
|
function normalizeItemInput(input, label, extra = []) {
|
|
144
144
|
object(input, label);
|
|
145
145
|
exactKeys(input, new Set([
|
|
146
|
-
"id", "kind", "subject", "value", "statement", "sources", "scope", "overrides", "verification", ...extra,
|
|
146
|
+
"id", "kind", "subject", "value", "statement", "sources", "scope", "overrides", "verification", "consumption", ...extra,
|
|
147
147
|
]), label);
|
|
148
148
|
stableId(input.id, `${label}.id`);
|
|
149
149
|
if (!ITEM_KINDS.has(input.kind)) invalid(`${label}.kind is invalid`);
|
|
@@ -159,6 +159,16 @@ function normalizeItemInput(input, label, extra = []) {
|
|
|
159
159
|
sources.forEach((entry) => stableId(entry, `${label}.sources entry`));
|
|
160
160
|
const overrides = uniqueStrings(input.overrides ?? [], `${label}.overrides`, { empty: true });
|
|
161
161
|
overrides.forEach((entry) => stableId(entry, `${label}.overrides entry`));
|
|
162
|
+
let consumption;
|
|
163
|
+
if (input.consumption !== undefined) {
|
|
164
|
+
object(input.consumption, `${label}.consumption`);
|
|
165
|
+
exactKeys(input.consumption, new Set(["requiredSources", "conditionalSources"]), `${label}.consumption`);
|
|
166
|
+
const requiredSources = uniqueStrings(input.consumption.requiredSources, `${label}.consumption.requiredSources`, { empty: true });
|
|
167
|
+
const conditionalSources = uniqueStrings(input.consumption.conditionalSources, `${label}.consumption.conditionalSources`, { empty: true });
|
|
168
|
+
for (const source of [...requiredSources, ...conditionalSources]) if (!sources.includes(source)) invalid(`${label}.consumption source must be included in sources: ${source}`);
|
|
169
|
+
if (requiredSources.some((source) => conditionalSources.includes(source))) invalid(`${label}.consumption roles must be mutually exclusive`);
|
|
170
|
+
consumption = { requiredSources, conditionalSources };
|
|
171
|
+
}
|
|
162
172
|
return {
|
|
163
173
|
id: input.id,
|
|
164
174
|
kind: input.kind,
|
|
@@ -168,18 +178,23 @@ function normalizeItemInput(input, label, extra = []) {
|
|
|
168
178
|
sources,
|
|
169
179
|
scope: normalizeScope(input.scope, `${label}.scope`),
|
|
170
180
|
overrides,
|
|
181
|
+
...(consumption ? { consumption } : {}),
|
|
171
182
|
...(input.verification !== undefined ? { verification: normalizeVerification(input.verification, `${label}.verification`) } : {}),
|
|
172
183
|
};
|
|
173
184
|
}
|
|
174
185
|
|
|
175
186
|
function normalizeSourceInput(input, label) {
|
|
176
187
|
object(input, label);
|
|
177
|
-
exactKeys(input, new Set(["id", "kind", "path", "pointer", "reference"]), label);
|
|
188
|
+
exactKeys(input, new Set(["id", "kind", "path", "pointer", "reference", "digestMode"]), label);
|
|
178
189
|
stableId(input.id, `${label}.id`);
|
|
179
190
|
if (!SOURCE_KINDS.has(input.kind)) invalid(`${label}.kind is invalid`);
|
|
180
191
|
if (["file", "path", "json-pointer"].includes(input.kind)) {
|
|
181
192
|
if (input.reference !== undefined) invalid(`${label}.reference is not allowed for local sources`);
|
|
182
193
|
const normalized = { id: input.id, kind: input.kind, path: projectPath(input.path, `${label}.path`) };
|
|
194
|
+
if (input.digestMode !== undefined) {
|
|
195
|
+
if (input.kind !== "file" || !["full-file", "outside-owned-ai-entry"].includes(input.digestMode)) invalid(`${label}.digestMode is invalid`);
|
|
196
|
+
normalized.digestMode = input.digestMode;
|
|
197
|
+
}
|
|
183
198
|
if (input.kind === "json-pointer") {
|
|
184
199
|
string(input.pointer, `${label}.pointer`, { empty: true });
|
|
185
200
|
if (input.pointer !== "" && !input.pointer.startsWith("/")) invalid(`${label}.pointer must use RFC 6901 syntax`);
|
|
@@ -187,7 +202,7 @@ function normalizeSourceInput(input, label) {
|
|
|
187
202
|
} else if (input.pointer !== undefined) invalid(`${label}.pointer is only allowed for json-pointer`);
|
|
188
203
|
return normalized;
|
|
189
204
|
}
|
|
190
|
-
if (input.path !== undefined || input.pointer !== undefined) invalid(`${label} cannot contain a local path`);
|
|
205
|
+
if (input.path !== undefined || input.pointer !== undefined || input.digestMode !== undefined) invalid(`${label} cannot contain a local path or digest mode`);
|
|
191
206
|
string(input.reference, `${label}.reference`);
|
|
192
207
|
return { id: input.id, kind: input.kind, reference: input.reference };
|
|
193
208
|
}
|
|
@@ -531,6 +546,8 @@ export function itemPrimitiveInput(input) {
|
|
|
531
546
|
scope: input.scope.kind,
|
|
532
547
|
scopePath: input.scope.path,
|
|
533
548
|
overrides: [...input.overrides],
|
|
549
|
+
requiredSources: [...(input.consumption?.requiredSources ?? [])],
|
|
550
|
+
conditionalSources: [...(input.consumption?.conditionalSources ?? [])],
|
|
534
551
|
verification: input.verification?.kind,
|
|
535
552
|
verificationSource: input.verification?.source,
|
|
536
553
|
verificationExpectedPresent: Boolean(input.verification && Object.hasOwn(input.verification, "expected")),
|
|
@@ -71,6 +71,8 @@ function itemArgs(input, options = {}) {
|
|
|
71
71
|
"--scope", input.scope.kind,
|
|
72
72
|
...(input.scope.path ? ["--scope-path", input.scope.path] : []),
|
|
73
73
|
...(input.overrides.length > 0 ? ["--overrides", ...input.overrides] : []),
|
|
74
|
+
...(input.consumption?.requiredSources?.length > 0 ? ["--required-source", ...input.consumption.requiredSources] : []),
|
|
75
|
+
...(input.consumption?.conditionalSources?.length > 0 ? ["--conditional-source", ...input.consumption.conditionalSources] : []),
|
|
74
76
|
];
|
|
75
77
|
if (input.verification) {
|
|
76
78
|
args.push("--verification", input.verification.kind);
|
|
@@ -89,6 +91,7 @@ function sourceArgs(input) {
|
|
|
89
91
|
...(input.path ? ["--path", input.path] : []),
|
|
90
92
|
...(input.pointer !== undefined ? ["--pointer", input.pointer] : []),
|
|
91
93
|
...(input.reference ? ["--reference", input.reference] : []),
|
|
94
|
+
...(input.digestMode ? ["--digest-mode", input.digestMode] : []),
|
|
92
95
|
];
|
|
93
96
|
}
|
|
94
97
|
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
+
import path from "node:path";
|
|
3
4
|
import { sha256 } from "./canonical-json.mjs";
|
|
4
5
|
import { PACKAGE_VERSION } from "./exchange-schema.mjs";
|
|
5
6
|
|
|
6
|
-
export const INITIALIZATION_INSTRUCTION_SCHEMA_VERSION =
|
|
7
|
+
export const INITIALIZATION_INSTRUCTION_SCHEMA_VERSION = 2;
|
|
7
8
|
export const INITIALIZATION_INSTRUCTION_ID = "ai-project-initialization";
|
|
8
|
-
export const INITIALIZATION_INSTRUCTION_VERSION =
|
|
9
|
+
export const INITIALIZATION_INSTRUCTION_VERSION = 2;
|
|
9
10
|
export const INITIALIZATION_INSTRUCTION_PACKAGE_PATH = "docs/AI-PROJECT-INITIALIZATION.md";
|
|
10
11
|
export const INITIALIZATION_INSTRUCTION_COMMAND = "project-context instructions --project PATH [--json | --prompt]";
|
|
11
12
|
|
|
@@ -23,12 +24,29 @@ export async function readInitializationInstruction() {
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
export async function buildInitializationInstruction(targetRoot, boundaries) {
|
|
27
|
+
let entryContent = null;
|
|
28
|
+
try {
|
|
29
|
+
entryContent = await readFile(path.join(targetRoot, "AGENTS.md"), "utf8");
|
|
30
|
+
} catch (error) {
|
|
31
|
+
if (error?.code !== "ENOENT") throw error;
|
|
32
|
+
}
|
|
26
33
|
return {
|
|
27
34
|
schemaVersion: INITIALIZATION_INSTRUCTION_SCHEMA_VERSION,
|
|
28
35
|
package: { name: "frontend-project-context", version: PACKAGE_VERSION },
|
|
29
36
|
instruction: await readInitializationInstruction(),
|
|
30
37
|
targetRoot,
|
|
31
38
|
boundaries: { ...boundaries },
|
|
39
|
+
entryRuleReview: {
|
|
40
|
+
path: "AGENTS.md",
|
|
41
|
+
present: entryContent !== null,
|
|
42
|
+
allowedDispositions: ["migrated-to-contract", "kept-in-entry-intentionally", "excluded-as-stale", "unresolved-blocker"],
|
|
43
|
+
records: entryContent === null || entryContent.trim().length === 0 ? [] : [{
|
|
44
|
+
id: "human-entry-content",
|
|
45
|
+
disposition: "unresolved-blocker",
|
|
46
|
+
rationale: "Classify every duplicated or legacy human entry rule in the concentrated initialization review before completion.",
|
|
47
|
+
}],
|
|
48
|
+
automaticDeletion: false,
|
|
49
|
+
},
|
|
32
50
|
};
|
|
33
51
|
}
|
|
34
52
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { digestJson } from "./canonical-json.mjs";
|
|
2
2
|
import { buildItemProposal } from "./authoring.mjs";
|
|
3
|
-
import { sourceStatus, validateContract, validateSourceLock } from "./contract-schema.mjs";
|
|
3
|
+
import { promoteContractToSchema3, sourceStatus, validateContract, validateSourceLock } from "./contract-schema.mjs";
|
|
4
4
|
import { fail } from "./errors.mjs";
|
|
5
5
|
import { writeProjectState } from "./project-store.mjs";
|
|
6
6
|
import { readSourceDigest } from "./source-reader.mjs";
|
|
@@ -104,7 +104,7 @@ export async function reviewSource(root, project, sourceId, options = {}) {
|
|
|
104
104
|
let status;
|
|
105
105
|
let reason;
|
|
106
106
|
try {
|
|
107
|
-
currentDigest = await readSourceDigest(root, source, options.sourceReadContext);
|
|
107
|
+
currentDigest = await readSourceDigest(root, source, options.sourceReadContext, { projectionsLock: project.projectionsLock });
|
|
108
108
|
if (lockedDigest === null || lockedDigest !== source.digest) {
|
|
109
109
|
status = "unreadable";
|
|
110
110
|
reason = lockedDigest === null ? "source-lock-missing" : "source-lock-mismatch";
|
|
@@ -179,7 +179,7 @@ export async function acceptSourceChange(root, project, input, options = {}) {
|
|
|
179
179
|
if (options.write) {
|
|
180
180
|
let actual;
|
|
181
181
|
try {
|
|
182
|
-
actual = await readSourceDigest(root, source);
|
|
182
|
+
actual = await readSourceDigest(root, source, undefined, { projectionsLock: project.projectionsLock });
|
|
183
183
|
} catch (error) {
|
|
184
184
|
fail("source-accept-digest-mismatch", "source became unavailable while acceptance was being prepared", {
|
|
185
185
|
exitCode: 1,
|
|
@@ -266,7 +266,7 @@ export async function deprecateSource(project, input, options = {}) {
|
|
|
266
266
|
|
|
267
267
|
const timestamp = input.at ?? new Date().toISOString();
|
|
268
268
|
const nextContract = structuredClone(project.contract);
|
|
269
|
-
nextContract.schemaVersion = 2;
|
|
269
|
+
if (nextContract.schemaVersion < 2) nextContract.schemaVersion = 2;
|
|
270
270
|
nextContract.sources = nextContract.sources.map((source) => {
|
|
271
271
|
const active = source.status === undefined ? { ...source, status: "active" } : source;
|
|
272
272
|
if (source.id !== input.id) return active;
|
|
@@ -326,6 +326,7 @@ export async function reviseItem(root, project, input, options = {}) {
|
|
|
326
326
|
});
|
|
327
327
|
}
|
|
328
328
|
const nextContract = structuredClone(project.contract);
|
|
329
|
+
if (replacement.consumption !== undefined) promoteContractToSchema3(nextContract);
|
|
329
330
|
nextContract.items[nextContract.items.findIndex((item) => item.id === input.id)] = replacement;
|
|
330
331
|
validateContract(nextContract);
|
|
331
332
|
if (options.write) await writeProjectState(project, nextContract, project.sourcesLock, options.storeOptions);
|
|
@@ -22,7 +22,7 @@ const MIGRATION_KINDS = new Set([
|
|
|
22
22
|
]);
|
|
23
23
|
const ROLLBACK_CLASSES = new Set(["package-only", "reversible-data", "forward-only"]);
|
|
24
24
|
const CONSUMER_CHANGE_KEYS = new Set([
|
|
25
|
-
"actionPlan", "adaptiveContextBundle", "capabilities", "contextQuery", "coverageAudit", "evidenceBundle", "evidenceInput", "exchange",
|
|
25
|
+
"actionPlan", "adaptiveContextBundle", "capabilities", "contextBundle", "contextQuery", "coverageAudit", "evidenceBundle", "evidenceInput", "exchange",
|
|
26
26
|
"hostPromotionEvidence", "integrationReviewBundle", "reviewBundle", "routingIndex", "stageContextBundle", "stageReceipt", "taskContextPlan",
|
|
27
27
|
"truthReconciliationInput", "truthReconciliationReviewBundle", "initializationInstruction", "projectStatus",
|
|
28
28
|
]);
|
|
@@ -103,7 +103,7 @@ export function validateMigrationManifest(input) {
|
|
|
103
103
|
]), "migration manifest");
|
|
104
104
|
if (manifest.schemaVersion !== MIGRATION_MANIFEST_SCHEMA_VERSION) invalid("migration manifest schemaVersion must be 2");
|
|
105
105
|
exactKeys(manifest.package, new Set(["name", "version"]), "migration manifest package");
|
|
106
|
-
if (manifest.package.name !== "frontend-project-context" || manifest.package.version !== "1.
|
|
106
|
+
if (manifest.package.name !== "frontend-project-context" || manifest.package.version !== "1.9.1") invalid("migration manifest package does not match this runtime");
|
|
107
107
|
versions(manifest.upgradeFrom, "upgradeFrom");
|
|
108
108
|
if (manifest.upgradeFrom.length === 0) invalid("upgradeFrom must not be empty");
|
|
109
109
|
exactKeys(manifest.stores, new Set(["contract", "projectionLock", "proposal", "sourceLock"]), "stores");
|
|
@@ -111,7 +111,7 @@ export function validateMigrationManifest(input) {
|
|
|
111
111
|
exactKeys(manifest.renderers, new Set(["aiEntry", "projection"]), "renderers");
|
|
112
112
|
for (const name of Object.keys(manifest.renderers)) versionMatrix(manifest.renderers[name], `renderers.${name}`);
|
|
113
113
|
exactKeys(manifest.protocols, new Set([
|
|
114
|
-
"exchange", "actionPlan", "adaptiveContextBundle", "contextQuery", "coverageAudit", "reviewBundle", "evidenceInput", "evidenceBundle", "taskContextPlan",
|
|
114
|
+
"exchange", "actionPlan", "adaptiveContextBundle", "contextBundle", "contextQuery", "coverageAudit", "reviewBundle", "evidenceInput", "evidenceBundle", "taskContextPlan",
|
|
115
115
|
"routingIndex", "stageReceipt", "stageContextBundle", "integrationReviewBundle", "hostPromotionEvidence",
|
|
116
116
|
"truthReconciliationInput", "truthReconciliationReviewBundle", "initializationInstruction", "projectStatus",
|
|
117
117
|
]), "protocols");
|
|
@@ -64,7 +64,7 @@ function groupedSections(sections) {
|
|
|
64
64
|
return groups;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
function
|
|
67
|
+
export function renderCollectedContextBundle(bundle, sources, task, options = {}) {
|
|
68
68
|
const locale = options.locale ?? "zh-CN";
|
|
69
69
|
const lines = [
|
|
70
70
|
"# Project Context Bundle",
|
|
@@ -100,11 +100,18 @@ function renderCollectedBundle(bundle, sources, task, options = {}) {
|
|
|
100
100
|
lines.push(`- \`${id}\` — ${source?.kind ?? "unknown"}: \`${location}\``);
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
|
+
if ((options.gaps ?? []).length > 0) {
|
|
104
|
+
lines.push("", "## Context gaps", "");
|
|
105
|
+
for (const gap of options.gaps) lines.push(`- \`${gap.code}\`${gap.path ? ` — \`${gap.path}\`` : ""}`);
|
|
106
|
+
}
|
|
107
|
+
if ((options.nextActions ?? []).length > 0) {
|
|
108
|
+
lines.push("", "## Next actions", "", ...options.nextActions.map((action) => `- \`${action}\``));
|
|
109
|
+
}
|
|
103
110
|
return `${lines.join("\n")}\n`;
|
|
104
111
|
}
|
|
105
112
|
|
|
106
113
|
export function renderContextBundle(contract, paths, task, options = {}) {
|
|
107
|
-
return
|
|
114
|
+
return renderCollectedContextBundle(collectBundle(contract, paths), contract.sources, task, options);
|
|
108
115
|
}
|
|
109
116
|
|
|
110
117
|
export function renderSelectedContextBundle(contract, paths, itemIds, task, options = {}) {
|
|
@@ -122,7 +129,7 @@ export function renderSelectedContextBundle(contract, paths, itemIds, task, opti
|
|
|
122
129
|
sections,
|
|
123
130
|
itemIds: [...selected].sort(),
|
|
124
131
|
};
|
|
125
|
-
return
|
|
132
|
+
return renderCollectedContextBundle(bundle, contract.sources, task, options);
|
|
126
133
|
}
|
|
127
134
|
|
|
128
135
|
// Task delivery deliberately has its own renderer. Managed AGENTS/Ruler projections
|
|
@@ -183,7 +190,7 @@ export function renderTaskContextBundle(contract, paths, itemIds, task, options
|
|
|
183
190
|
|
|
184
191
|
export function renderProjection(contract, paths, target) {
|
|
185
192
|
const bundle = collectBundle(contract, paths);
|
|
186
|
-
const body =
|
|
193
|
+
const body = renderCollectedContextBundle(bundle, contract.sources);
|
|
187
194
|
const bundleDigest = sha256(body);
|
|
188
195
|
const marker = `<!-- managed-by: project-context; renderer-version: ${RENDERER_VERSION}; target: ${target}; contract-digest: ${bundle.contractDigest}; bundle-digest: ${bundleDigest} -->`;
|
|
189
196
|
return {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { canonicalJson, sha256 } from "./canonical-json.mjs";
|
|
3
|
+
import { canonicalJson, digestJson, sha256 } from "./canonical-json.mjs";
|
|
4
|
+
import { AI_ENTRY_REGION_ID, parseAiEntryRegion } from "./ai-entry.mjs";
|
|
4
5
|
import { sourceStatus } from "./contract-schema.mjs";
|
|
5
6
|
import { fail } from "./errors.mjs";
|
|
6
7
|
import { resolveExistingInside } from "./path-policy.mjs";
|
|
@@ -97,10 +98,34 @@ export async function digestPathIdentity(projectRoot, relativePath, context) {
|
|
|
97
98
|
});
|
|
98
99
|
}
|
|
99
100
|
|
|
100
|
-
|
|
101
|
+
async function digestOutsideOwnedAiEntry(projectRoot, source, context, projectionsLock) {
|
|
102
|
+
const lock = projectionsLock?.projections?.find((entry) => entry.path === source.path);
|
|
103
|
+
if (!lock || lock.ownership !== "region" || lock.target !== "ai-entry" || lock.regionId !== AI_ENTRY_REGION_ID) {
|
|
104
|
+
fail("source-digest-ownership-invalid", `outside-owned-ai-entry requires a trusted AI Entry lock: ${source.path}`);
|
|
105
|
+
}
|
|
106
|
+
const { absolute } = await resolveExistingInside(projectRoot, source.path);
|
|
107
|
+
const content = await cached(context, `body:${absolute}`, async () => {
|
|
108
|
+
if (context?.metrics) context.metrics.sourceBodyReads += 1;
|
|
109
|
+
return readFile(absolute, "utf8");
|
|
110
|
+
});
|
|
111
|
+
const parsed = parseAiEntryRegion(content);
|
|
112
|
+
if (parsed.state !== "present") fail("source-digest-marker-invalid", `AI Entry markers are not valid: ${source.path}`);
|
|
113
|
+
if (sha256(parsed.region) !== lock.regionDigest) fail("source-digest-ownership-invalid", `AI Entry region digest does not match its lock: ${source.path}`);
|
|
114
|
+
const renderer = /project-context:ai-entry; schema-version: 1; renderer-version: (\d+)/u.exec(parsed.region)?.[1];
|
|
115
|
+
if (renderer === undefined || Number(renderer) !== lock.rendererVersion) {
|
|
116
|
+
fail("source-digest-ownership-invalid", `AI Entry renderer does not match its lock: ${source.path}`);
|
|
117
|
+
}
|
|
118
|
+
return digestJson({ before: content.slice(0, parsed.start), after: content.slice(parsed.end) });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function readSourceDigest(projectRoot, source, context, options = {}) {
|
|
101
122
|
if (sourceStatus(source) === "deprecated") return null;
|
|
102
123
|
if (source.kind === "human-decision" || source.kind === "external-reference") return null;
|
|
103
124
|
if (source.kind === "path") return digestPathIdentity(projectRoot, source.path, context);
|
|
125
|
+
if (source.kind === "file" && source.digestMode === "outside-owned-ai-entry") {
|
|
126
|
+
return cached(context, `digest:outside-owned-ai-entry:${source.path}`, () =>
|
|
127
|
+
digestOutsideOwnedAiEntry(projectRoot, source, context, options.projectionsLock));
|
|
128
|
+
}
|
|
104
129
|
if (source.kind === "file") return digestPath(projectRoot, source.path, context);
|
|
105
130
|
return sha256(canonicalJson(await readJsonSourceValue(projectRoot, source, context)));
|
|
106
131
|
}
|
|
@@ -121,7 +121,7 @@ export function validateUpgradeAssessment(input) {
|
|
|
121
121
|
stores: new Set(["contract", "projectionLock", "proposal", "sourceLock"]),
|
|
122
122
|
renderers: new Set(["aiEntry", "projection"]),
|
|
123
123
|
protocols: new Set([
|
|
124
|
-
"actionPlan", "adaptiveContextBundle", "contextQuery", "coverageAudit", "evidenceBundle", "evidenceInput", "exchange",
|
|
124
|
+
"actionPlan", "adaptiveContextBundle", "contextBundle", "contextQuery", "coverageAudit", "evidenceBundle", "evidenceInput", "exchange",
|
|
125
125
|
"hostPromotionEvidence", "integrationReviewBundle", "reviewBundle", "routingIndex", "stageContextBundle", "stageReceipt",
|
|
126
126
|
"taskContextPlan", "truthReconciliationInput", "truthReconciliationReviewBundle", "initializationInstruction", "projectStatus",
|
|
127
127
|
]),
|