frontend-project-context 1.3.1 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/CHANGELOG.md +51 -2
  2. package/README.md +156 -40
  3. package/UPGRADING.md +55 -1
  4. package/docs/04-PROGRAM-DESIGN.md +34 -4
  5. package/docs/05-ACCEPTANCE-CONTRACT.md +40 -3
  6. package/docs/08-INSTALLATION-AND-DISTRIBUTION.md +67 -22
  7. package/docs/14-FORMAL-RELEASE-READINESS.md +30 -1
  8. package/docs/18-BRANCH-AWARE-STAGED-CONTEXT-DESIGN.md +2 -2
  9. package/docs/19-POST-1.3.1-AI-TAKEOVER-EVIDENCE-AND-UPGRADE-PLAN.md +579 -0
  10. package/docs/20-PHASE-A-AI-TAKEOVER-AND-HEALTH-CLOSURE-DESIGN.md +535 -0
  11. package/docs/21-PHASE-B-EVIDENCE-FEEDBACK-PROTOCOL-DESIGN.md +347 -0
  12. package/docs/22-PHASE-C-TARGET-UPGRADE-PROTOCOL-DESIGN.md +398 -0
  13. package/docs/23-ADAPTIVE-BOUNDED-TASK-CONTEXT-DESIGN.md +432 -0
  14. package/docs/24-A130-REAL-HOST-TARGET-PROJECT-COMPARISON.md +210 -0
  15. package/docs/25-REAL-PROJECT-SOURCE-OF-TRUTH-MAINTENANCE-DESIGN.md +409 -0
  16. package/docs/26-A130-QUALITY-CLOSURE-AND-ADAPTIVE-DELIVERY-REPAIR-DESIGN.md +609 -0
  17. package/docs/README.md +38 -6
  18. package/docs/USER-AND-AI-OPERATION-MANUAL.md +840 -0
  19. package/examples/README.md +29 -2
  20. package/examples/package.json +6 -2
  21. package/migration-manifest.json +110 -0
  22. package/package.json +3 -2
  23. package/schemas/action-plan.schema.json +31 -3
  24. package/schemas/adaptive-context-bundle.schema.json +70 -0
  25. package/schemas/capabilities.schema.json +64 -18
  26. package/schemas/context-query.schema.json +69 -0
  27. package/schemas/coverage-audit.schema.json +32 -0
  28. package/schemas/evidence-bundle.schema.json +64 -0
  29. package/schemas/evidence-input.schema.json +82 -0
  30. package/schemas/host-promotion-evidence.schema.json +33 -0
  31. package/schemas/migration-manifest.schema.json +29 -0
  32. package/schemas/migration-plan.schema.json +32 -0
  33. package/schemas/project-status.schema.json +75 -0
  34. package/schemas/projection-lock.schema.json +48 -0
  35. package/schemas/review-bundle.schema.json +3 -3
  36. package/schemas/routing-index.schema.json +58 -0
  37. package/schemas/truth-reconciliation-input.schema.json +60 -0
  38. package/schemas/truth-reconciliation-review-bundle.schema.json +155 -0
  39. package/schemas/upgrade-assessment.schema.json +48 -0
  40. package/schemas/upgrade-result-bundle.schema.json +35 -0
  41. package/src/project-context/a130-evaluation.mjs +91 -0
  42. package/src/project-context/adaptive-context-schema.mjs +392 -0
  43. package/src/project-context/adaptive-context.mjs +547 -0
  44. package/src/project-context/ai-entry.mjs +320 -0
  45. package/src/project-context/assist.mjs +4 -2
  46. package/src/project-context/capabilities.mjs +62 -17
  47. package/src/project-context/checker.mjs +24 -6
  48. package/src/project-context/cli.mjs +113 -3
  49. package/src/project-context/contract-schema.mjs +30 -16
  50. package/src/project-context/dashboard-model.mjs +4 -4
  51. package/src/project-context/dashboard-renderer.mjs +3 -3
  52. package/src/project-context/discovery.mjs +13 -8
  53. package/src/project-context/evidence-schema.mjs +209 -0
  54. package/src/project-context/evidence.mjs +99 -0
  55. package/src/project-context/exchange-schema.mjs +23 -12
  56. package/src/project-context/exchange.mjs +26 -4
  57. package/src/project-context/maintenance.mjs +4 -4
  58. package/src/project-context/migration-manifest.mjs +168 -0
  59. package/src/project-context/project-status.mjs +157 -0
  60. package/src/project-context/projection-store.mjs +8 -1
  61. package/src/project-context/renderer.mjs +75 -1
  62. package/src/project-context/source-reader.mjs +63 -30
  63. package/src/project-context/task-context.mjs +14 -2
  64. package/src/project-context/truth-reconciliation-schema.mjs +488 -0
  65. package/src/project-context/truth-reconciliation.mjs +543 -0
  66. package/src/project-context/upgrade-schema.mjs +219 -0
  67. package/src/project-context/upgrade.mjs +494 -0
