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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { canonicalJson } from "./canonical-json.mjs";
|
|
2
2
|
import {
|
|
3
|
+
promoteContractToSchema3,
|
|
3
4
|
sourceForContract,
|
|
4
5
|
sourceRegistrationShape,
|
|
5
6
|
sourceStatus,
|
|
@@ -32,8 +33,8 @@ function sourceLocator(source) {
|
|
|
32
33
|
});
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
export async function buildRegisteredSource(root, input) {
|
|
36
|
-
const { id, kind, path: sourcePath, pointer, reference } = input;
|
|
36
|
+
export async function buildRegisteredSource(root, input, options = {}) {
|
|
37
|
+
const { id, kind, path: sourcePath, pointer, reference, digestMode } = input;
|
|
37
38
|
let source;
|
|
38
39
|
if (LOCAL_SOURCE_KINDS.has(kind)) {
|
|
39
40
|
if (!has(sourcePath) || has(reference) || (kind === "json-pointer" ? !has(pointer) : has(pointer))) {
|
|
@@ -44,21 +45,23 @@ export async function buildRegisteredSource(root, input) {
|
|
|
44
45
|
kind,
|
|
45
46
|
path: normalizeRelativePath(sourcePath, { label: "source path" }),
|
|
46
47
|
...(kind === "json-pointer" ? { pointer } : {}),
|
|
48
|
+
...(digestMode !== undefined ? { digestMode } : {}),
|
|
47
49
|
};
|
|
48
|
-
|
|
50
|
+
if (digestMode !== undefined && kind !== "file") fail("argument-conflict", "--digest-mode is only valid for file sources");
|
|
51
|
+
source.digest = await readSourceDigest(root, source, undefined, { projectionsLock: options.projectionsLock });
|
|
49
52
|
} else if (REFERENCE_SOURCE_KINDS.has(kind)) {
|
|
50
|
-
if (!has(reference) || has(sourcePath) || has(pointer)) {
|
|
53
|
+
if (!has(reference) || has(sourcePath) || has(pointer) || has(digestMode)) {
|
|
51
54
|
fail("argument-conflict", `${kind} source requires only --reference`, { details: { source: id } });
|
|
52
55
|
}
|
|
53
56
|
source = { id, kind, reference };
|
|
54
57
|
} else {
|
|
55
58
|
fail("schema-invalid-enum", `source kind is invalid: ${kind}`, { details: { source: id } });
|
|
56
59
|
}
|
|
57
|
-
return validateSource(source);
|
|
60
|
+
return validateSource(source, "source", { contractSchemaVersion: digestMode === undefined ? 1 : 3, proposal: true });
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
export async function registerSource(root, project, input, options = {}) {
|
|
61
|
-
const source = await buildRegisteredSource(root, input);
|
|
64
|
+
const source = await buildRegisteredSource(root, input, { projectionsLock: project.projectionsLock });
|
|
62
65
|
const existingById = project.contract.sources.find((entry) => entry.id === source.id);
|
|
63
66
|
if (existingById) {
|
|
64
67
|
if (sourceStatus(existingById) === "deprecated" || canonicalJson(sourceRegistrationShape(existingById)) !== canonicalJson(source)) {
|
|
@@ -77,6 +80,7 @@ export async function registerSource(root, project, input, options = {}) {
|
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
const nextContract = structuredClone(project.contract);
|
|
83
|
+
if (source.digestMode !== undefined) promoteContractToSchema3(nextContract);
|
|
80
84
|
const storedSource = sourceForContract(source, nextContract.schemaVersion);
|
|
81
85
|
nextContract.sources.push(storedSource);
|
|
82
86
|
nextContract.sources.sort((left, right) => left.id.localeCompare(right.id));
|
|
@@ -91,7 +95,7 @@ export async function registerSource(root, project, input, options = {}) {
|
|
|
91
95
|
|
|
92
96
|
if (options.write) {
|
|
93
97
|
if (LOCAL_SOURCE_KINDS.has(source.kind)) {
|
|
94
|
-
const actual = await readSourceDigest(root, source);
|
|
98
|
+
const actual = await readSourceDigest(root, source, undefined, { projectionsLock: project.projectionsLock });
|
|
95
99
|
if (actual !== source.digest) {
|
|
96
100
|
fail("source-changed-during-register", `source changed while registration was being prepared: ${source.id}`, {
|
|
97
101
|
exitCode: 1,
|
|
@@ -157,6 +161,21 @@ export function buildItemProposal(project, input) {
|
|
|
157
161
|
details: { item: input.id, source: verification.source },
|
|
158
162
|
});
|
|
159
163
|
}
|
|
164
|
+
const requiredSources = input.requiredSources ?? [];
|
|
165
|
+
const conditionalSources = input.conditionalSources ?? [];
|
|
166
|
+
if (new Set(requiredSources).size !== requiredSources.length) fail("schema-duplicate", "--required-source contains duplicate IDs");
|
|
167
|
+
if (new Set(conditionalSources).size !== conditionalSources.length) fail("schema-duplicate", "--conditional-source contains duplicate IDs");
|
|
168
|
+
const roleSources = [...requiredSources, ...conditionalSources];
|
|
169
|
+
const overlap = requiredSources.find((source) => conditionalSources.includes(source));
|
|
170
|
+
if (overlap) fail("argument-conflict", `source cannot be both required and conditional: ${overlap}`);
|
|
171
|
+
for (const sourceId of roleSources) {
|
|
172
|
+
if (!sourceIds.includes(sourceId)) fail("source-reference-missing", `consumption source must be included in --sources: ${sourceId}`);
|
|
173
|
+
if (!LOCAL_SOURCE_KINDS.has(sourceMap.get(sourceId)?.kind)) fail("argument-conflict", `consumption source must be local: ${sourceId}`);
|
|
174
|
+
}
|
|
175
|
+
const consumption = roleSources.length > 0 ? {
|
|
176
|
+
requiredSources: [...requiredSources].sort((left, right) => left.localeCompare(right)),
|
|
177
|
+
conditionalSources: [...conditionalSources].sort((left, right) => left.localeCompare(right)),
|
|
178
|
+
} : undefined;
|
|
160
179
|
const item = {
|
|
161
180
|
id: input.id,
|
|
162
181
|
kind: input.kind,
|
|
@@ -167,9 +186,10 @@ export function buildItemProposal(project, input) {
|
|
|
167
186
|
status: "proposed",
|
|
168
187
|
sources: sourceIds,
|
|
169
188
|
overrides: input.overrides ?? [],
|
|
189
|
+
...(consumption ? { consumption } : {}),
|
|
170
190
|
...(verification ? { verification } : {}),
|
|
171
191
|
};
|
|
172
|
-
validateItem(item, "proposal item", { proposal: true });
|
|
192
|
+
validateItem(item, "proposal item", { proposal: true, contractSchemaVersion: 3 });
|
|
173
193
|
|
|
174
194
|
if (item.overrides.length > 0) {
|
|
175
195
|
const hypothetical = { ...structuredClone(item), status: "approved", approval: { by: "proposal-preflight", at: "1970-01-01T00:00:00.000Z" } };
|
|
@@ -13,6 +13,7 @@ import { inspectProjectInitialization, loadProject } from "./project-store.mjs";
|
|
|
13
13
|
import { RENDERER_VERSION } from "./renderer.mjs";
|
|
14
14
|
import { AI_ENTRY_RENDERER_VERSION } from "./ai-entry.mjs";
|
|
15
15
|
import { PROJECT_STATUS_SCHEMA_VERSION } from "./project-status.mjs";
|
|
16
|
+
import { CONTEXT_BUNDLE_SCHEMA_VERSION } from "./context-bundle.mjs";
|
|
16
17
|
import { EVIDENCE_BUNDLE_SCHEMA_VERSION, EVIDENCE_INPUT_SCHEMA_VERSION } from "./evidence-schema.mjs";
|
|
17
18
|
import {
|
|
18
19
|
CONTEXT_BUDGET_UNIT,
|
|
@@ -49,7 +50,9 @@ function schemas(projectionLockWritten = 1) {
|
|
|
49
50
|
adaptiveContextBundle: ADAPTIVE_CONTEXT_BUNDLE_SCHEMA_VERSION,
|
|
50
51
|
assistBundle: ASSIST_BUNDLE_SCHEMA_VERSION,
|
|
51
52
|
capabilities: CAPABILITIES_SCHEMA_VERSION,
|
|
52
|
-
contract:
|
|
53
|
+
contract: 3,
|
|
54
|
+
contractReadable: [1, 2, 3],
|
|
55
|
+
contextBundle: CONTEXT_BUNDLE_SCHEMA_VERSION,
|
|
53
56
|
contextQuery: CONTEXT_QUERY_SCHEMA_VERSION,
|
|
54
57
|
coverageAudit: COVERAGE_AUDIT_SCHEMA_VERSION,
|
|
55
58
|
dashboardViewModel: DASHBOARD_SCHEMA_VERSION,
|
|
@@ -26,7 +26,7 @@ export async function checkProject(root, project, options = {}) {
|
|
|
26
26
|
continue;
|
|
27
27
|
}
|
|
28
28
|
try {
|
|
29
|
-
const actual = await readSourceDigest(root, source, sourceReadContext);
|
|
29
|
+
const actual = await readSourceDigest(root, source, sourceReadContext, { projectionsLock: project.projectionsLock });
|
|
30
30
|
if (actual !== locked) findings.push({ code: "source-changed", source: source.id, path: source.path, expected: locked, actual });
|
|
31
31
|
} catch (error) {
|
|
32
32
|
const code = error.code === "source-missing"
|
|
@@ -5,6 +5,7 @@ import { buildItemProposal, registerSource } from "./authoring.mjs";
|
|
|
5
5
|
import { blockingContextFindings, checkExitCode, checkProject } from "./checker.mjs";
|
|
6
6
|
import { digestJson, prettyCanonicalJson } from "./canonical-json.mjs";
|
|
7
7
|
import { validateProposal } from "./contract-schema.mjs";
|
|
8
|
+
import { buildContextBundle } from "./context-bundle.mjs";
|
|
8
9
|
import { buildDashboardModel } from "./dashboard-model.mjs";
|
|
9
10
|
import { renderDashboardHtml } from "./dashboard-renderer.mjs";
|
|
10
11
|
import { discoverProject } from "./discovery.mjs";
|
|
@@ -20,7 +21,6 @@ import { initializeProject, inspectProjectInitialization, loadProject } from "./
|
|
|
20
21
|
import { publishProjection } from "./projection-store.mjs";
|
|
21
22
|
import { publishAiEntry, removeAiEntry } from "./ai-entry.mjs";
|
|
22
23
|
import { buildProjectStatus } from "./project-status.mjs";
|
|
23
|
-
import { renderContextBundle } from "./renderer.mjs";
|
|
24
24
|
import { buildIntegrationReviewBundleFiles, buildStageContextBundleFiles } from "./task-context.mjs";
|
|
25
25
|
import { applyMigrationPlanFile, buildMigrationPlanFile, buildUpgradeAssessment } from "./upgrade.mjs";
|
|
26
26
|
import { buildTruthReconciliationReviewFiles } from "./truth-reconciliation.mjs";
|
|
@@ -38,16 +38,16 @@ Usage:
|
|
|
38
38
|
project-context upgrade-plan --project PATH --assessment FILE [--json]
|
|
39
39
|
project-context upgrade-apply --project PATH --plan FILE [--write] [--json]
|
|
40
40
|
project-context setup --project PATH --id ID --name NAME [--output FILE] [--write] [--json]
|
|
41
|
-
project-context register --project PATH --id SOURCE_ID --kind KIND [--path PATH] [--pointer POINTER] [--reference TEXT] [--write] [--json]
|
|
42
|
-
project-context propose --project PATH --id ITEM_ID --kind KIND --subject SUBJECT (--value TEXT | --value-json JSON) --statement TEXT --sources SOURCE_ID... --scope SCOPE [--scope-path PATH] [--overrides ITEM_ID...] [--verification KIND] [--verification-source SOURCE_ID] [--verification-expected-json JSON] [--output FILE --write] [--json]
|
|
41
|
+
project-context register --project PATH --id SOURCE_ID --kind KIND [--path PATH] [--pointer POINTER] [--reference TEXT] [--digest-mode full-file|outside-owned-ai-entry] [--write] [--json]
|
|
42
|
+
project-context propose --project PATH --id ITEM_ID --kind KIND --subject SUBJECT (--value TEXT | --value-json JSON) --statement TEXT --sources SOURCE_ID... --scope SCOPE [--scope-path PATH] [--overrides ITEM_ID...] [--required-source SOURCE_ID...] [--conditional-source SOURCE_ID...] [--verification KIND] [--verification-source SOURCE_ID] [--verification-expected-json JSON] [--output FILE --write] [--json]
|
|
43
43
|
project-context review-source --project PATH --id SOURCE_ID [--json]
|
|
44
44
|
project-context accept-source-change --project PATH --id SOURCE_ID --expected-digest SHA256 [--affected-items ITEM_ID...] [--write] [--json]
|
|
45
|
-
project-context revise --project PATH --id ITEM_ID [--expected-item-digest SHA256] --kind KIND --subject SUBJECT (--value TEXT | --value-json JSON) --statement TEXT --sources SOURCE_ID... --scope SCOPE [--scope-path PATH] [--overrides ITEM_ID...] [--verification KIND] [--verification-source SOURCE_ID] [--verification-expected-json JSON] [--write] [--json]
|
|
45
|
+
project-context revise --project PATH --id ITEM_ID [--expected-item-digest SHA256] --kind KIND --subject SUBJECT (--value TEXT | --value-json JSON) --statement TEXT --sources SOURCE_ID... --scope SCOPE [--scope-path PATH] [--overrides ITEM_ID...] [--required-source SOURCE_ID...] [--conditional-source SOURCE_ID...] [--verification KIND] [--verification-source SOURCE_ID] [--verification-expected-json JSON] [--write] [--json]
|
|
46
46
|
project-context deprecate --project PATH --id ITEM_ID [--expected-item-digest SHA256] --by NAME --rationale TEXT [--write] [--json]
|
|
47
47
|
project-context deprecate-source --project PATH --id SOURCE_ID [--expected-source-digest SHA256] --by NAME --rationale TEXT [--write] [--json]
|
|
48
48
|
project-context discover --project PATH [--output FILE --write] [--json]
|
|
49
49
|
project-context approve --project PATH (--proposal FILE | --pending) --ids ID... --by NAME [--rationale TEXT] [--write] [--json | --full-json]
|
|
50
|
-
project-context context --project PATH --path RELATIVE_PATH... [--task TEXT] [--locale zh-CN|en|all] [--json]
|
|
50
|
+
project-context context --project PATH (--locate --task TEXT | --path RELATIVE_PATH... [--task TEXT]) [--locale zh-CN|en|all] [--json]
|
|
51
51
|
project-context context-query --project PATH --input FILE [--previous FILE] [--json | --prompt]
|
|
52
52
|
project-context coverage-audit --project PATH [--changed-path RELATIVE_PATH...] [--json]
|
|
53
53
|
project-context index-context --project PATH [--write] [--json]
|
|
@@ -67,13 +67,13 @@ All commands are read-only unless their own --write flag is present.
|
|
|
67
67
|
const VALUE_FLAGS = new Set([
|
|
68
68
|
"project", "id", "name", "output", "proposal", "by", "task", "target", "rationale",
|
|
69
69
|
"kind", "pointer", "reference", "subject", "value", "value-json", "statement", "scope", "scope-path",
|
|
70
|
-
"verification", "verification-source", "verification-expected-json",
|
|
70
|
+
"verification", "verification-source", "verification-expected-json", "digest-mode",
|
|
71
71
|
"expected-digest", "expected-item-digest", "expected-source-digest", "locale", "plan", "stage", "from-version", "assessment", "previous", "previous-review",
|
|
72
72
|
]);
|
|
73
73
|
const LIST_FLAGS = new Set([
|
|
74
|
-
"ids", "path", "input", "changed-path", "sources", "overrides", "affected-items", "receipt", "receipt-bundle", "main-changed-path", "branch-changed-path",
|
|
74
|
+
"ids", "path", "input", "changed-path", "sources", "overrides", "required-source", "conditional-source", "affected-items", "receipt", "receipt-bundle", "main-changed-path", "branch-changed-path",
|
|
75
75
|
]);
|
|
76
|
-
const BOOLEAN_FLAGS = new Set(["write", "json", "full-json", "help", "pending", "prompt"]);
|
|
76
|
+
const BOOLEAN_FLAGS = new Set(["write", "json", "full-json", "help", "pending", "prompt", "locate"]);
|
|
77
77
|
const COMMAND_OPTIONS = new Map([
|
|
78
78
|
["init", new Set(["project", "id", "name", "write", "json", "help"])],
|
|
79
79
|
["capabilities", new Set(["project", "json", "help"])],
|
|
@@ -84,10 +84,10 @@ const COMMAND_OPTIONS = new Map([
|
|
|
84
84
|
["upgrade-plan", new Set(["project", "assessment", "json", "help"])],
|
|
85
85
|
["upgrade-apply", new Set(["project", "plan", "write", "json", "help"])],
|
|
86
86
|
["setup", new Set(["project", "id", "name", "output", "write", "json", "help"])],
|
|
87
|
-
["register", new Set(["project", "id", "kind", "path", "pointer", "reference", "write", "json", "help"])],
|
|
87
|
+
["register", new Set(["project", "id", "kind", "path", "pointer", "reference", "digest-mode", "write", "json", "help"])],
|
|
88
88
|
["propose", new Set([
|
|
89
89
|
"project", "id", "kind", "subject", "value", "value-json", "statement", "sources", "scope", "scope-path",
|
|
90
|
-
"overrides", "verification", "verification-source", "verification-expected-json", "output", "write", "json", "help",
|
|
90
|
+
"overrides", "required-source", "conditional-source", "verification", "verification-source", "verification-expected-json", "output", "write", "json", "help",
|
|
91
91
|
])],
|
|
92
92
|
["review-source", new Set(["project", "id", "json", "help"])],
|
|
93
93
|
["accept-source-change", new Set([
|
|
@@ -95,7 +95,7 @@ const COMMAND_OPTIONS = new Map([
|
|
|
95
95
|
])],
|
|
96
96
|
["revise", new Set([
|
|
97
97
|
"project", "id", "expected-item-digest", "kind", "subject", "value", "value-json", "statement", "sources",
|
|
98
|
-
"scope", "scope-path", "overrides", "verification", "verification-source", "verification-expected-json",
|
|
98
|
+
"scope", "scope-path", "overrides", "required-source", "conditional-source", "verification", "verification-source", "verification-expected-json",
|
|
99
99
|
"write", "json", "help",
|
|
100
100
|
])],
|
|
101
101
|
["deprecate", new Set([
|
|
@@ -106,7 +106,7 @@ const COMMAND_OPTIONS = new Map([
|
|
|
106
106
|
])],
|
|
107
107
|
["discover", new Set(["project", "output", "write", "json", "help"])],
|
|
108
108
|
["approve", new Set(["project", "proposal", "pending", "ids", "by", "rationale", "write", "json", "full-json", "help"])],
|
|
109
|
-
["context", new Set(["project", "path", "task", "locale", "json", "help"])],
|
|
109
|
+
["context", new Set(["project", "path", "locate", "task", "locale", "json", "help"])],
|
|
110
110
|
["context-query", new Set(["project", "input", "previous", "json", "prompt", "help"])],
|
|
111
111
|
["coverage-audit", new Set(["project", "changed-path", "json", "help"])],
|
|
112
112
|
["index-context", new Set(["project", "write", "json", "help"])],
|
|
@@ -233,6 +233,8 @@ function itemInput(options) {
|
|
|
233
233
|
scope: required(options, "scope"),
|
|
234
234
|
scopePath: options["scope-path"],
|
|
235
235
|
overrides: options.overrides ?? [],
|
|
236
|
+
requiredSources: options["required-source"] ?? [],
|
|
237
|
+
conditionalSources: options["conditional-source"] ?? [],
|
|
236
238
|
verification: options.verification,
|
|
237
239
|
verificationSource: options["verification-source"],
|
|
238
240
|
verificationExpectedPresent: hasExpected,
|
|
@@ -449,7 +451,7 @@ async function runCommand(command, options) {
|
|
|
449
451
|
if (command === "coverage-audit") {
|
|
450
452
|
const audit = await buildCoverageAuditFiles(root, options["changed-path"] ?? []);
|
|
451
453
|
const summary = `Registration coverage ${audit.registrationCoverage}: ${audit.categories["review-required"].length} review-required candidate(s).\n`;
|
|
452
|
-
return { exitCode:
|
|
454
|
+
return { exitCode: ["review-required", "unresolved"].includes(audit.registrationCoverage) ? 1 : 0, stdout: jsonOrText(options, audit, summary), stderr: "" };
|
|
453
455
|
}
|
|
454
456
|
if (command === "index-context") {
|
|
455
457
|
const index = await indexContextFiles(root, { write: options.write });
|
|
@@ -479,6 +481,7 @@ async function runCommand(command, options) {
|
|
|
479
481
|
path: optionalOneListValue(options, "path"),
|
|
480
482
|
pointer: options.pointer,
|
|
481
483
|
reference: options.reference,
|
|
484
|
+
digestMode: options["digest-mode"],
|
|
482
485
|
}, { write: options.write });
|
|
483
486
|
const summary = result.action === "unchanged"
|
|
484
487
|
? `Source unchanged: ${result.source.id}.\n`
|
|
@@ -637,9 +640,19 @@ async function runCommand(command, options) {
|
|
|
637
640
|
if (command === "context") {
|
|
638
641
|
const findings = blockingContextFindings(await checkProject(root, project));
|
|
639
642
|
if (findings.length > 0) fail("context-blocked", "context generation is blocked by contract or source findings", { exitCode: 1, details: { findings } });
|
|
640
|
-
const
|
|
641
|
-
const
|
|
642
|
-
|
|
643
|
+
const hasLocate = options.locate === true;
|
|
644
|
+
const hasPaths = options.path !== undefined;
|
|
645
|
+
if (hasLocate === hasPaths) fail("argument-conflict", "context requires exactly one of --locate or --path");
|
|
646
|
+
if (hasLocate && (typeof options.task !== "string" || options.task.trim().length === 0)) {
|
|
647
|
+
fail("argument-missing", "--locate requires a non-empty --task");
|
|
648
|
+
}
|
|
649
|
+
const bundle = await buildContextBundle(root, project, {
|
|
650
|
+
mode: hasLocate ? "locate" : "targeted",
|
|
651
|
+
paths: options.path ?? [],
|
|
652
|
+
task: options.task,
|
|
653
|
+
locale: contextLocale(options),
|
|
654
|
+
});
|
|
655
|
+
return { exitCode: 0, stdout: options.json ? prettyCanonicalJson(bundle) : bundle.content, stderr: "" };
|
|
643
656
|
}
|
|
644
657
|
if (command === "publish") {
|
|
645
658
|
const findings = blockingContextFindings(await checkProject(root, project));
|
|
@@ -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
|
+
}
|