frontend-project-context 1.3.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -2
- package/README.md +94 -16
- package/UPGRADING.md +47 -2
- package/docs/04-PROGRAM-DESIGN.md +40 -4
- package/docs/05-ACCEPTANCE-CONTRACT.md +33 -3
- package/docs/08-INSTALLATION-AND-DISTRIBUTION.md +36 -6
- package/docs/14-FORMAL-RELEASE-READINESS.md +46 -0
- package/docs/18-BRANCH-AWARE-STAGED-CONTEXT-DESIGN.md +62 -2
- package/docs/19-POST-1.3.1-AI-TAKEOVER-EVIDENCE-AND-UPGRADE-PLAN.md +579 -0
- package/docs/20-PHASE-A-AI-TAKEOVER-AND-HEALTH-CLOSURE-DESIGN.md +535 -0
- package/docs/21-PHASE-B-EVIDENCE-FEEDBACK-PROTOCOL-DESIGN.md +347 -0
- package/docs/22-PHASE-C-TARGET-UPGRADE-PROTOCOL-DESIGN.md +398 -0
- package/docs/README.md +21 -5
- package/docs/USER-AND-AI-OPERATION-MANUAL.md +797 -0
- package/examples/README.md +38 -0
- package/examples/package.json +6 -2
- package/migration-manifest.json +88 -0
- package/package.json +3 -2
- package/schemas/action-plan.schema.json +31 -3
- package/schemas/capabilities.schema.json +50 -18
- package/schemas/evidence-bundle.schema.json +64 -0
- package/schemas/evidence-input.schema.json +82 -0
- package/schemas/migration-manifest.schema.json +29 -0
- package/schemas/migration-plan.schema.json +32 -0
- package/schemas/project-status.schema.json +75 -0
- package/schemas/projection-lock.schema.json +48 -0
- package/schemas/review-bundle.schema.json +3 -3
- package/schemas/upgrade-assessment.schema.json +48 -0
- package/schemas/upgrade-result-bundle.schema.json +35 -0
- package/src/project-context/ai-entry.mjs +320 -0
- package/src/project-context/capabilities.mjs +44 -17
- package/src/project-context/checker.mjs +20 -3
- package/src/project-context/cli.mjs +84 -7
- package/src/project-context/contract-schema.mjs +30 -16
- package/src/project-context/dashboard-model.mjs +4 -4
- package/src/project-context/dashboard-renderer.mjs +3 -3
- package/src/project-context/discovery.mjs +6 -1
- package/src/project-context/evidence-schema.mjs +209 -0
- package/src/project-context/evidence.mjs +99 -0
- package/src/project-context/exchange-schema.mjs +21 -11
- package/src/project-context/exchange.mjs +26 -4
- package/src/project-context/maintenance.mjs +2 -2
- package/src/project-context/migration-manifest.mjs +166 -0
- package/src/project-context/project-status.mjs +157 -0
- package/src/project-context/projection-store.mjs +8 -1
- package/src/project-context/task-context-schema.mjs +237 -1
- package/src/project-context/task-context.mjs +154 -13
- package/src/project-context/upgrade-schema.mjs +215 -0
- package/src/project-context/upgrade.mjs +494 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { buildSyncAssistBundle } from "./assist.mjs";
|
|
2
|
+
import { inspectAiEntry } from "./ai-entry.mjs";
|
|
3
|
+
import { checkExitCode, checkProject } from "./checker.mjs";
|
|
4
|
+
import { ProjectContextError } from "./errors.mjs";
|
|
5
|
+
import { PACKAGE_VERSION } from "./exchange-schema.mjs";
|
|
6
|
+
import { inspectProjectInitialization, loadProject } from "./project-store.mjs";
|
|
7
|
+
|
|
8
|
+
export const PROJECT_STATUS_SCHEMA_VERSION = 1;
|
|
9
|
+
|
|
10
|
+
function uniqueSorted(values) {
|
|
11
|
+
return [...new Set(values.filter((value) => value !== undefined && value !== null))]
|
|
12
|
+
.sort((left, right) => String(left).localeCompare(String(right)));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function emptySummary() {
|
|
16
|
+
return { findings: 0, changedSources: 0, pendingItems: 0, affectedProjections: 0 };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function compactReadTarget(target) {
|
|
20
|
+
return {
|
|
21
|
+
sourceId: target.sourceId,
|
|
22
|
+
locator: structuredClone(target.locator),
|
|
23
|
+
reason: target.reason,
|
|
24
|
+
priority: target.priority,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function compactWorkUnit(unit) {
|
|
29
|
+
const compact = { kind: unit.kind };
|
|
30
|
+
for (const key of ["sourceIds", "itemIds", "fallbackItemIds", "paths", "projectionPaths", "findingCodes"]) {
|
|
31
|
+
if (unit[key] !== undefined) compact[key] = [...unit[key]];
|
|
32
|
+
}
|
|
33
|
+
return compact;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function base(initialization, health, nextActions) {
|
|
37
|
+
return {
|
|
38
|
+
schemaVersion: PROJECT_STATUS_SCHEMA_VERSION,
|
|
39
|
+
package: { name: "frontend-project-context", version: PACKAGE_VERSION },
|
|
40
|
+
initialization: {
|
|
41
|
+
state: initialization,
|
|
42
|
+
present: [],
|
|
43
|
+
missing: [],
|
|
44
|
+
},
|
|
45
|
+
project: null,
|
|
46
|
+
snapshots: null,
|
|
47
|
+
health,
|
|
48
|
+
entry: { state: "absent", path: null, rendererVersion: null },
|
|
49
|
+
summary: emptySummary(),
|
|
50
|
+
findingCodes: [],
|
|
51
|
+
sourceIds: [],
|
|
52
|
+
itemIds: [],
|
|
53
|
+
projectionPaths: [],
|
|
54
|
+
readTargets: [],
|
|
55
|
+
workUnits: [],
|
|
56
|
+
nextActions,
|
|
57
|
+
boundaries: {},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function statusBoundaries(boundaries) {
|
|
62
|
+
return structuredClone(boundaries);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function buildProjectStatus(root, boundaries) {
|
|
66
|
+
const initialization = await inspectProjectInitialization(root);
|
|
67
|
+
if (initialization.status !== "initialized") {
|
|
68
|
+
const result = base(initialization.status, initialization.status, initialization.status === "uninitialized" ? ["run-setup-preview"] : ["resolve-conflict"]);
|
|
69
|
+
result.initialization.present = [...initialization.present];
|
|
70
|
+
result.initialization.missing = [...initialization.missing];
|
|
71
|
+
if (initialization.status === "partial") result.findingCodes = ["project-state-partial"];
|
|
72
|
+
result.summary.findings = result.findingCodes.length;
|
|
73
|
+
result.boundaries = statusBoundaries(boundaries);
|
|
74
|
+
return { status: result, exitCode: initialization.status === "uninitialized" ? 1 : 2 };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let project;
|
|
78
|
+
try {
|
|
79
|
+
project = await loadProject(root);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (!(error instanceof ProjectContextError)) throw error;
|
|
82
|
+
const result = base("invalid", "invalid", ["resolve-conflict"]);
|
|
83
|
+
result.initialization.present = [...initialization.present];
|
|
84
|
+
result.initialization.missing = [...initialization.missing];
|
|
85
|
+
result.findingCodes = ["project-state-invalid"];
|
|
86
|
+
result.summary.findings = 1;
|
|
87
|
+
result.boundaries = statusBoundaries(boundaries);
|
|
88
|
+
return { status: result, exitCode: 2 };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const regionEntries = project.projectionsLock.projections.filter((entry) => entry.ownership === "region" && entry.target === "ai-entry");
|
|
92
|
+
const entryPath = regionEntries[0]?.path ?? "AGENTS.md";
|
|
93
|
+
const [sync, entry] = await Promise.all([
|
|
94
|
+
buildSyncAssistBundle(root, project),
|
|
95
|
+
inspectAiEntry(root, project, entryPath),
|
|
96
|
+
]);
|
|
97
|
+
const findings = [...sync.findings];
|
|
98
|
+
if (entry.state === "absent") findings.push({ code: "ai-entry-missing", path: null });
|
|
99
|
+
if (entry.state === "conflict" && !findings.some((finding) => finding.code === "ai-entry-ownership-conflict")) {
|
|
100
|
+
findings.push({ code: "ai-entry-ownership-conflict", path: entry.path });
|
|
101
|
+
}
|
|
102
|
+
const conflict = entry.state === "conflict" || findings.some((finding) => finding.code.includes("ownership-conflict"));
|
|
103
|
+
const sourceOrContractBlocker = findings.some((finding) =>
|
|
104
|
+
finding.code.startsWith("source-") || finding.code.startsWith("verification-") || finding.code.startsWith("scope-") || finding.code === "contract-conflict",
|
|
105
|
+
);
|
|
106
|
+
const health = conflict ? "conflict" : findings.length > 0 || entry.state !== "current" ? "attention" : "clean";
|
|
107
|
+
const nextActions = [];
|
|
108
|
+
if (conflict) nextActions.push("resolve-conflict");
|
|
109
|
+
else {
|
|
110
|
+
if (sync.summary.changedSources > 0 || sourceOrContractBlocker) nextActions.push("review-source-change");
|
|
111
|
+
if (sync.summary.pendingItems > 0) nextActions.push("review-pending");
|
|
112
|
+
if (sync.summary.affectedProjections > 0) nextActions.push("review-projection");
|
|
113
|
+
if (entry.state !== "current") nextActions.push("publish-ai-entry");
|
|
114
|
+
if (findings.length > 0 && nextActions.length === 0) nextActions.push("run-sync");
|
|
115
|
+
if (health === "clean") nextActions.push("ready-for-task");
|
|
116
|
+
}
|
|
117
|
+
const sourceIds = uniqueSorted([
|
|
118
|
+
...findings.flatMap((finding) => finding.source ? [finding.source] : []),
|
|
119
|
+
...sync.workUnits.flatMap((unit) => unit.sourceIds ?? []),
|
|
120
|
+
]);
|
|
121
|
+
const itemIds = uniqueSorted([
|
|
122
|
+
...findings.flatMap((finding) => finding.items ?? (finding.item ? [finding.item] : [])),
|
|
123
|
+
...sync.workUnits.flatMap((unit) => unit.itemIds ?? []),
|
|
124
|
+
]);
|
|
125
|
+
const projectionPaths = uniqueSorted([
|
|
126
|
+
...sync.projectionPaths,
|
|
127
|
+
...findings.flatMap((finding) => finding.path ? [finding.path] : []),
|
|
128
|
+
]);
|
|
129
|
+
const status = {
|
|
130
|
+
schemaVersion: PROJECT_STATUS_SCHEMA_VERSION,
|
|
131
|
+
package: { name: "frontend-project-context", version: PACKAGE_VERSION },
|
|
132
|
+
initialization: { state: "initialized", present: [...initialization.present], missing: [] },
|
|
133
|
+
project: { id: project.contract.project.id, name: project.contract.project.name },
|
|
134
|
+
snapshots: {
|
|
135
|
+
contract: project.contractDigest,
|
|
136
|
+
sourcesLock: project.sourcesLockDigest,
|
|
137
|
+
projectionsLock: project.projectionsLockDigest,
|
|
138
|
+
},
|
|
139
|
+
health,
|
|
140
|
+
entry,
|
|
141
|
+
summary: {
|
|
142
|
+
findings: findings.length,
|
|
143
|
+
changedSources: sync.summary.changedSources,
|
|
144
|
+
pendingItems: sync.summary.pendingItems,
|
|
145
|
+
affectedProjections: projectionPaths.length,
|
|
146
|
+
},
|
|
147
|
+
findingCodes: uniqueSorted(findings.map((finding) => finding.code)),
|
|
148
|
+
sourceIds,
|
|
149
|
+
itemIds,
|
|
150
|
+
projectionPaths,
|
|
151
|
+
readTargets: sync.readTargets.map(compactReadTarget),
|
|
152
|
+
workUnits: sync.workUnits.map(compactWorkUnit),
|
|
153
|
+
nextActions: uniqueSorted(nextActions),
|
|
154
|
+
boundaries: statusBoundaries(boundaries),
|
|
155
|
+
};
|
|
156
|
+
return { status, exitCode: health === "clean" ? 0 : conflict ? 3 : Math.max(1, checkExitCode(findings)) };
|
|
157
|
+
}
|
|
@@ -28,6 +28,12 @@ export async function publishProjection(root, project, options, dependencies = {
|
|
|
28
28
|
const rendered = renderProjection(project.contract, paths, target);
|
|
29
29
|
const existing = await readContent(resolved.absolute);
|
|
30
30
|
const lockEntry = project.projectionsLock.projections.find((entry) => entry.path === resolved.normalized);
|
|
31
|
+
if (lockEntry?.ownership === "region") {
|
|
32
|
+
fail("ai-entry-path-conflict", `refusing whole-file projection over managed AI Entry: ${resolved.normalized}`, {
|
|
33
|
+
exitCode: 3,
|
|
34
|
+
details: { path: resolved.normalized },
|
|
35
|
+
});
|
|
36
|
+
}
|
|
31
37
|
if (existing !== null) {
|
|
32
38
|
const marker = parseProjectionMarker(existing);
|
|
33
39
|
if (
|
|
@@ -46,6 +52,7 @@ export async function publishProjection(root, project, options, dependencies = {
|
|
|
46
52
|
const nextEntry = {
|
|
47
53
|
path: resolved.normalized,
|
|
48
54
|
target,
|
|
55
|
+
...(project.projectionsLock.schemaVersion === 2 ? { ownership: "file" } : {}),
|
|
49
56
|
paths: rendered.paths,
|
|
50
57
|
contractDigest: rendered.contractDigest,
|
|
51
58
|
bundleDigest: rendered.bundleDigest,
|
|
@@ -54,7 +61,7 @@ export async function publishProjection(root, project, options, dependencies = {
|
|
|
54
61
|
rendererVersion: rendered.rendererVersion,
|
|
55
62
|
};
|
|
56
63
|
const nextLock = {
|
|
57
|
-
schemaVersion:
|
|
64
|
+
schemaVersion: project.projectionsLock.schemaVersion,
|
|
58
65
|
projections: [
|
|
59
66
|
...project.projectionsLock.projections.filter((entry) => entry.path !== resolved.normalized),
|
|
60
67
|
nextEntry,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { canonicalValue, digestJson } from "./canonical-json.mjs";
|
|
1
|
+
import { canonicalJson, canonicalValue, digestJson, validateJsonValue } from "./canonical-json.mjs";
|
|
2
2
|
import { fail } from "./errors.mjs";
|
|
3
3
|
import { normalizeRelativePath } from "./path-policy.mjs";
|
|
4
4
|
|
|
@@ -288,3 +288,239 @@ export function validateStageReceipt(input, planInput, options = {}) {
|
|
|
288
288
|
export function stageReceiptDigest(receipt, plan) {
|
|
289
289
|
return digestJson(validateStageReceipt(receipt, plan));
|
|
290
290
|
}
|
|
291
|
+
|
|
292
|
+
function requiredKeys(value, required, label, kind) {
|
|
293
|
+
for (const key of required) {
|
|
294
|
+
if (!Object.hasOwn(value, key)) invalid(kind, `${label} is missing required field: ${key}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function canonicalPaths(value, label, kind, options = {}) {
|
|
299
|
+
const normalized = paths(value, label, kind, options);
|
|
300
|
+
if (canonicalJson(value) !== canonicalJson(normalized)) invalid(kind, `${label} must be sorted canonical project-relative paths`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function canonicalTexts(value, label, kind, options = {}) {
|
|
304
|
+
const normalized = uniqueStrings(value, label, kind, options);
|
|
305
|
+
if (canonicalJson(value) !== canonicalJson(normalized)) invalid(kind, `${label} must be sorted unique strings`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function positiveInteger(value, label, kind) {
|
|
309
|
+
if (!Number.isSafeInteger(value) || value <= 0) invalid(kind, `${label} must be a positive integer`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function nonNegativeInteger(value, label, kind) {
|
|
313
|
+
if (!Number.isSafeInteger(value) || value < 0) invalid(kind, `${label} must be a non-negative integer`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function validateIdentity(value, label, kind) {
|
|
317
|
+
object(value, label, kind);
|
|
318
|
+
requiredKeys(value, ["id", "name"], label, kind);
|
|
319
|
+
exactKeys(value, new Set(["id", "name"]), label, kind);
|
|
320
|
+
stableId(value.id, `${label}.id`, kind);
|
|
321
|
+
string(value.name, `${label}.name`, kind);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function validateBundleTask(value, label, kind) {
|
|
325
|
+
object(value, label, kind);
|
|
326
|
+
requiredKeys(value, ["id", "title", "goal"], label, kind);
|
|
327
|
+
exactKeys(value, new Set(["id", "title", "goal"]), label, kind);
|
|
328
|
+
stableId(value.id, `${label}.id`, kind);
|
|
329
|
+
string(value.title, `${label}.title`, kind);
|
|
330
|
+
string(value.goal, `${label}.goal`, kind);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function validateBundleWorkspace(value, label, kind) {
|
|
334
|
+
object(value, label, kind);
|
|
335
|
+
requiredKeys(value, ["branchLabel", "baseRevision"], label, kind);
|
|
336
|
+
exactKeys(value, new Set(["branchLabel", "baseRevision"]), label, kind);
|
|
337
|
+
string(value.branchLabel, `${label}.branchLabel`, kind);
|
|
338
|
+
string(value.baseRevision, `${label}.baseRevision`, kind);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function validateBundleStage(value, label, kind) {
|
|
342
|
+
object(value, label, kind);
|
|
343
|
+
requiredKeys(value, ["id", "title", "objective", "acceptance", "paths"], label, kind);
|
|
344
|
+
exactKeys(value, new Set(["id", "title", "objective", "acceptance", "paths"]), label, kind);
|
|
345
|
+
stableId(value.id, `${label}.id`, kind);
|
|
346
|
+
string(value.title, `${label}.title`, kind);
|
|
347
|
+
string(value.objective, `${label}.objective`, kind);
|
|
348
|
+
if (!Array.isArray(value.acceptance) || value.acceptance.length === 0) invalid(kind, `${label}.acceptance must be a non-empty array`);
|
|
349
|
+
const acceptanceIds = new Set();
|
|
350
|
+
for (const [index, entry] of value.acceptance.entries()) {
|
|
351
|
+
const entryLabel = `${label}.acceptance[${index}]`;
|
|
352
|
+
object(entry, entryLabel, kind);
|
|
353
|
+
requiredKeys(entry, ["id", "text"], entryLabel, kind);
|
|
354
|
+
exactKeys(entry, new Set(["id", "text"]), entryLabel, kind);
|
|
355
|
+
stableId(entry.id, `${entryLabel}.id`, kind);
|
|
356
|
+
string(entry.text, `${entryLabel}.text`, kind);
|
|
357
|
+
if (acceptanceIds.has(entry.id)) invalid(kind, `${label}.acceptance contains duplicate id: ${entry.id}`);
|
|
358
|
+
acceptanceIds.add(entry.id);
|
|
359
|
+
}
|
|
360
|
+
const sortedAcceptanceIds = [...acceptanceIds].sort((left, right) => left.localeCompare(right));
|
|
361
|
+
if (canonicalJson(value.acceptance.map((entry) => entry.id)) !== canonicalJson(sortedAcceptanceIds)) {
|
|
362
|
+
invalid(kind, `${label}.acceptance must be sorted by id`);
|
|
363
|
+
}
|
|
364
|
+
canonicalPaths(value.paths, `${label}.paths`, kind);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function validateReceiptSummary(value, label, kind) {
|
|
368
|
+
object(value, label, kind);
|
|
369
|
+
const keys = ["stageId", "status", "inputBundleDigest", "changedPaths", "acceptanceResults", "verificationResults", "decisions", "openIssues"];
|
|
370
|
+
requiredKeys(value, keys, label, kind);
|
|
371
|
+
exactKeys(value, new Set(keys), label, kind);
|
|
372
|
+
stableId(value.stageId, `${label}.stageId`, kind);
|
|
373
|
+
if (!["completed", "blocked"].includes(value.status)) invalid(kind, `${label}.status must be completed or blocked`);
|
|
374
|
+
digest(value.inputBundleDigest, `${label}.inputBundleDigest`, kind);
|
|
375
|
+
canonicalPaths(value.changedPaths, `${label}.changedPaths`, kind, { empty: true });
|
|
376
|
+
if (!Array.isArray(value.acceptanceResults)) invalid(kind, `${label}.acceptanceResults must be an array`);
|
|
377
|
+
const acceptanceIds = new Set();
|
|
378
|
+
for (const [index, entry] of value.acceptanceResults.entries()) {
|
|
379
|
+
acceptanceResult(entry, `${label}.acceptanceResults[${index}]`, kind);
|
|
380
|
+
if (acceptanceIds.has(entry.id)) invalid(kind, `${label}.acceptanceResults contains duplicate id: ${entry.id}`);
|
|
381
|
+
acceptanceIds.add(entry.id);
|
|
382
|
+
}
|
|
383
|
+
if (canonicalJson(value.acceptanceResults.map((entry) => entry.id)) !== canonicalJson([...acceptanceIds].sort((left, right) => left.localeCompare(right)))) {
|
|
384
|
+
invalid(kind, `${label}.acceptanceResults must be sorted by id`);
|
|
385
|
+
}
|
|
386
|
+
if (!Array.isArray(value.verificationResults)) invalid(kind, `${label}.verificationResults must be an array`);
|
|
387
|
+
const verificationIds = new Set();
|
|
388
|
+
for (const [index, entry] of value.verificationResults.entries()) {
|
|
389
|
+
verificationResult(entry, `${label}.verificationResults[${index}]`, kind);
|
|
390
|
+
if (verificationIds.has(entry.id)) invalid(kind, `${label}.verificationResults contains duplicate id: ${entry.id}`);
|
|
391
|
+
verificationIds.add(entry.id);
|
|
392
|
+
}
|
|
393
|
+
if (canonicalJson(value.verificationResults.map((entry) => entry.id)) !== canonicalJson([...verificationIds].sort((left, right) => left.localeCompare(right)))) {
|
|
394
|
+
invalid(kind, `${label}.verificationResults must be sorted by id`);
|
|
395
|
+
}
|
|
396
|
+
canonicalTexts(value.decisions, `${label}.decisions`, kind, { empty: true });
|
|
397
|
+
canonicalTexts(value.openIssues, `${label}.openIssues`, kind, { empty: true });
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function validateBundleScope(value, label, kind) {
|
|
401
|
+
object(value, label, kind);
|
|
402
|
+
exactKeys(value, new Set(["kind", "path"]), label, kind);
|
|
403
|
+
if (!["project", "path-prefix", "file"].includes(value.kind)) invalid(kind, `${label}.kind is invalid`);
|
|
404
|
+
if (value.kind === "project") {
|
|
405
|
+
if (value.path !== undefined) invalid(kind, `${label}.path is not allowed for project scope`);
|
|
406
|
+
} else {
|
|
407
|
+
canonicalPaths([value.path], `${label}.path`, kind);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function validateContractItem(value, label, kind) {
|
|
412
|
+
object(value, label, kind);
|
|
413
|
+
const keys = ["id", "kind", "subject", "value", "statement", "scope", "sourceIds"];
|
|
414
|
+
requiredKeys(value, keys, label, kind);
|
|
415
|
+
exactKeys(value, new Set(keys), label, kind);
|
|
416
|
+
stableId(value.id, `${label}.id`, kind);
|
|
417
|
+
if (!["fact", "policy", "reference", "validation-description"].includes(value.kind)) invalid(kind, `${label}.kind is invalid`);
|
|
418
|
+
stableId(value.subject, `${label}.subject`, kind);
|
|
419
|
+
try {
|
|
420
|
+
validateJsonValue(value.value);
|
|
421
|
+
} catch (error) {
|
|
422
|
+
invalid(kind, `${label}.value must be strict JSON`, { reason: error.message });
|
|
423
|
+
}
|
|
424
|
+
string(value.statement, `${label}.statement`, kind);
|
|
425
|
+
validateBundleScope(value.scope, `${label}.scope`, kind);
|
|
426
|
+
const sourceIds = uniqueStrings(value.sourceIds, `${label}.sourceIds`, kind);
|
|
427
|
+
sourceIds.forEach((id) => stableId(id, `${label}.sourceIds entry`, kind));
|
|
428
|
+
if (canonicalJson(value.sourceIds) !== canonicalJson(sourceIds)) invalid(kind, `${label}.sourceIds must be sorted`);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function validateFinding(value, label, kind) {
|
|
432
|
+
object(value, label, kind);
|
|
433
|
+
requiredKeys(value, ["code", "severity"], label, kind);
|
|
434
|
+
stableId(value.code, `${label}.code`, kind);
|
|
435
|
+
if (!["attention", "blocked"].includes(value.severity)) invalid(kind, `${label}.severity is invalid`);
|
|
436
|
+
try {
|
|
437
|
+
validateJsonValue(value);
|
|
438
|
+
} catch (error) {
|
|
439
|
+
invalid(kind, `${label} must be strict JSON`, { reason: error.message });
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export function validateStageContextBundle(input) {
|
|
444
|
+
const kind = "stage-context-bundle";
|
|
445
|
+
object(input, "stage context bundle", kind);
|
|
446
|
+
const keys = [
|
|
447
|
+
"schemaVersion", "kind", "project", "task", "stage", "workspace", "planDigest", "snapshots",
|
|
448
|
+
"dependencyReceipts", "changedPaths", "contractItems", "readTargets", "findings", "excluded", "budget", "status", "bundleDigest",
|
|
449
|
+
];
|
|
450
|
+
requiredKeys(input, keys, "stage context bundle", kind);
|
|
451
|
+
exactKeys(input, new Set(keys), "stage context bundle", kind);
|
|
452
|
+
if (input.schemaVersion !== STAGE_CONTEXT_BUNDLE_SCHEMA_VERSION) invalid(kind, `stage context bundle schemaVersion must be ${STAGE_CONTEXT_BUNDLE_SCHEMA_VERSION}`);
|
|
453
|
+
if (input.kind !== "stage-context-bundle") invalid(kind, "stage context bundle.kind must be stage-context-bundle");
|
|
454
|
+
validateIdentity(input.project, "stage context bundle.project", kind);
|
|
455
|
+
validateBundleTask(input.task, "stage context bundle.task", kind);
|
|
456
|
+
validateBundleStage(input.stage, "stage context bundle.stage", kind);
|
|
457
|
+
validateBundleWorkspace(input.workspace, "stage context bundle.workspace", kind);
|
|
458
|
+
digest(input.planDigest, "stage context bundle.planDigest", kind);
|
|
459
|
+
snapshots(input.snapshots, "stage context bundle.snapshots", kind);
|
|
460
|
+
if (!Array.isArray(input.dependencyReceipts)) invalid(kind, "stage context bundle.dependencyReceipts must be an array");
|
|
461
|
+
const dependencyIds = new Set();
|
|
462
|
+
for (const [index, entry] of input.dependencyReceipts.entries()) {
|
|
463
|
+
validateReceiptSummary(entry, `stage context bundle.dependencyReceipts[${index}]`, kind);
|
|
464
|
+
if (dependencyIds.has(entry.stageId)) invalid(kind, `stage context bundle contains duplicate dependency receipt: ${entry.stageId}`);
|
|
465
|
+
dependencyIds.add(entry.stageId);
|
|
466
|
+
}
|
|
467
|
+
if (canonicalJson(input.dependencyReceipts.map((entry) => entry.stageId)) !== canonicalJson([...dependencyIds].sort((left, right) => left.localeCompare(right)))) {
|
|
468
|
+
invalid(kind, "stage context bundle.dependencyReceipts must be sorted by stageId");
|
|
469
|
+
}
|
|
470
|
+
canonicalPaths(input.changedPaths, "stage context bundle.changedPaths", kind, { empty: true });
|
|
471
|
+
if (!Array.isArray(input.contractItems)) invalid(kind, "stage context bundle.contractItems must be an array");
|
|
472
|
+
const itemIds = new Set();
|
|
473
|
+
for (const [index, entry] of input.contractItems.entries()) {
|
|
474
|
+
validateContractItem(entry, `stage context bundle.contractItems[${index}]`, kind);
|
|
475
|
+
if (itemIds.has(entry.id)) invalid(kind, `stage context bundle contains duplicate contract item: ${entry.id}`);
|
|
476
|
+
itemIds.add(entry.id);
|
|
477
|
+
}
|
|
478
|
+
if (canonicalJson(input.contractItems.map((entry) => entry.id)) !== canonicalJson([...itemIds].sort((left, right) => left.localeCompare(right)))) {
|
|
479
|
+
invalid(kind, "stage context bundle.contractItems must be sorted by id");
|
|
480
|
+
}
|
|
481
|
+
if (!Array.isArray(input.readTargets)) invalid(kind, "stage context bundle.readTargets must be an array");
|
|
482
|
+
const readPaths = new Set();
|
|
483
|
+
for (const [index, entry] of input.readTargets.entries()) {
|
|
484
|
+
const label = `stage context bundle.readTargets[${index}]`;
|
|
485
|
+
object(entry, label, kind);
|
|
486
|
+
requiredKeys(entry, ["path", "reason"], label, kind);
|
|
487
|
+
exactKeys(entry, new Set(["path", "reason", "sourceId"]), label, kind);
|
|
488
|
+
canonicalPaths([entry.path], `${label}.path`, kind);
|
|
489
|
+
if (!["stage-scope", "host-changed-path-signal", "contract-source"].includes(entry.reason)) invalid(kind, `${label}.reason is invalid`);
|
|
490
|
+
if (entry.reason === "contract-source") stableId(entry.sourceId, `${label}.sourceId`, kind);
|
|
491
|
+
else if (entry.sourceId !== undefined) invalid(kind, `${label}.sourceId requires contract-source reason`);
|
|
492
|
+
if (readPaths.has(entry.path)) invalid(kind, `stage context bundle contains duplicate read target: ${entry.path}`);
|
|
493
|
+
readPaths.add(entry.path);
|
|
494
|
+
}
|
|
495
|
+
if (canonicalJson(input.readTargets.map((entry) => entry.path)) !== canonicalJson([...readPaths].sort((left, right) => left.localeCompare(right)))) {
|
|
496
|
+
invalid(kind, "stage context bundle.readTargets must be sorted by path");
|
|
497
|
+
}
|
|
498
|
+
if (!Array.isArray(input.findings)) invalid(kind, "stage context bundle.findings must be an array");
|
|
499
|
+
input.findings.forEach((entry, index) => validateFinding(entry, `stage context bundle.findings[${index}]`, kind));
|
|
500
|
+
canonicalTexts(input.excluded, "stage context bundle.excluded", kind);
|
|
501
|
+
object(input.budget, "stage context bundle.budget", kind);
|
|
502
|
+
const budgetKeys = ["unit", "maxUtf8Bytes", "maxReadTargets", "usedUtf8Bytes", "readTargetCount"];
|
|
503
|
+
requiredKeys(input.budget, budgetKeys, "stage context bundle.budget", kind);
|
|
504
|
+
exactKeys(input.budget, new Set(budgetKeys), "stage context bundle.budget", kind);
|
|
505
|
+
if (input.budget.unit !== CONTEXT_BUDGET_UNIT) invalid(kind, `stage context bundle.budget.unit must be ${CONTEXT_BUDGET_UNIT}`);
|
|
506
|
+
positiveInteger(input.budget.maxUtf8Bytes, "stage context bundle.budget.maxUtf8Bytes", kind);
|
|
507
|
+
positiveInteger(input.budget.maxReadTargets, "stage context bundle.budget.maxReadTargets", kind);
|
|
508
|
+
nonNegativeInteger(input.budget.usedUtf8Bytes, "stage context bundle.budget.usedUtf8Bytes", kind);
|
|
509
|
+
nonNegativeInteger(input.budget.readTargetCount, "stage context bundle.budget.readTargetCount", kind);
|
|
510
|
+
if (input.budget.readTargetCount !== input.readTargets.length) invalid(kind, "stage context bundle read target count is invalid");
|
|
511
|
+
if (!["ready", "blocked"].includes(input.status)) invalid(kind, "stage context bundle.status must be ready or blocked");
|
|
512
|
+
digest(input.bundleDigest, "stage context bundle.bundleDigest", kind);
|
|
513
|
+
|
|
514
|
+
const normalized = canonicalValue(input);
|
|
515
|
+
const withoutDigest = structuredClone(normalized);
|
|
516
|
+
delete withoutDigest.bundleDigest;
|
|
517
|
+
const expectedDigest = digestJson(withoutDigest);
|
|
518
|
+
if (normalized.bundleDigest !== expectedDigest) {
|
|
519
|
+
invalid(kind, "stage context bundle digest is invalid", { expected: expectedDigest, actual: normalized.bundleDigest });
|
|
520
|
+
}
|
|
521
|
+
const expectedBytes = Buffer.byteLength(canonicalJson(normalized), "utf8");
|
|
522
|
+
if (normalized.budget.usedUtf8Bytes !== expectedBytes) {
|
|
523
|
+
invalid(kind, "stage context bundle byte count is invalid", { expected: expectedBytes, actual: normalized.budget.usedUtf8Bytes });
|
|
524
|
+
}
|
|
525
|
+
return normalized;
|
|
526
|
+
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
INTEGRATION_REVIEW_BUNDLE_SCHEMA_VERSION,
|
|
11
11
|
STAGE_CONTEXT_BUNDLE_SCHEMA_VERSION,
|
|
12
12
|
taskContextPlanDigest,
|
|
13
|
+
validateStageContextBundle,
|
|
13
14
|
validateStageReceipt,
|
|
14
15
|
validateTaskContextPlan,
|
|
15
16
|
} from "./task-context-schema.mjs";
|
|
@@ -185,17 +186,13 @@ function acceptanceFor(plan, stage) {
|
|
|
185
186
|
return plan.task.acceptance.filter((entry) => ids.has(entry.id)).map((entry) => structuredClone(entry));
|
|
186
187
|
}
|
|
187
188
|
|
|
188
|
-
|
|
189
|
-
const plan = validateTaskContextPlan(planInput);
|
|
190
|
-
if (plan.projectId !== project.contract.project.id) fail("task-context-project-mismatch", "task context plan belongs to a different project");
|
|
191
|
-
await validatePlanPaths(root, plan);
|
|
189
|
+
async function buildStageContextBundleCore(root, project, plan, options, receipts, bindingFindings = []) {
|
|
192
190
|
const stage = plan.stages.find((entry) => entry.id === options.stageId);
|
|
193
191
|
if (!stage) fail("task-context-stage-missing", `task context plan has no stage: ${options.stageId}`);
|
|
194
192
|
const changedPaths = normalizeSignalPaths(options.changedPaths ?? [], "changed path");
|
|
195
193
|
await validateProjectPaths(root, changedPaths, "changed path");
|
|
196
|
-
const receipts = normalizedReceipts(options.receipts ?? [], plan);
|
|
197
194
|
const planDigest = taskContextPlanDigest(plan);
|
|
198
|
-
const findings = [...baselineFindings(plan, project), ...checkerFindings(await checkProject(root, project))];
|
|
195
|
+
const findings = [...bindingFindings, ...baselineFindings(plan, project), ...checkerFindings(await checkProject(root, project))];
|
|
199
196
|
const dependencyReceipts = [];
|
|
200
197
|
for (const dependencyId of stage.dependsOn) {
|
|
201
198
|
const receipt = receipts.get(dependencyId);
|
|
@@ -259,6 +256,142 @@ export async function buildStageContextBundle(root, project, planInput, options)
|
|
|
259
256
|
return bundle;
|
|
260
257
|
}
|
|
261
258
|
|
|
259
|
+
function sameJson(left, right) {
|
|
260
|
+
return canonicalJson(left) === canonicalJson(right);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function artifactStageId(input) {
|
|
264
|
+
return input && typeof input === "object" && !Array.isArray(input) && input.stage
|
|
265
|
+
&& typeof input.stage === "object" && !Array.isArray(input.stage) && typeof input.stage.id === "string"
|
|
266
|
+
? input.stage.id
|
|
267
|
+
: undefined;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function verifyReceiptBundleBindings(root, project, plan, receipts, receiptBundleInputs) {
|
|
271
|
+
const findings = [];
|
|
272
|
+
const candidatesByStage = new Map();
|
|
273
|
+
for (const input of receiptBundleInputs) {
|
|
274
|
+
const stageId = artifactStageId(input);
|
|
275
|
+
let bundle;
|
|
276
|
+
try {
|
|
277
|
+
bundle = validateStageContextBundle(input);
|
|
278
|
+
} catch (error) {
|
|
279
|
+
if (!(error instanceof ProjectContextError) || error.code !== "stage-context-bundle-schema-invalid") throw error;
|
|
280
|
+
findings.push(finding("stage-receipt-input-bundle-invalid", "blocked", {
|
|
281
|
+
...(stageId ? { stageId } : {}),
|
|
282
|
+
reason: error.message,
|
|
283
|
+
}));
|
|
284
|
+
}
|
|
285
|
+
if (!stageId) continue;
|
|
286
|
+
const candidates = candidatesByStage.get(stageId) ?? [];
|
|
287
|
+
candidates.push(bundle);
|
|
288
|
+
candidatesByStage.set(stageId, candidates);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
for (const [stageId, candidates] of candidatesByStage) {
|
|
292
|
+
if (!receipts.has(stageId)) findings.push(finding("stage-receipt-input-bundle-mismatch", "blocked", {
|
|
293
|
+
stageId,
|
|
294
|
+
reason: "receipt-missing",
|
|
295
|
+
}));
|
|
296
|
+
if (candidates.length > 1) findings.push(finding("stage-receipt-input-bundle-duplicate", "blocked", {
|
|
297
|
+
stageId,
|
|
298
|
+
count: candidates.length,
|
|
299
|
+
}));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const planDigest = taskContextPlanDigest(plan);
|
|
303
|
+
const currentSnapshots = snapshots(project);
|
|
304
|
+
const verifiedReceipts = new Map();
|
|
305
|
+
const verification = new Map();
|
|
306
|
+
|
|
307
|
+
async function verify(stageId) {
|
|
308
|
+
if (verification.has(stageId)) return verification.get(stageId);
|
|
309
|
+
const pending = (async () => {
|
|
310
|
+
const receipt = receipts.get(stageId);
|
|
311
|
+
if (!receipt) return false;
|
|
312
|
+
const candidates = candidatesByStage.get(stageId) ?? [];
|
|
313
|
+
if (candidates.length === 0) {
|
|
314
|
+
findings.push(finding("stage-receipt-input-bundle-missing", "blocked", { stageId }));
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
if (candidates.length !== 1 || !candidates[0]) return false;
|
|
318
|
+
const bundle = candidates[0];
|
|
319
|
+
let valid = true;
|
|
320
|
+
const mismatch = (reason, details = {}) => {
|
|
321
|
+
valid = false;
|
|
322
|
+
findings.push(finding("stage-receipt-input-bundle-mismatch", "blocked", { stageId, reason, ...details }));
|
|
323
|
+
};
|
|
324
|
+
const stale = (reason, details = {}) => {
|
|
325
|
+
valid = false;
|
|
326
|
+
findings.push(finding("stage-receipt-input-bundle-stale", "blocked", { stageId, reason, ...details }));
|
|
327
|
+
};
|
|
328
|
+
if (bundle.project.id !== receipt.projectId) mismatch("project-id", { expected: receipt.projectId, actual: bundle.project.id });
|
|
329
|
+
if (bundle.task.id !== receipt.taskId) mismatch("task-id", { expected: receipt.taskId, actual: bundle.task.id });
|
|
330
|
+
if (bundle.stage.id !== receipt.stageId) mismatch("stage-id", { expected: receipt.stageId, actual: bundle.stage.id });
|
|
331
|
+
if (bundle.project.id !== plan.projectId || bundle.task.id !== plan.task.id || !plan.stages.some((entry) => entry.id === bundle.stage.id)) {
|
|
332
|
+
mismatch("plan-identity");
|
|
333
|
+
}
|
|
334
|
+
if (receipt.planDigest !== planDigest) {
|
|
335
|
+
stale("receipt-plan-digest", { expected: planDigest, actual: receipt.planDigest });
|
|
336
|
+
findings.push(finding("stage-receipt-stale", "blocked", { stageId, reason: "plan-digest" }));
|
|
337
|
+
}
|
|
338
|
+
if (bundle.planDigest !== planDigest) stale("bundle-plan-digest", { expected: planDigest, actual: bundle.planDigest });
|
|
339
|
+
for (const key of Object.keys(currentSnapshots)) {
|
|
340
|
+
if (bundle.snapshots[key] !== plan.snapshots[key] || bundle.snapshots[key] !== currentSnapshots[key]) {
|
|
341
|
+
stale(`${key}-snapshot`, {
|
|
342
|
+
plan: plan.snapshots[key],
|
|
343
|
+
current: currentSnapshots[key],
|
|
344
|
+
actual: bundle.snapshots[key],
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const stage = plan.stages.find((entry) => entry.id === stageId);
|
|
350
|
+
const dependencyReceipts = new Map();
|
|
351
|
+
for (const dependencyId of stage.dependsOn) {
|
|
352
|
+
if (receipts.has(dependencyId) && await verify(dependencyId)) dependencyReceipts.set(dependencyId, receipts.get(dependencyId));
|
|
353
|
+
}
|
|
354
|
+
const rebuilt = await buildStageContextBundleCore(root, project, plan, {
|
|
355
|
+
stageId,
|
|
356
|
+
changedPaths: bundle.changedPaths,
|
|
357
|
+
}, dependencyReceipts);
|
|
358
|
+
if (!sameJson(bundle, rebuilt)) stale("canonical-content", {
|
|
359
|
+
expectedBundleDigest: rebuilt.bundleDigest,
|
|
360
|
+
actualBundleDigest: bundle.bundleDigest,
|
|
361
|
+
});
|
|
362
|
+
if (receipt.inputBundleDigest !== bundle.bundleDigest) mismatch("receipt-input-bundle-digest", {
|
|
363
|
+
expected: bundle.bundleDigest,
|
|
364
|
+
actual: receipt.inputBundleDigest,
|
|
365
|
+
});
|
|
366
|
+
if (receipt.status === "completed" && bundle.status === "blocked") {
|
|
367
|
+
valid = false;
|
|
368
|
+
findings.push(finding("stage-receipt-input-bundle-invalid", "blocked", {
|
|
369
|
+
stageId,
|
|
370
|
+
reason: "completed-receipt-bound-to-blocked-bundle",
|
|
371
|
+
}));
|
|
372
|
+
}
|
|
373
|
+
if (valid) verifiedReceipts.set(stageId, receipt);
|
|
374
|
+
return valid;
|
|
375
|
+
})();
|
|
376
|
+
verification.set(stageId, pending);
|
|
377
|
+
return pending;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
for (const stage of plan.stages) {
|
|
381
|
+
if (receipts.has(stage.id)) await verify(stage.id);
|
|
382
|
+
}
|
|
383
|
+
return { receipts: verifiedReceipts, findings: stableFindings(findings) };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export async function buildStageContextBundle(root, project, planInput, options) {
|
|
387
|
+
const plan = validateTaskContextPlan(planInput);
|
|
388
|
+
if (plan.projectId !== project.contract.project.id) fail("task-context-project-mismatch", "task context plan belongs to a different project");
|
|
389
|
+
await validatePlanPaths(root, plan);
|
|
390
|
+
const receipts = normalizedReceipts(options.receipts ?? [], plan);
|
|
391
|
+
const binding = await verifyReceiptBundleBindings(root, project, plan, receipts, options.receiptBundles ?? []);
|
|
392
|
+
return buildStageContextBundleCore(root, project, plan, options, binding.receipts, binding.findings);
|
|
393
|
+
}
|
|
394
|
+
|
|
262
395
|
function receiptScopeEscapes(plan, receipt) {
|
|
263
396
|
const stage = plan.stages.find((entry) => entry.id === receipt.stageId);
|
|
264
397
|
if (!stage) return receipt.changedPaths;
|
|
@@ -286,9 +419,12 @@ export async function buildIntegrationReviewBundle(root, project, planInput, opt
|
|
|
286
419
|
const mainChangedPaths = normalizeSignalPaths(options.mainChangedPaths ?? [], "main changed path");
|
|
287
420
|
const branchChangedPaths = normalizeSignalPaths(options.branchChangedPaths ?? [], "branch changed path");
|
|
288
421
|
await validateProjectPaths(root, [...mainChangedPaths, ...branchChangedPaths], "integration changed path");
|
|
289
|
-
const
|
|
422
|
+
const suppliedReceipts = normalizedReceipts(options.receipts ?? [], plan);
|
|
423
|
+
const binding = await verifyReceiptBundleBindings(root, project, plan, suppliedReceipts, options.receiptBundles ?? []);
|
|
424
|
+
const receipts = binding.receipts;
|
|
290
425
|
const planDigest = taskContextPlanDigest(plan);
|
|
291
426
|
const findings = [
|
|
427
|
+
...binding.findings,
|
|
292
428
|
...baselineFindings(plan, project),
|
|
293
429
|
...checkerFindings(await checkProject(root, project)),
|
|
294
430
|
...pathOverlapFindings(mainChangedPaths, branchChangedPaths),
|
|
@@ -337,7 +473,7 @@ export async function buildIntegrationReviewBundle(root, project, planInput, opt
|
|
|
337
473
|
return { ...bundle, bundleDigest: digestJson(bundle) };
|
|
338
474
|
}
|
|
339
475
|
|
|
340
|
-
async function readInputs(root, planPath, receiptPaths) {
|
|
476
|
+
async function readInputs(root, planPath, receiptPaths, receiptBundlePaths) {
|
|
341
477
|
const resolvedPlan = await resolveExistingInside(root, planPath);
|
|
342
478
|
const plan = await readJsonFile(resolvedPlan.absolute, "task context plan");
|
|
343
479
|
const receipts = [];
|
|
@@ -345,17 +481,22 @@ async function readInputs(root, planPath, receiptPaths) {
|
|
|
345
481
|
const resolved = await resolveExistingInside(root, receiptPath);
|
|
346
482
|
receipts.push(await readJsonFile(resolved.absolute, "stage receipt"));
|
|
347
483
|
}
|
|
348
|
-
|
|
484
|
+
const receiptBundles = [];
|
|
485
|
+
for (const bundlePath of receiptBundlePaths) {
|
|
486
|
+
const resolved = await resolveExistingInside(root, bundlePath);
|
|
487
|
+
receiptBundles.push(await readJsonFile(resolved.absolute, "stage context bundle"));
|
|
488
|
+
}
|
|
489
|
+
return { plan, receipts, receiptBundles };
|
|
349
490
|
}
|
|
350
491
|
|
|
351
492
|
export async function buildStageContextBundleFiles(root, planPath, options) {
|
|
352
|
-
const { plan, receipts } = await readInputs(root, planPath, options.receiptPaths ?? []);
|
|
493
|
+
const { plan, receipts, receiptBundles } = await readInputs(root, planPath, options.receiptPaths ?? [], options.receiptBundlePaths ?? []);
|
|
353
494
|
const project = await loadProject(root);
|
|
354
|
-
return buildStageContextBundle(root, project, plan, { ...options, receipts });
|
|
495
|
+
return buildStageContextBundle(root, project, plan, { ...options, receipts, receiptBundles });
|
|
355
496
|
}
|
|
356
497
|
|
|
357
498
|
export async function buildIntegrationReviewBundleFiles(root, planPath, options) {
|
|
358
|
-
const { plan, receipts } = await readInputs(root, planPath, options.receiptPaths ?? []);
|
|
499
|
+
const { plan, receipts, receiptBundles } = await readInputs(root, planPath, options.receiptPaths ?? [], options.receiptBundlePaths ?? []);
|
|
359
500
|
const project = await loadProject(root);
|
|
360
|
-
return buildIntegrationReviewBundle(root, project, plan, { ...options, receipts });
|
|
501
|
+
return buildIntegrationReviewBundle(root, project, plan, { ...options, receipts, receiptBundles });
|
|
361
502
|
}
|