@@ -0,0 +1,488 @@
1
+ import { canonicalValue, digestJson, validateJsonValue } from "./canonical-json.mjs";
2
+ import { fail } from "./errors.mjs";
3
+ import { normalizeRelativePath } from "./path-policy.mjs";
4
+
5
+ export const HOST_PROMOTION_EVIDENCE_SCHEMA_VERSION = 1;
6
+ export const TRUTH_RECONCILIATION_INPUT_SCHEMA_VERSION = 2;
7
+ export const TRUTH_RECONCILIATION_REVIEW_BUNDLE_SCHEMA_VERSION = 2;
8
+
9
+ const ID = /^[a-z0-9]+(?:[.-][a-z0-9]+)*$/u;
10
+ const SHA256 = /^sha256:[a-f0-9]{64}$/u;
11
+ const ITEM_KINDS = new Set(["fact", "policy", "reference", "validation-description"]);
12
+ const STAGES = new Set(["qa", "preproduction", "production", "withdrawn", "rollback"]);
13
+ const TARGET_STAGES = new Set(["development", "qa", "preproduction", "production"]);
14
+ const LIFECYCLES = new Set(["active", "released", "withdrawn", "rolled-back"]);
15
+ const VERIFICATION_STATUSES = new Set(["passed", "failed", "skipped", "observed"]);
16
+ const CHANGE_CLASSES = new Set([
17
+ "duplicate", "compatible-compose", "scope-split", "supersede", "semantic-conflict", "implementation-only",
18
+ ]);
19
+ const SEVERITIES = new Set(["attention", "conflict", "blocked"]);
20
+ const RESOLUTION_KINDS = new Set([
21
+ "accept-branch-change", "compose-compatible-changes", "defer-scoped-conflict", "external-implementation-fix-required",
22
+ "fix-source-and-rerun", "keep-current-baseline", "refresh-promotion-evidence", "replace-or-deprecate-source", "split-scope-or-override",
23
+ ]);
24
+
25
+ function invalid(message, details) {
26
+ fail("truth-reconciliation-input-invalid", message, { details });
27
+ }
28
+
29
+ function object(value, label) {
30
+ if (!value || typeof value !== "object" || Array.isArray(value)) invalid(`${label} must be an object`);
31
+ }
32
+
33
+ function exactKeys(value, keys, label) {
34
+ for (const key of Object.keys(value)) if (!keys.has(key)) invalid(`${label} contains unknown field: ${key}`);
35
+ }
36
+
37
+ function requiredKeys(value, keys, label) {
38
+ for (const key of keys) if (!Object.hasOwn(value, key)) invalid(`${label}.${key} is required`);
39
+ }
40
+
41
+ function string(value, label) {
42
+ if (typeof value !== "string" || value.length === 0) invalid(`${label} must be a non-empty string`);
43
+ }
44
+
45
+ function id(value, label) {
46
+ string(value, label);
47
+ if (!ID.test(value)) invalid(`${label} must use stable lowercase dot/kebab naming`);
48
+ return value;
49
+ }
50
+
51
+ function digest(value, label) {
52
+ string(value, label);
53
+ if (!SHA256.test(value)) invalid(`${label} must be sha256`);
54
+ return value;
55
+ }
56
+
57
+ function enumeration(value, allowed, label) {
58
+ if (!allowed.has(value)) invalid(`${label} is invalid`);
59
+ return value;
60
+ }
61
+
62
+ function array(value, label, max = 256) {
63
+ if (!Array.isArray(value)) invalid(`${label} must be an array`);
64
+ if (value.length > max) invalid(`${label} exceeds ${max} entries`);
65
+ }
66
+
67
+ function uniqueSorted(values, label, normalize, identity = (value) => JSON.stringify(value)) {
68
+ array(values, label);
69
+ const entries = values.map((value, index) => normalize(value, `${label}[${index}]`));
70
+ const byIdentity = new Map();
71
+ for (const entry of entries) {
72
+ const key = identity(entry);
73
+ if (byIdentity.has(key)) invalid(`${label} contains duplicate entry: ${key}`);
74
+ byIdentity.set(key, entry);
75
+ }
76
+ return [...byIdentity.values()].sort((left, right) => identity(left).localeCompare(identity(right)));
77
+ }
78
+
79
+ function ids(value, label) {
80
+ return uniqueSorted(value, label, id, (entry) => entry);
81
+ }
82
+
83
+ function paths(value, label) {
84
+ return uniqueSorted(value, label, (entry, entryLabel) => {
85
+ try {
86
+ return normalizeRelativePath(entry, { allowRoot: true, label: entryLabel });
87
+ } catch (error) {
88
+ invalid(`${entryLabel} must stay inside the project`, { path: entry, reason: error.code ?? "invalid-path" });
89
+ }
90
+ }, (entry) => entry);
91
+ }
92
+
93
+ export function normalizeTruthScope(value, label) {
94
+ object(value, label);
95
+ exactKeys(value, new Set(["kind", "path"]), label);
96
+ requiredKeys(value, ["kind"], label);
97
+ if (!new Set(["project", "path-prefix", "file"]).has(value.kind)) invalid(`${label}.kind is invalid`);
98
+ if (value.kind === "project") {
99
+ if (value.path !== undefined && value.path !== ".") invalid(`${label}.path must be omitted or '.' for project scope`);
100
+ return { kind: "project" };
101
+ }
102
+ try {
103
+ return { kind: value.kind, path: normalizeRelativePath(value.path, { label: `${label}.path` }) };
104
+ } catch (error) {
105
+ invalid(`${label}.path must stay inside the project`, { path: value.path, reason: error.code ?? "invalid-path" });
106
+ }
107
+ }
108
+
109
+ export function normalizeTruthSnapshots(value, label) {
110
+ object(value, label);
111
+ const keys = new Set(["contract", "sourcesLock", "projectionsLock"]);
112
+ exactKeys(value, keys, label);
113
+ requiredKeys(value, keys, label);
114
+ return {
115
+ contract: digest(value.contract, `${label}.contract`),
116
+ sourcesLock: digest(value.sourcesLock, `${label}.sourcesLock`),
117
+ projectionsLock: digest(value.projectionsLock, `${label}.projectionsLock`),
118
+ };
119
+ }
120
+
121
+ function normalizeVerificationResult(value, label) {
122
+ object(value, label);
123
+ const keys = new Set(["id", "status"]);
124
+ exactKeys(value, keys, label);
125
+ requiredKeys(value, keys, label);
126
+ return { id: id(value.id, `${label}.id`), status: enumeration(value.status, VERIFICATION_STATUSES, `${label}.status`) };
127
+ }
128
+
129
+ export function normalizeHostPromotionEvidence(value, label = "host promotion evidence") {
130
+ object(value, label);
131
+ const keys = new Set([
132
+ "schemaVersion", "kind", "taskId", "stage", "baseRevision", "candidateRevision", "codeSnapshotDigest",
133
+ "sourceSnapshotDigest", "artifactDigest", "changedPaths", "projectSnapshots", "acceptanceSuiteDigest",
134
+ "verificationResults", "integration", "equivalentToCodeSnapshotDigest",
135
+ ]);
136
+ exactKeys(value, keys, label);
137
+ requiredKeys(value, [
138
+ "schemaVersion", "kind", "taskId", "stage", "baseRevision", "candidateRevision", "codeSnapshotDigest",
139
+ "sourceSnapshotDigest", "artifactDigest", "changedPaths", "projectSnapshots", "acceptanceSuiteDigest", "verificationResults",
140
+ ], label);
141
+ if (value.schemaVersion !== HOST_PROMOTION_EVIDENCE_SCHEMA_VERSION) invalid(`${label}.schemaVersion must be 1`);
142
+ if (value.kind !== "host-promotion-evidence") invalid(`${label}.kind must be host-promotion-evidence`);
143
+ if (value.integration !== undefined && typeof value.integration !== "boolean") invalid(`${label}.integration must be boolean`);
144
+ return {
145
+ schemaVersion: HOST_PROMOTION_EVIDENCE_SCHEMA_VERSION,
146
+ kind: "host-promotion-evidence",
147
+ taskId: id(value.taskId, `${label}.taskId`),
148
+ stage: enumeration(value.stage, STAGES, `${label}.stage`),
149
+ baseRevision: (string(value.baseRevision, `${label}.baseRevision`), value.baseRevision),
150
+ candidateRevision: (string(value.candidateRevision, `${label}.candidateRevision`), value.candidateRevision),
151
+ codeSnapshotDigest: digest(value.codeSnapshotDigest, `${label}.codeSnapshotDigest`),
152
+ sourceSnapshotDigest: digest(value.sourceSnapshotDigest, `${label}.sourceSnapshotDigest`),
153
+ artifactDigest: digest(value.artifactDigest, `${label}.artifactDigest`),
154
+ changedPaths: paths(value.changedPaths, `${label}.changedPaths`),
155
+ projectSnapshots: normalizeTruthSnapshots(value.projectSnapshots, `${label}.projectSnapshots`),
156
+ acceptanceSuiteDigest: digest(value.acceptanceSuiteDigest, `${label}.acceptanceSuiteDigest`),
157
+ verificationResults: uniqueSorted(value.verificationResults, `${label}.verificationResults`, normalizeVerificationResult, (entry) => entry.id),
158
+ integration: value.integration ?? false,
159
+ ...(value.equivalentToCodeSnapshotDigest !== undefined
160
+ ? { equivalentToCodeSnapshotDigest: digest(value.equivalentToCodeSnapshotDigest, `${label}.equivalentToCodeSnapshotDigest`) }
161
+ : {}),
162
+ };
163
+ }
164
+
165
+ function normalizeCurrentSource(value, label) {
166
+ object(value, label);
167
+ exactKeys(value, new Set(["id", "digest"]), label);
168
+ requiredKeys(value, ["id", "digest"], label);
169
+ return { id: id(value.id, `${label}.id`), digest: digest(value.digest, `${label}.digest`) };
170
+ }
171
+
172
+ function normalizeCurrentItem(value, label) {
173
+ object(value, label);
174
+ exactKeys(value, new Set(["id", "digest", "subject", "kind", "scope"]), label);
175
+ requiredKeys(value, ["id", "digest", "subject", "kind", "scope"], label);
176
+ return {
177
+ id: id(value.id, `${label}.id`), digest: digest(value.digest, `${label}.digest`),
178
+ subject: id(value.subject, `${label}.subject`), kind: enumeration(value.kind, ITEM_KINDS, `${label}.kind`),
179
+ scope: normalizeTruthScope(value.scope, `${label}.scope`),
180
+ };
181
+ }
182
+
183
+ function normalizeSourceChange(value, label) {
184
+ object(value, label);
185
+ const keys = new Set(["sourceId", "expectedDigest", "candidateDigest", "affectedItemIds", "subjects", "scopes", "classification"]);
186
+ exactKeys(value, keys, label);
187
+ requiredKeys(value, keys, label);
188
+ return {
189
+ sourceId: id(value.sourceId, `${label}.sourceId`),
190
+ expectedDigest: digest(value.expectedDigest, `${label}.expectedDigest`),
191
+ candidateDigest: digest(value.candidateDigest, `${label}.candidateDigest`),
192
+ affectedItemIds: ids(value.affectedItemIds, `${label}.affectedItemIds`),
193
+ subjects: ids(value.subjects, `${label}.subjects`),
194
+ scopes: uniqueSorted(value.scopes, `${label}.scopes`, normalizeTruthScope),
195
+ classification: enumeration(value.classification, CHANGE_CLASSES, `${label}.classification`),
196
+ };
197
+ }
198
+
199
+ function normalizeItemChange(value, label) {
200
+ object(value, label);
201
+ const keys = new Set(["itemId", "expectedDigest", "candidateDigest", "sourceIds", "subject", "kind", "scope", "classification"]);
202
+ exactKeys(value, keys, label);
203
+ requiredKeys(value, keys, label);
204
+ return {
205
+ itemId: id(value.itemId, `${label}.itemId`),
206
+ expectedDigest: digest(value.expectedDigest, `${label}.expectedDigest`),
207
+ candidateDigest: digest(value.candidateDigest, `${label}.candidateDigest`),
208
+ sourceIds: ids(value.sourceIds, `${label}.sourceIds`),
209
+ subject: id(value.subject, `${label}.subject`),
210
+ kind: enumeration(value.kind, ITEM_KINDS, `${label}.kind`),
211
+ scope: normalizeTruthScope(value.scope, `${label}.scope`),
212
+ classification: enumeration(value.classification, CHANGE_CLASSES, `${label}.classification`),
213
+ };
214
+ }
215
+
216
+ function normalizeCandidate(value, label) {
217
+ object(value, label);
218
+ const keys = new Set([
219
+ "id", "taskId", "lifecycle", "targetStage", "baseRevision", "expectedProjectSnapshots", "currentCodeSnapshotDigest",
220
+ "currentSourceSnapshotDigest", "currentArtifactDigest", "currentAcceptanceSuiteDigest", "changedPaths", "sourceChanges", "itemChanges",
221
+ "decisionCandidateIds", "promotionEvidence",
222
+ ]);
223
+ exactKeys(value, keys, label);
224
+ requiredKeys(value, keys, label);
225
+ return {
226
+ id: id(value.id, `${label}.id`), taskId: id(value.taskId, `${label}.taskId`),
227
+ lifecycle: enumeration(value.lifecycle, LIFECYCLES, `${label}.lifecycle`),
228
+ targetStage: enumeration(value.targetStage, TARGET_STAGES, `${label}.targetStage`),
229
+ baseRevision: (string(value.baseRevision, `${label}.baseRevision`), value.baseRevision),
230
+ expectedProjectSnapshots: normalizeTruthSnapshots(value.expectedProjectSnapshots, `${label}.expectedProjectSnapshots`),
231
+ currentCodeSnapshotDigest: digest(value.currentCodeSnapshotDigest, `${label}.currentCodeSnapshotDigest`),
232
+ currentSourceSnapshotDigest: digest(value.currentSourceSnapshotDigest, `${label}.currentSourceSnapshotDigest`),
233
+ currentArtifactDigest: digest(value.currentArtifactDigest, `${label}.currentArtifactDigest`),
234
+ currentAcceptanceSuiteDigest: digest(value.currentAcceptanceSuiteDigest, `${label}.currentAcceptanceSuiteDigest`),
235
+ changedPaths: paths(value.changedPaths, `${label}.changedPaths`),
236
+ sourceChanges: uniqueSorted(value.sourceChanges, `${label}.sourceChanges`, normalizeSourceChange, (entry) => entry.sourceId),
237
+ itemChanges: uniqueSorted(value.itemChanges, `${label}.itemChanges`, normalizeItemChange, (entry) => entry.itemId),
238
+ decisionCandidateIds: ids(value.decisionCandidateIds, `${label}.decisionCandidateIds`),
239
+ promotionEvidence: uniqueSorted(value.promotionEvidence, `${label}.promotionEvidence`, normalizeHostPromotionEvidence, (entry) => `${entry.stage}:${entry.candidateRevision}:${entry.artifactDigest}`),
240
+ };
241
+ }
242
+
243
+ function normalizeGlobalFinding(value, label) {
244
+ object(value, label);
245
+ const keys = new Set(["code", "severity", "sourceIds", "itemIds", "subjects", "scopes", "itemKinds"]);
246
+ exactKeys(value, keys, label);
247
+ requiredKeys(value, keys, label);
248
+ return {
249
+ code: id(value.code, `${label}.code`), severity: enumeration(value.severity, SEVERITIES, `${label}.severity`),
250
+ sourceIds: ids(value.sourceIds, `${label}.sourceIds`), itemIds: ids(value.itemIds, `${label}.itemIds`),
251
+ subjects: ids(value.subjects, `${label}.subjects`),
252
+ scopes: uniqueSorted(value.scopes, `${label}.scopes`, normalizeTruthScope),
253
+ itemKinds: uniqueSorted(value.itemKinds, `${label}.itemKinds`, (entry, entryLabel) => enumeration(entry, ITEM_KINDS, entryLabel), (entry) => entry),
254
+ };
255
+ }
256
+
257
+ function normalizeResolutionDecision(value, label) {
258
+ object(value, label);
259
+ const keys = new Set(["findingDigest", "resolutionKind", "humanInputs", "externalAction"]);
260
+ exactKeys(value, keys, label);
261
+ requiredKeys(value, ["findingDigest", "resolutionKind", "humanInputs"], label);
262
+ if (value.humanInputs === null || typeof value.humanInputs !== "object" || Array.isArray(value.humanInputs)) invalid(`${label}.humanInputs must be an object`);
263
+ for (const [key, entry] of Object.entries(value.humanInputs)) {
264
+ string(key, `${label}.humanInputs key`);
265
+ try { validateJsonValue(entry); } catch (error) { invalid(`${label}.humanInputs.${key} must be JSON-compatible`); }
266
+ }
267
+ if (value.externalAction !== undefined) {
268
+ object(value.externalAction, `${label}.externalAction`);
269
+ exactKeys(value.externalAction, new Set(["kind", "completed", "evidenceDigests"]), `${label}.externalAction`);
270
+ requiredKeys(value.externalAction, ["kind", "completed", "evidenceDigests"], `${label}.externalAction`);
271
+ string(value.externalAction.kind, `${label}.externalAction.kind`);
272
+ if (typeof value.externalAction.completed !== "boolean") invalid(`${label}.externalAction.completed must be boolean`);
273
+ }
274
+ return canonicalValue({
275
+ findingDigest: digest(value.findingDigest, `${label}.findingDigest`),
276
+ resolutionKind: enumeration(value.resolutionKind, RESOLUTION_KINDS, `${label}.resolutionKind`),
277
+ humanInputs: value.humanInputs,
278
+ ...(value.externalAction !== undefined ? { externalAction: { kind: value.externalAction.kind, completed: value.externalAction.completed, evidenceDigests: uniqueSorted(value.externalAction.evidenceDigests, `${label}.externalAction.evidenceDigests`, digest, (entry) => entry) } } : {}),
279
+ });
280
+ }
281
+
282
+ function normalizeResolutionAttempt(value, label) {
283
+ if (value === null) return null;
284
+ object(value, label);
285
+ const keys = new Set(["previousReviewDigest", "decisions", "projectContextStatus"]);
286
+ exactKeys(value, keys, label);
287
+ requiredKeys(value, keys, label);
288
+ if (!new Set(["clean", "attention", "conflict", "invalid", "partial"]).has(value.projectContextStatus)) invalid(`${label}.projectContextStatus is invalid`);
289
+ return {
290
+ previousReviewDigest: digest(value.previousReviewDigest, `${label}.previousReviewDigest`),
291
+ decisions: uniqueSorted(value.decisions, `${label}.decisions`, normalizeResolutionDecision, (entry) => entry.findingDigest),
292
+ projectContextStatus: value.projectContextStatus,
293
+ };
294
+ }
295
+
296
+ export function normalizeTruthReconciliationInput(input) {
297
+ try { validateJsonValue(input); } catch (error) { invalid("truth reconciliation input must be JSON-compatible", { reason: error.message }); }
298
+ object(input, "truth reconciliation input");
299
+ const keys = new Set(["schemaVersion", "kind", "projectId", "current", "task", "candidates", "globalFindings", "resolutionAttempt"]);
300
+ exactKeys(input, keys, "truth reconciliation input");
301
+ requiredKeys(input, keys, "truth reconciliation input");
302
+ if (input.schemaVersion !== TRUTH_RECONCILIATION_INPUT_SCHEMA_VERSION) invalid("truth reconciliation input.schemaVersion must be 2");
303
+ if (input.kind !== "truth-reconciliation-input") invalid("truth reconciliation input.kind must be truth-reconciliation-input");
304
+ object(input.current, "current");
305
+ exactKeys(input.current, new Set(["revision", "projectSnapshots", "sources", "items"]), "current");
306
+ requiredKeys(input.current, ["revision", "projectSnapshots", "sources", "items"], "current");
307
+ object(input.task, "task");
308
+ exactKeys(input.task, new Set(["id", "paths"]), "task");
309
+ requiredKeys(input.task, ["id", "paths"], "task");
310
+ const normalized = {
311
+ schemaVersion: 2, kind: "truth-reconciliation-input", projectId: id(input.projectId, "projectId"),
312
+ current: {
313
+ revision: (string(input.current.revision, "current.revision"), input.current.revision),
314
+ projectSnapshots: normalizeTruthSnapshots(input.current.projectSnapshots, "current.projectSnapshots"),
315
+ sources: uniqueSorted(input.current.sources, "current.sources", normalizeCurrentSource, (entry) => entry.id),
316
+ items: uniqueSorted(input.current.items, "current.items", normalizeCurrentItem, (entry) => entry.id),
317
+ },
318
+ task: { id: id(input.task.id, "task.id"), paths: paths(input.task.paths, "task.paths") },
319
+ candidates: uniqueSorted(input.candidates, "candidates", normalizeCandidate, (entry) => entry.id),
320
+ globalFindings: uniqueSorted(input.globalFindings, "globalFindings", normalizeGlobalFinding, (entry) => `${entry.code}:${JSON.stringify(entry.scopes)}`),
321
+ resolutionAttempt: normalizeResolutionAttempt(input.resolutionAttempt, "resolutionAttempt"),
322
+ };
323
+ return canonicalValue(normalized);
324
+ }
325
+
326
+ export function finalizeTruthReviewBundle(bundleWithoutDigest) {
327
+ const canonical = canonicalValue(bundleWithoutDigest);
328
+ return canonicalValue({ ...canonical, bundleDigest: digestJson(canonical) });
329
+ }
330
+
331
+ function reviewBoolean(value, expected, label) {
332
+ if (typeof value !== "boolean" || (expected !== undefined && value !== expected)) invalid(`${label} is invalid`);
333
+ }
334
+ function reviewStrings(value, label, { idsOnly = false, nonEmpty = false } = {}) {
335
+ array(value, label);
336
+ if (nonEmpty && value.length === 0) invalid(`${label} must not be empty`);
337
+ const seen = new Set();
338
+ value.forEach((entry, index) => {
339
+ if (idsOnly) id(entry, `${label}[${index}]`); else string(entry, `${label}[${index}]`);
340
+ if (seen.has(entry)) invalid(`${label} contains duplicate entry: ${entry}`);
341
+ seen.add(entry);
342
+ });
343
+ }
344
+ function reviewBaseline(value, label) {
345
+ object(value, label);
346
+ const keys = new Set(["actualDigest", "actualLabel", "expectedDigest", "expectedLabel"]);
347
+ exactKeys(value, keys, label); requiredKeys(value, keys, label);
348
+ for (const key of ["actualDigest", "expectedDigest"]) if (value[key] !== null) digest(value[key], `${label}.${key}`);
349
+ for (const key of ["actualLabel", "expectedLabel"]) if (value[key] !== null) string(value[key], `${label}.${key}`);
350
+ }
351
+ function reviewInvocation(value, label) {
352
+ object(value, label); exactKeys(value, new Set(["args", "command"]), label); requiredKeys(value, ["args", "command"], label);
353
+ id(value.command, `${label}.command`);
354
+ array(value.args, `${label}.args`);
355
+ value.args.forEach((entry, index) => { if (typeof entry !== "string") invalid(`${label}.args[${index}] must be a string`); });
356
+ }
357
+ function reviewPreview(value, label) {
358
+ object(value, label);
359
+ exactKeys(value, new Set(["externalAction", "invocation", "requiredHumanInputs", "resolutionKind"]), label);
360
+ requiredKeys(value, ["resolutionKind"], label);
361
+ enumeration(value.resolutionKind, RESOLUTION_KINDS, `${label}.resolutionKind`);
362
+ if (value.invocation !== undefined) reviewInvocation(value.invocation, `${label}.invocation`);
363
+ if (value.requiredHumanInputs !== undefined) reviewStrings(value.requiredHumanInputs, `${label}.requiredHumanInputs`);
364
+ if (value.externalAction !== undefined) {
365
+ object(value.externalAction, `${label}.externalAction`);
366
+ exactKeys(value.externalAction, new Set(["affectedItemIds", "affectedPaths", "affectedSourceIds", "kind"]), `${label}.externalAction`);
367
+ requiredKeys(value.externalAction, ["kind"], `${label}.externalAction`);
368
+ string(value.externalAction.kind, `${label}.externalAction.kind`);
369
+ if (value.externalAction.affectedSourceIds !== undefined) reviewStrings(value.externalAction.affectedSourceIds, `${label}.externalAction.affectedSourceIds`, { idsOnly: true });
370
+ if (value.externalAction.affectedItemIds !== undefined) reviewStrings(value.externalAction.affectedItemIds, `${label}.externalAction.affectedItemIds`, { idsOnly: true });
371
+ if (value.externalAction.affectedPaths !== undefined) paths(value.externalAction.affectedPaths, `${label}.externalAction.affectedPaths`);
372
+ }
373
+ }
374
+ function reviewResolutionContract(value, label) {
375
+ object(value, label);
376
+ const keys = new Set(["completionCriteria", "nextActions", "recomputeFromCurrentBaseline", "requiredHumanInputs", "requiredPostResolutionEvidence", "resolutionKinds", "resolutionState"]);
377
+ exactKeys(value, keys, label); requiredKeys(value, keys, label);
378
+ enumeration(value.resolutionState, new Set(["needs-human-decision", "contract-action-required", "external-fix-required", "revalidation-required", "resolved", "deferred-open"]), `${label}.resolutionState`);
379
+ reviewStrings(value.resolutionKinds, `${label}.resolutionKinds`, { nonEmpty: true });
380
+ value.resolutionKinds.forEach((entry) => enumeration(entry, RESOLUTION_KINDS, `${label}.resolutionKinds entry`));
381
+ reviewStrings(value.requiredHumanInputs, `${label}.requiredHumanInputs`);
382
+ array(value.nextActions, `${label}.nextActions`); if (value.nextActions.length === 0) invalid(`${label}.nextActions must not be empty`);
383
+ value.nextActions.forEach((entry, index) => reviewPreview(entry, `${label}.nextActions[${index}]`));
384
+ reviewStrings(value.requiredPostResolutionEvidence, `${label}.requiredPostResolutionEvidence`, { nonEmpty: true });
385
+ reviewStrings(value.completionCriteria, `${label}.completionCriteria`, { nonEmpty: true });
386
+ reviewBoolean(value.recomputeFromCurrentBaseline, true, `${label}.recomputeFromCurrentBaseline`);
387
+ }
388
+
389
+ export function validateTruthReviewBundle(input) {
390
+ try { validateJsonValue(input); } catch (error) { invalid("truth review bundle must be JSON-compatible", { reason: error.message }); }
391
+ object(input, "truth review bundle");
392
+ const keys = new Set(["schemaVersion", "kind", "project", "current", "task", "inputDigest", "summary", "candidateOutcomes", "collisionGroups", "findings", "resolutionOutcomes", "guarantees", "boundaries", "bundleDigest"]);
393
+ exactKeys(input, keys, "truth review bundle");
394
+ requiredKeys(input, keys, "truth review bundle");
395
+ if (input.schemaVersion !== 2 || input.kind !== "truth-reconciliation-review-bundle") invalid("truth review bundle identity is invalid");
396
+ digest(input.bundleDigest, "truth review bundle.bundleDigest");
397
+ const copy = structuredClone(input);
398
+ delete copy.bundleDigest;
399
+ if (digestJson(canonicalValue(copy)) !== input.bundleDigest) invalid("truth review bundle.bundleDigest does not match canonical content");
400
+ object(input.project, "truth review bundle.project");
401
+ exactKeys(input.project, new Set(["id", "name"]), "truth review bundle.project"); requiredKeys(input.project, ["id", "name"], "truth review bundle.project");
402
+ id(input.project.id, "truth review bundle.project.id"); string(input.project.name, "truth review bundle.project.name");
403
+ object(input.current, "truth review bundle.current");
404
+ exactKeys(input.current, new Set(["projectSnapshots", "revision"]), "truth review bundle.current"); requiredKeys(input.current, ["projectSnapshots", "revision"], "truth review bundle.current");
405
+ string(input.current.revision, "truth review bundle.current.revision"); normalizeTruthSnapshots(input.current.projectSnapshots, "truth review bundle.current.projectSnapshots");
406
+ object(input.task, "truth review bundle.task");
407
+ exactKeys(input.task, new Set(["id", "paths"]), "truth review bundle.task"); requiredKeys(input.task, ["id", "paths"], "truth review bundle.task");
408
+ id(input.task.id, "truth review bundle.task.id"); paths(input.task.paths, "truth review bundle.task.paths");
409
+ digest(input.inputDigest, "truth review bundle.inputDigest");
410
+ object(input.summary, "truth review bundle.summary");
411
+ const summaryKeys = new Set(["candidates", "collisionGroups", "eligibleCandidates", "existingDelivery", "findings", "globalHealth", "taskHealth"]);
412
+ exactKeys(input.summary, summaryKeys, "truth review bundle.summary"); requiredKeys(input.summary, summaryKeys, "truth review bundle.summary");
413
+ for (const key of ["candidates", "collisionGroups", "eligibleCandidates", "findings"]) if (!Number.isSafeInteger(input.summary[key]) || input.summary[key] < 0) invalid(`truth review bundle.summary.${key} is invalid`);
414
+ enumeration(input.summary.taskHealth, new Set(["ready", "blocked"]), "truth review bundle.summary.taskHealth");
415
+ enumeration(input.summary.globalHealth, new Set(["clean", "attention"]), "truth review bundle.summary.globalHealth");
416
+ if (input.summary.existingDelivery !== "not-blocked") invalid("truth review bundle.summary.existingDelivery is invalid");
417
+ array(input.candidateOutcomes, "truth review bundle.candidateOutcomes");
418
+ for (const [index, outcome] of input.candidateOutcomes.entries()) {
419
+ const label = `truth review bundle.candidateOutcomes[${index}]`;
420
+ object(outcome, label);
421
+ const outcomeKeys = new Set(["baseline", "candidateId", "decisionCandidates", "eligibleForReconciliation", "lifecycle", "promotionEvidence"]);
422
+ exactKeys(outcome, outcomeKeys, label); requiredKeys(outcome, outcomeKeys, label);
423
+ id(outcome.candidateId, `${label}.candidateId`); enumeration(outcome.lifecycle, LIFECYCLES, `${label}.lifecycle`);
424
+ enumeration(outcome.baseline, new Set(["current", "stale"]), `${label}.baseline`);
425
+ enumeration(outcome.decisionCandidates, new Set(["authority-free-candidates", "discarded-not-promoted"]), `${label}.decisionCandidates`);
426
+ reviewBoolean(outcome.eligibleForReconciliation, undefined, `${label}.eligibleForReconciliation`);
427
+ object(outcome.promotionEvidence, `${label}.promotionEvidence`);
428
+ const promotionKeys = new Set(["assurance", "currentEvidenceCount", "integrationVerified", "localQaReusable", "staleEvidenceCount"]);
429
+ exactKeys(outcome.promotionEvidence, promotionKeys, `${label}.promotionEvidence`); requiredKeys(outcome.promotionEvidence, promotionKeys, `${label}.promotionEvidence`);
430
+ if (outcome.promotionEvidence.assurance !== "host-asserted") invalid(`${label}.promotionEvidence.assurance is invalid`);
431
+ for (const key of ["currentEvidenceCount", "staleEvidenceCount"]) if (!Number.isSafeInteger(outcome.promotionEvidence[key]) || outcome.promotionEvidence[key] < 0) invalid(`${label}.promotionEvidence.${key} is invalid`);
432
+ reviewBoolean(outcome.promotionEvidence.localQaReusable, undefined, `${label}.promotionEvidence.localQaReusable`);
433
+ reviewBoolean(outcome.promotionEvidence.integrationVerified, undefined, `${label}.promotionEvidence.integrationVerified`);
434
+ }
435
+ array(input.collisionGroups, "truth review bundle.collisionGroups");
436
+ for (const [index, group] of input.collisionGroups.entries()) {
437
+ const label = `truth review bundle.collisionGroups[${index}]`;
438
+ object(group, label);
439
+ const groupKeys = new Set(["affectedItemIds", "affectedScopes", "affectedSourceIds", "affectedSubjects", "candidateDigests", "candidateIds", "classification", "key", "kind", "promoted", "resolutionKinds"]);
440
+ exactKeys(group, groupKeys, label); requiredKeys(group, groupKeys, label);
441
+ enumeration(group.kind, new Set(["source", "item", "subject"]), `${label}.kind`); id(group.key, `${label}.key`); enumeration(group.classification, CHANGE_CLASSES, `${label}.classification`);
442
+ for (const key of ["candidateIds", "affectedSourceIds", "affectedItemIds", "affectedSubjects"]) reviewStrings(group[key], `${label}.${key}`, { idsOnly: true });
443
+ reviewStrings(group.candidateDigests, `${label}.candidateDigests`); group.candidateDigests.forEach((entry) => digest(entry, `${label}.candidateDigests entry`));
444
+ array(group.affectedScopes, `${label}.affectedScopes`); group.affectedScopes.forEach((entry, scopeIndex) => normalizeTruthScope(entry, `${label}.affectedScopes[${scopeIndex}]`));
445
+ reviewStrings(group.resolutionKinds, `${label}.resolutionKinds`, { nonEmpty: true }); group.resolutionKinds.forEach((entry) => enumeration(entry, RESOLUTION_KINDS, `${label}.resolutionKinds entry`));
446
+ reviewBoolean(group.promoted, false, `${label}.promoted`);
447
+ }
448
+ array(input.findings, "truth review bundle.findings");
449
+ array(input.resolutionOutcomes, "truth review bundle.resolutionOutcomes");
450
+ for (const [index, finding] of input.findings.entries()) {
451
+ const label = `truth review bundle.findings[${index}]`;
452
+ object(finding, label);
453
+ const findingKeys = new Set(["affectedItemIds", "affectedScopes", "affectedSourceIds", "affectedSubjects", "baseline", "blocksExistingDelivery", "blocksTask", "code", "findingDigest", "humanRoles", "recomputeFromCurrentBaseline", "recoveryPreviews", "resolutionContract", "resolutionKinds", "severity", "validateAfterResolution"]);
454
+ exactKeys(finding, findingKeys, label); requiredKeys(finding, findingKeys, label);
455
+ id(finding.code, `${label}.code`); enumeration(finding.severity, SEVERITIES, `${label}.severity`);
456
+ for (const key of ["affectedSourceIds", "affectedItemIds", "affectedSubjects", "humanRoles", "validateAfterResolution"]) reviewStrings(finding[key], `${label}.${key}`, { idsOnly: true });
457
+ array(finding.affectedScopes, `${label}.affectedScopes`); finding.affectedScopes.forEach((entry, scopeIndex) => normalizeTruthScope(entry, `${label}.affectedScopes[${scopeIndex}]`));
458
+ reviewBaseline(finding.baseline, `${label}.baseline`);
459
+ reviewBoolean(finding.blocksTask, undefined, `${label}.blocksTask`); reviewBoolean(finding.blocksExistingDelivery, false, `${label}.blocksExistingDelivery`);
460
+ reviewStrings(finding.resolutionKinds, `${label}.resolutionKinds`, { nonEmpty: true }); finding.resolutionKinds.forEach((entry) => enumeration(entry, RESOLUTION_KINDS, `${label}.resolutionKinds entry`));
461
+ array(finding.recoveryPreviews, `${label}.recoveryPreviews`); if (finding.recoveryPreviews.length === 0) invalid(`${label}.recoveryPreviews must not be empty`);
462
+ finding.recoveryPreviews.forEach((entry, previewIndex) => reviewPreview(entry, `${label}.recoveryPreviews[${previewIndex}]`));
463
+ reviewBoolean(finding.recomputeFromCurrentBaseline, true, `${label}.recomputeFromCurrentBaseline`);
464
+ digest(finding.findingDigest, `${label}.findingDigest`);
465
+ reviewResolutionContract(finding.resolutionContract, `${label}.resolutionContract`);
466
+ }
467
+ for (const [index, outcome] of input.resolutionOutcomes.entries()) {
468
+ const label = `truth review bundle.resolutionOutcomes[${index}]`;
469
+ object(outcome, label);
470
+ const outcomeKeys = new Set(["completionCriteriaMet", "findingDigest", "outcome", "recomputedFromCurrentBaseline", "resolutionKind", "resolutionState"]);
471
+ exactKeys(outcome, outcomeKeys, label); requiredKeys(outcome, outcomeKeys, label);
472
+ digest(outcome.findingDigest, `${label}.findingDigest`); enumeration(outcome.resolutionKind, RESOLUTION_KINDS, `${label}.resolutionKind`);
473
+ enumeration(outcome.resolutionState, new Set(["revalidation-required", "external-fix-required", "resolved", "deferred-open"]), `${label}.resolutionState`);
474
+ enumeration(outcome.outcome, new Set(["open", "deferred", "resolved", "rejected"]), `${label}.outcome`);
475
+ reviewStrings(outcome.completionCriteriaMet, `${label}.completionCriteriaMet`);
476
+ reviewBoolean(outcome.recomputedFromCurrentBaseline, true, `${label}.recomputedFromCurrentBaseline`);
477
+ }
478
+ object(input.guarantees, "truth review bundle.guarantees");
479
+ exactKeys(input.guarantees, new Set(["hostAsserted", "humanDecided", "projectContextVerified"]), "truth review bundle.guarantees");
480
+ requiredKeys(input.guarantees, ["hostAsserted", "humanDecided", "projectContextVerified"], "truth review bundle.guarantees");
481
+ for (const key of ["hostAsserted", "humanDecided", "projectContextVerified"]) reviewStrings(input.guarantees[key], `truth review bundle.guarantees.${key}`, { idsOnly: true });
482
+ object(input.boundaries, "truth review bundle.boundaries");
483
+ const boundaryKeys = new Set(["automaticApproval", "automaticPromotion", "blocksExistingDelivery", "ciClient", "git", "network", "persistentQueue", "provider", "readOnly", "taskExecution"]);
484
+ exactKeys(input.boundaries, boundaryKeys, "truth review bundle.boundaries"); requiredKeys(input.boundaries, boundaryKeys, "truth review bundle.boundaries");
485
+ reviewBoolean(input.boundaries.readOnly, true, "truth review bundle.boundaries.readOnly");
486
+ for (const key of [...boundaryKeys].filter((entry) => entry !== "readOnly")) reviewBoolean(input.boundaries[key], false, `truth review bundle.boundaries.${key}`);
487
+ return canonicalValue(input);
488
+ }