ai-delivery-workflow 0.5.0 → 0.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/README.md +5 -6
- package/bin/ai-delivery.mjs +2 -24
- package/docs/CLI-PARAMETER-REFERENCE.zh-CN.md +6 -67
- package/docs/DUAL-REPOSITORY-WORKSPACE.zh-CN.md +1 -1
- package/docs/FILE-REFERENCE.zh-CN.md +6 -28
- package/docs/MAINTENANCE-VERIFICATION.zh-CN.md +2 -2
- package/docs/PROJECT-MANUAL.zh-CN.md +23 -38
- package/docs/STATE-CLI-MAINTENANCE.zh-CN.md +8 -10
- package/docs/STATE-CLI-USER-GUIDE.zh-CN.md +6 -16
- package/docs/agents/workflow-manager-acceptance-matrix.md +9 -15
- package/docs/agents/workflow-manager-current-state.md +12 -15
- package/lib/communication-confirmation.mjs +177 -0
- package/lib/delivery-state.mjs +59 -910
- package/lib/fs-utils.mjs +0 -16
- package/lib/project-bootstrap.mjs +288 -61
- package/lib/project-installer.mjs +49 -181
- package/lib/project-repair.mjs +217 -0
- package/lib/toml-hooks.mjs +2 -4
- package/lib/workflow-manager-governance.mjs +3 -47
- package/lib/workspace.mjs +2 -3
- package/package.json +2 -2
- package/skills/ai-delivery-bootstrap/SKILL.md +5 -1
- package/skills/ai-delivery-bootstrap/references/bootstrap-contract.md +1 -1
- package/skills/ai-delivery-checkpoint-task/SKILL.md +2 -4
- package/skills/ai-delivery-checkpoint-task/references/checkpoint-contract.md +4 -2
- package/skills/ai-delivery-checkpoint-task/scripts/hook-event.mjs +2 -8
- package/skills/ai-delivery-checkpoint-task/scripts/task-state.mjs +26 -46
- package/skills/ai-delivery-close-version/SKILL.md +13 -10
- package/skills/ai-delivery-close-version/references/version-closeout-contract.md +2 -1
- package/skills/ai-delivery-define-product/SKILL.md +6 -3
- package/skills/ai-delivery-design-architecture/SKILL.md +10 -5
- package/skills/ai-delivery-design-architecture/assets/architecture-template/architecture-gate.yaml +2 -0
- package/skills/ai-delivery-design-architecture/assets/architecture-template/prototype-architecture-validation.yaml +2 -0
- package/skills/ai-delivery-design-architecture/references/architecture-contract.md +4 -0
- package/skills/ai-delivery-design-experience/SKILL.md +8 -6
- package/skills/ai-delivery-design-experience/assets/experience-template/product-prototype-reconciliation.yaml +2 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/ui-handoff.yaml +3 -0
- package/skills/ai-delivery-design-experience/references/experience-contract.md +3 -13
- package/skills/ai-delivery-design-experience/references/prototype-management-contract.md +1 -1
- package/skills/ai-delivery-design-tests/SKILL.md +4 -0
- package/skills/ai-delivery-develop-iteration/SKILL.md +6 -1
- package/skills/ai-delivery-develop-iteration/assets/development-template/candidate-manifest.yaml +2 -0
- package/skills/ai-delivery-execute-work-package/SKILL.md +4 -2
- package/skills/ai-delivery-orchestrate/SKILL.md +7 -7
- package/skills/ai-delivery-orchestrate/assets/codebase-template/AGENTS.md +14 -0
- package/skills/ai-delivery-orchestrate/assets/codebase-template/CODEBASE_GUIDE.md +124 -0
- package/skills/ai-delivery-orchestrate/assets/codebase-template/docs/CODEBASE_INDEX.md +87 -0
- package/skills/ai-delivery-orchestrate/assets/communication-template/confirmation-examples.yaml +67 -0
- package/skills/ai-delivery-orchestrate/assets/communication-template/confirmation-summary.yaml +42 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/iterations/ITER-0001/02-solution-design/product-impact-assessment.yaml +42 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/hooks/hook-event.mjs +2 -8
- package/skills/ai-delivery-orchestrate/assets/project-template/AGENTS.md +4 -5
- package/skills/ai-delivery-orchestrate/references/codebase-document-contract.md +91 -0
- package/skills/ai-delivery-orchestrate/references/communication-confirmation-contract.md +52 -0
- package/skills/ai-delivery-orchestrate/references/formal-state-contract.md +11 -20
- package/skills/ai-delivery-orchestrate/references/product-impact-assessment-contract.md +87 -0
- package/skills/ai-delivery-orchestrate/references/workflow-model.md +1 -3
- package/skills/ai-delivery-orchestrate/scripts/confirmation-state.mjs +30 -0
- package/skills/ai-delivery-orchestrate-release/SKILL.md +3 -1
- package/skills/ai-delivery-plan-iteration/SKILL.md +6 -1
- package/skills/ai-delivery-prepare-platform/SKILL.md +1 -1
- package/skills/ai-delivery-prepare-release/SKILL.md +2 -0
- package/skills/ai-delivery-prepare-release/assets/release-template/release-manifest.yaml +0 -2
- package/skills/ai-delivery-review-change/SKILL.md +3 -1
- package/skills/ai-delivery-validate-artifacts/SKILL.md +10 -7
- package/skills/ai-delivery-validate-artifacts/references/artifact-contract.md +2 -1
- package/skills/ai-delivery-verify-candidate/SKILL.md +5 -2
- package/skills/ai-delivery-verify-candidate/references/candidate-verification-contract.md +1 -1
- package/docs/PROTOTYPE-MIGRATION.zh-CN.md +0 -164
- package/docs/VIEWER-MAINTENANCE.zh-CN.md +0 -150
- package/docs/VIEWER-USER-GUIDE.zh-CN.md +0 -113
- package/lib/project-migration.mjs +0 -1193
- package/lib/prototype-migration.mjs +0 -713
- package/skills/ai-delivery-design-experience/assets/experience-template/LEGACY-COMPATIBILITY.md +0 -16
- package/skills/ai-delivery-design-experience/assets/experience-template/canvas-catalog.csv +0 -1
- package/skills/ai-delivery-design-experience/assets/experience-template/canvas-pages.csv +0 -1
- package/skills/ai-delivery-design-experience/assets/experience-template/page-catalog.csv +0 -1
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-file-terminals.csv +0 -1
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-files.csv +0 -1
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-manifest.yaml +0 -76
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-set.yaml +0 -24
package/lib/delivery-state.mjs
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import zlib from "node:zlib";
|
|
5
4
|
import {
|
|
6
5
|
historyAnchorId,
|
|
7
6
|
readVerifiedHistoryAnchor,
|
|
8
7
|
structuredDocumentChecksum,
|
|
9
8
|
} from "./history-anchor.mjs";
|
|
10
|
-
import {
|
|
9
|
+
import { atomicWrite } from "./fs-utils.mjs";
|
|
11
10
|
|
|
12
11
|
const SEVEN_NODE_DEVELOPMENT_IDS = [
|
|
13
12
|
"00-bootstrap",
|
|
@@ -18,27 +17,6 @@ const SEVEN_NODE_DEVELOPMENT_IDS = [
|
|
|
18
17
|
"05-candidate-assurance",
|
|
19
18
|
"06-version-closeout",
|
|
20
19
|
];
|
|
21
|
-
const LEGACY_DEVELOPMENT_IDS = [
|
|
22
|
-
"01-product-discovery",
|
|
23
|
-
"02-product-definition",
|
|
24
|
-
"03-architecture-initial",
|
|
25
|
-
"04-experience-design",
|
|
26
|
-
"05-architecture-reconcile",
|
|
27
|
-
"06-platform-preparation",
|
|
28
|
-
"07-iteration-planning",
|
|
29
|
-
"08-test-design",
|
|
30
|
-
"09-development",
|
|
31
|
-
"10-change-review",
|
|
32
|
-
"11-candidate-verification",
|
|
33
|
-
"12-version-closeout",
|
|
34
|
-
];
|
|
35
|
-
const LEGACY_PRODUCTION_IDS = [
|
|
36
|
-
"12-release-preparation",
|
|
37
|
-
"13-production-approval",
|
|
38
|
-
"14-production-deployment",
|
|
39
|
-
"15-production-verification",
|
|
40
|
-
"16-version-archive",
|
|
41
|
-
];
|
|
42
20
|
const RELEASE_NODE_IDS = [
|
|
43
21
|
"R00-release-request",
|
|
44
22
|
"R01-version-selection",
|
|
@@ -54,12 +32,9 @@ const RELEASE_NODE_IDS = [
|
|
|
54
32
|
];
|
|
55
33
|
const NODE_IDS = new Set([
|
|
56
34
|
...SEVEN_NODE_DEVELOPMENT_IDS,
|
|
57
|
-
...LEGACY_DEVELOPMENT_IDS,
|
|
58
|
-
...LEGACY_PRODUCTION_IDS,
|
|
59
35
|
...RELEASE_NODE_IDS,
|
|
60
36
|
]);
|
|
61
37
|
const SEVEN_NODE_DEVELOPMENT_ID_SET = new Set(SEVEN_NODE_DEVELOPMENT_IDS);
|
|
62
|
-
const LEGACY_DEVELOPMENT_ID_SET = new Set(LEGACY_DEVELOPMENT_IDS);
|
|
63
38
|
const RELEASE_NODE_ID_SET = new Set(RELEASE_NODE_IDS);
|
|
64
39
|
const ARTIFACT_STATUSES = new Set([
|
|
65
40
|
"draft",
|
|
@@ -86,10 +61,9 @@ const ARTIFACT_STATUSES = new Set([
|
|
|
86
61
|
]);
|
|
87
62
|
const WORKING_DRAFT_STATUSES = new Set(["draft", "review-draft"]);
|
|
88
63
|
const GATE_DECISIONS = new Set(["approved", "rejected", "blocked", "not-required"]);
|
|
89
|
-
const UX_GATE_IDS = new Set(["UX-
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
const PRODUCT_TERMINAL_NODES = new Set(["01-product-shaping", "02-product-definition"]);
|
|
64
|
+
const UX_GATE_IDS = new Set(["UX-VS", "UX-UI"]);
|
|
65
|
+
const UX_GATE_NODES = new Set(["02-solution-design"]);
|
|
66
|
+
const PRODUCT_TERMINAL_NODES = new Set(["01-product-shaping"]);
|
|
93
67
|
const SUCCESSFUL_ARTIFACT_STATUSES = new Set([
|
|
94
68
|
"approved", "valid", "frozen", "immutable", "passed", "deployed", "released",
|
|
95
69
|
"release-ready", "included-in-release", "archived",
|
|
@@ -102,18 +76,6 @@ const NODE_RULES = new Map([
|
|
|
102
76
|
["04-implementation", { scope: "development", prerequisites: ["03-delivery-readiness"], gates: [] }],
|
|
103
77
|
["05-candidate-assurance", { scope: "development", prerequisites: ["04-implementation"], gates: [] }],
|
|
104
78
|
["06-version-closeout", { scope: "development", prerequisites: ["05-candidate-assurance"], gates: [] }],
|
|
105
|
-
["01-product-discovery", { scope: "development", prerequisites: ["00-bootstrap"], gates: ["GATE-A"] }],
|
|
106
|
-
["02-product-definition", { scope: "development", prerequisites: ["01-product-discovery"], gates: ["GATE-B"] }],
|
|
107
|
-
["03-architecture-initial", { scope: "development", prerequisites: ["02-product-definition"], gates: [] }],
|
|
108
|
-
["04-experience-design", { scope: "development", prerequisites: ["03-architecture-initial"], gates: ["UX-LF", "UX-VS", "UX-UI"] }],
|
|
109
|
-
["05-architecture-reconcile", { scope: "development", prerequisites: ["04-experience-design"], gates: [] }],
|
|
110
|
-
["06-platform-preparation", { scope: "development", prerequisites: ["03-architecture-initial"], gates: [] }],
|
|
111
|
-
["07-iteration-planning", { scope: "development", prerequisites: ["05-architecture-reconcile", "06-platform-preparation"], gates: [] }],
|
|
112
|
-
["08-test-design", { scope: "development", prerequisites: ["07-iteration-planning"], gates: [] }],
|
|
113
|
-
["09-development", { scope: "development", prerequisites: ["08-test-design"], gates: [] }],
|
|
114
|
-
["10-change-review", { scope: "development", prerequisites: ["09-development"], gates: [] }],
|
|
115
|
-
["11-candidate-verification", { scope: "development", prerequisites: ["10-change-review"], gates: [] }],
|
|
116
|
-
["12-version-closeout", { scope: "development", prerequisites: ["11-candidate-verification"], gates: [] }],
|
|
117
79
|
["R00-release-request", { scope: "release", prerequisites: [], gates: [] }],
|
|
118
80
|
["R01-version-selection", { scope: "release", prerequisites: ["R00-release-request"], gates: [] }],
|
|
119
81
|
["R02-release-eligibility", { scope: "release", prerequisites: ["R01-version-selection"], gates: [] }],
|
|
@@ -129,15 +91,12 @@ const NODE_RULES = new Map([
|
|
|
129
91
|
const WRITABLE_NODE_IDS = new Set([...SEVEN_NODE_DEVELOPMENT_IDS, ...RELEASE_NODE_IDS]);
|
|
130
92
|
const SCOPED_COMPLETION_NODES = new Set([
|
|
131
93
|
"03-delivery-readiness", "04-implementation", "05-candidate-assurance", "06-version-closeout",
|
|
132
|
-
"07-iteration-planning", "08-test-design", "09-development",
|
|
133
|
-
"10-change-review", "11-candidate-verification", "12-version-closeout",
|
|
134
94
|
]);
|
|
135
95
|
const CANDIDATE_REQUIRED_NODES = new Set([
|
|
136
96
|
"04-implementation", "05-candidate-assurance", "06-version-closeout",
|
|
137
|
-
"11-candidate-verification", "12-version-closeout",
|
|
138
97
|
]);
|
|
139
98
|
const CANDIDATE_BOUND_NODES = new Set(["05-candidate-assurance", "06-version-closeout"]);
|
|
140
|
-
const SCOPE_FREEZE_NODES = new Set(["03-delivery-readiness"
|
|
99
|
+
const SCOPE_FREEZE_NODES = new Set(["03-delivery-readiness"]);
|
|
141
100
|
const SLICE_BUDGET_LIMITS = new Map([
|
|
142
101
|
["logical-tasks", 8],
|
|
143
102
|
["active-hours", 8],
|
|
@@ -170,7 +129,7 @@ function parseArgs(argv) {
|
|
|
170
129
|
}
|
|
171
130
|
|
|
172
131
|
function lifecycleDescriptor(command, positional, flags) {
|
|
173
|
-
if (command === "artifact" &&
|
|
132
|
+
if (command === "artifact" && positional[1] === "register") {
|
|
174
133
|
return {
|
|
175
134
|
kind: "artifact",
|
|
176
135
|
action: `artifact-${positional[1]}`,
|
|
@@ -202,26 +161,12 @@ function lifecycleDescriptor(command, positional, flags) {
|
|
|
202
161
|
};
|
|
203
162
|
}
|
|
204
163
|
|
|
205
|
-
function
|
|
206
|
-
if (
|
|
207
|
-
throw new Error(`Legacy workflow node is read-only: ${node}`);
|
|
208
|
-
}
|
|
164
|
+
function assertWritableNode(node) {
|
|
165
|
+
if (!WRITABLE_NODE_IDS.has(node)) throw new Error(`Unsupported workflow node: ${node}`);
|
|
209
166
|
return node;
|
|
210
167
|
}
|
|
211
168
|
|
|
212
|
-
function
|
|
213
|
-
if (RETIRED_GATE_IDS.has(gateId)) {
|
|
214
|
-
throw new Error(`${gateId} Gate contract is retired; historical records remain read-only.`);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function assertRetiredGateContract(gateId) {
|
|
219
|
-
if (!RETIRED_GATE_IDS.has(gateId)) {
|
|
220
|
-
throw new Error(`Gate contract is not retired: ${gateId}.`);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function assertNoLegacyDevelopmentWrite(files, command, positional, flags, options) {
|
|
169
|
+
function assertCurrentNodeInputs(files, command, positional, flags, options) {
|
|
225
170
|
let node = null;
|
|
226
171
|
if ((command === "artifact" && positional[1] === "register")
|
|
227
172
|
|| (command === "gate" && positional[1] === "request")) {
|
|
@@ -250,7 +195,7 @@ function assertNoLegacyDevelopmentWrite(files, command, positional, flags, optio
|
|
|
250
195
|
}
|
|
251
196
|
}
|
|
252
197
|
}
|
|
253
|
-
|
|
198
|
+
if (node) assertWritableNode(node);
|
|
254
199
|
}
|
|
255
200
|
|
|
256
201
|
function dispatchLifecycle(options, files, descriptor, phase, flags, result = null, error = null) {
|
|
@@ -484,9 +429,7 @@ function statePaths(projectRoot) {
|
|
|
484
429
|
events: path.join(delivery, "state-events.jsonl"),
|
|
485
430
|
eventShards: path.join(workflow, "control", "events"),
|
|
486
431
|
historySnapshots: path.join(workflow, "control", "snapshots", "formal-state"),
|
|
487
|
-
artifactHistoryArchives: path.join(workflow, "control", "archives", "history", "artifact-registry"),
|
|
488
432
|
artifactRetention: path.join(workflow, "control", "retention", "artifact-registry"),
|
|
489
|
-
compactRuntime: path.join(delivery, "runtime", "formal-state-compact"),
|
|
490
433
|
pruneRuntime: path.join(delivery, "runtime", "formal-state-prune"),
|
|
491
434
|
lockRoot: path.join(delivery, "runtime", "formal-state-locks"),
|
|
492
435
|
transactionJournal: path.join(delivery, "runtime", "formal-state-locks", "formal-state-transaction.json"),
|
|
@@ -517,9 +460,6 @@ function normalizeScope(value, scopeId = null) {
|
|
|
517
460
|
node_completion_receipts: Array.isArray(value?.node_completion_receipts)
|
|
518
461
|
? value.node_completion_receipts
|
|
519
462
|
: [],
|
|
520
|
-
contract_retirement_receipts: Array.isArray(value?.contract_retirement_receipts)
|
|
521
|
-
? value.contract_retirement_receipts
|
|
522
|
-
: [],
|
|
523
463
|
latest_candidate_id: value?.latest_candidate_id ?? null,
|
|
524
464
|
latest_release_id: value?.latest_release_id ?? null,
|
|
525
465
|
updated_at: value?.updated_at ?? "",
|
|
@@ -545,67 +485,30 @@ export function findIncompatibleNodeStates(scope) {
|
|
|
545
485
|
.map(([node, states]) => ({ node, states: [...states] }));
|
|
546
486
|
}
|
|
547
487
|
|
|
548
|
-
function exposeLegacyScopeAliases(state) {
|
|
549
|
-
const preferred = state.last_transition?.scope === "release"
|
|
550
|
-
? state.release_state
|
|
551
|
-
: state.development_state.scope_id ? state.development_state : state.release_state;
|
|
552
|
-
const aliases = {
|
|
553
|
-
iteration_id: preferred.scope_id,
|
|
554
|
-
active_nodes: preferred.active_nodes,
|
|
555
|
-
blocked_nodes: preferred.blocked_nodes,
|
|
556
|
-
completed_nodes: preferred.completed_nodes,
|
|
557
|
-
pending_gates: preferred.pending_gates,
|
|
558
|
-
gate_decisions: preferred.gate_decisions,
|
|
559
|
-
latest_candidate_id: state.development_state.latest_candidate_id
|
|
560
|
-
|| state.release_state.latest_candidate_id,
|
|
561
|
-
latest_release_id: state.release_state.latest_release_id,
|
|
562
|
-
};
|
|
563
|
-
for (const [key, alias] of Object.entries(aliases)) {
|
|
564
|
-
Object.defineProperty(state, key, { value: alias, enumerable: false, configurable: true });
|
|
565
|
-
}
|
|
566
|
-
return state;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
488
|
function normalizeWorkflowState(value) {
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
||
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
active_nodes: value.active_nodes,
|
|
581
|
-
blocked_nodes: value.blocked_nodes,
|
|
582
|
-
completed_nodes: value.completed_nodes,
|
|
583
|
-
pending_gates: value.pending_gates,
|
|
584
|
-
gate_decisions: value.gate_decisions,
|
|
585
|
-
latest_candidate_id: value.latest_candidate_id,
|
|
586
|
-
latest_release_id: value.latest_release_id,
|
|
587
|
-
updated_at: value.updated_at,
|
|
588
|
-
last_transition: value.last_transition,
|
|
589
|
-
});
|
|
489
|
+
if (value?.schema_version !== 2) {
|
|
490
|
+
throw new Error(`Formal workflow state schema ${value?.schema_version ?? "missing"} is unsupported; expected schema 2.`);
|
|
491
|
+
}
|
|
492
|
+
if (!value.development_state || !value.release_state
|
|
493
|
+
|| Object.prototype.hasOwnProperty.call(value, "iteration_id")
|
|
494
|
+
|| Object.prototype.hasOwnProperty.call(value, "active_nodes")
|
|
495
|
+
|| Object.prototype.hasOwnProperty.call(value, "blocked_nodes")
|
|
496
|
+
|| Object.prototype.hasOwnProperty.call(value, "completed_nodes")) {
|
|
497
|
+
throw new Error("Formal workflow state must use separate development_state and release_state scopes.");
|
|
498
|
+
}
|
|
590
499
|
const state = {
|
|
591
|
-
schema_version:
|
|
592
|
-
revision: value.revision
|
|
500
|
+
schema_version: 2,
|
|
501
|
+
revision: value.revision,
|
|
593
502
|
project_id: value.project_id ?? null,
|
|
594
|
-
development_state: normalizeScope(
|
|
595
|
-
|
|
596
|
-
!legacyRelease ? value.iteration_id ?? null : null,
|
|
597
|
-
),
|
|
598
|
-
release_state: normalizeScope(
|
|
599
|
-
value.release_state || (legacyRelease ? legacyScope : null),
|
|
600
|
-
legacyRelease ? value.iteration_id ?? null : null,
|
|
601
|
-
),
|
|
503
|
+
development_state: normalizeScope(value.development_state),
|
|
504
|
+
release_state: normalizeScope(value.release_state),
|
|
602
505
|
latest_line_version_id: value.latest_line_version_id ?? null,
|
|
603
506
|
production_version_id: value.production_version_id ?? null,
|
|
604
507
|
production_release_id: value.production_release_id ?? null,
|
|
605
508
|
updated_at: value.updated_at ?? "",
|
|
606
509
|
last_transition: value.last_transition ?? null,
|
|
607
510
|
};
|
|
608
|
-
return
|
|
511
|
+
return state;
|
|
609
512
|
}
|
|
610
513
|
|
|
611
514
|
function scopeNameForNode(node) {
|
|
@@ -660,10 +563,6 @@ function completionReceiptChecksum(receipt) {
|
|
|
660
563
|
return checksumWithoutField(receipt, "receipt_checksum");
|
|
661
564
|
}
|
|
662
565
|
|
|
663
|
-
function contractRetirementReceiptChecksum(receipt) {
|
|
664
|
-
return checksumWithoutField(receipt, "receipt_checksum");
|
|
665
|
-
}
|
|
666
|
-
|
|
667
566
|
function currentScopeManifest(scope) {
|
|
668
567
|
if (!scope.current_scope_manifest_id) return null;
|
|
669
568
|
return scope.scope_manifests.find((manifest) => manifest?.manifest_id === scope.current_scope_manifest_id) || null;
|
|
@@ -895,7 +794,7 @@ const FORMAL_STREAMS = new Map([
|
|
|
895
794
|
viewFile: (files) => files.workflowState,
|
|
896
795
|
normalize: normalizeWorkflowState,
|
|
897
796
|
applyOperations: applyWorkflowStateOperations,
|
|
898
|
-
allowedSchemas: new Set([
|
|
797
|
+
allowedSchemas: new Set([2]),
|
|
899
798
|
}],
|
|
900
799
|
["artifact-registry", {
|
|
901
800
|
snapshotFile: "artifact-registry.yaml",
|
|
@@ -1225,14 +1124,15 @@ function prepareStreamMutation(files, mutation, options, transactionId) {
|
|
|
1225
1124
|
...event,
|
|
1226
1125
|
after_checksum: sha256(serialized),
|
|
1227
1126
|
};
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
:
|
|
1127
|
+
if (!transaction || transaction.schema_version !== 2 || !Array.isArray(transaction.operations)) {
|
|
1128
|
+
throw new Error(`Formal ${stream} mutations require a schema 2 delta transaction.`);
|
|
1129
|
+
}
|
|
1130
|
+
const shardRecord = {
|
|
1131
|
+
...eventRecord,
|
|
1132
|
+
transaction_id: transactionId,
|
|
1133
|
+
...transaction,
|
|
1134
|
+
previous_checksum: previousStreamChecksum(files, stream, transaction.before_revision, options),
|
|
1135
|
+
};
|
|
1236
1136
|
const record = {
|
|
1237
1137
|
...shardRecord,
|
|
1238
1138
|
record_checksum: unsignedRecordChecksum(shardRecord, options.yaml),
|
|
@@ -1365,9 +1265,7 @@ function replayStreamRecords(stream, records, options, base = {}) {
|
|
|
1365
1265
|
if (record.revision !== previousRevision + 1) {
|
|
1366
1266
|
throw new Error(`Formal event shard revision gap: ${stream}/${record.revision}; expected ${previousRevision + 1}.`);
|
|
1367
1267
|
}
|
|
1368
|
-
if (record.
|
|
1369
|
-
value = record.after_value;
|
|
1370
|
-
} else if (record.schema_version === 2 && Array.isArray(record.operations)) {
|
|
1268
|
+
if (record.schema_version === 2 && Array.isArray(record.operations)) {
|
|
1371
1269
|
if (record.before_revision !== record.revision - 1 || record.after_revision !== record.revision) {
|
|
1372
1270
|
throw new Error(`Formal event shard revision metadata mismatch: ${stream}/${record.revision}.`);
|
|
1373
1271
|
}
|
|
@@ -1398,7 +1296,7 @@ function replayStreamRecords(stream, records, options, base = {}) {
|
|
|
1398
1296
|
function validateEventShardRecord(record, stream, name) {
|
|
1399
1297
|
const expectedName = `${String(record.revision).padStart(6, "0")}-${record.event_id}.yaml`;
|
|
1400
1298
|
if (record.stream !== stream || !Number.isInteger(record.revision) || !record.event_id
|
|
1401
|
-
||
|
|
1299
|
+
|| record.schema_version !== 2 || !Array.isArray(record.operations)) {
|
|
1402
1300
|
throw new Error(`Formal event shard is invalid: ${stream}/${name}`);
|
|
1403
1301
|
}
|
|
1404
1302
|
return record;
|
|
@@ -1720,8 +1618,7 @@ function createFormalSnapshot(files, flags, options) {
|
|
|
1720
1618
|
function rebuildFormalViews(files, options) {
|
|
1721
1619
|
return withLock(files, "formal-state-rebuild", () => {
|
|
1722
1620
|
const retentionReceipts = readArtifactRetentionReceipts(files, options);
|
|
1723
|
-
|
|
1724
|
-
if (retentionReceipts.length || historyArchives.length) {
|
|
1621
|
+
if (retentionReceipts.length) {
|
|
1725
1622
|
const trusted = readLatestHistoryAnchor(files, options);
|
|
1726
1623
|
if (!trusted) throw new Error("Retained history rebuild requires a trusted history anchor.");
|
|
1727
1624
|
const errors = [];
|
|
@@ -1769,17 +1666,13 @@ function rebuildFormalViews(files, options) {
|
|
|
1769
1666
|
}
|
|
1770
1667
|
const records = readEventShards(files, options);
|
|
1771
1668
|
if (!records.length) {
|
|
1772
|
-
return { command: "rebuild", rebuilt: false, event_shard_count: 0, message: "No formal event shards exist;
|
|
1669
|
+
return { command: "rebuild", rebuilt: false, event_shard_count: 0, message: "No formal event shards exist; current views were left unchanged." };
|
|
1773
1670
|
}
|
|
1774
1671
|
const workflowRecords = records.filter((record) => record.stream === "workflow-state");
|
|
1775
1672
|
const registryRecords = records.filter((record) => record.stream === "artifact-registry");
|
|
1776
1673
|
const workflow = workflowRecords.length ? replayStreamRecords("workflow-state", workflowRecords, options) : null;
|
|
1777
1674
|
const registry = registryRecords.length ? replayStreamRecords("artifact-registry", registryRecords, options) : null;
|
|
1778
|
-
const index = records.map((record) =>
|
|
1779
|
-
const { after_value: ignored, ...indexed } = record;
|
|
1780
|
-
if (record.after_value) delete indexed.record_checksum;
|
|
1781
|
-
return JSON.stringify(indexed);
|
|
1782
|
-
}).join("\n");
|
|
1675
|
+
const index = records.map((record) => JSON.stringify(record)).join("\n");
|
|
1783
1676
|
const writes = [{ file: files.events, content: `${index}\n` }];
|
|
1784
1677
|
if (workflow) writes.unshift({ file: files.workflowState, content: options.yaml.stringify(workflow) });
|
|
1785
1678
|
if (registry) writes.unshift({ file: files.artifactRegistry, content: options.yaml.stringify(registry) });
|
|
@@ -1848,7 +1741,7 @@ function registerArtifact(files, flags, options) {
|
|
|
1848
1741
|
}
|
|
1849
1742
|
}
|
|
1850
1743
|
const node = requiredFlag(flags, "--node");
|
|
1851
|
-
|
|
1744
|
+
assertWritableNode(node);
|
|
1852
1745
|
if (!NODE_IDS.has(node)) throw new Error(`Unknown workflow node: ${node}`);
|
|
1853
1746
|
const artifactScope = scopeNameForNode(node);
|
|
1854
1747
|
const artifactScopeState = scopeState(workflowState, artifactScope);
|
|
@@ -1921,63 +1814,6 @@ function registerArtifact(files, flags, options) {
|
|
|
1921
1814
|
}, { waitForActive: true });
|
|
1922
1815
|
}
|
|
1923
1816
|
|
|
1924
|
-
function changeArtifactStatus(files, flags, options) {
|
|
1925
|
-
const expected = expectedRevision(flags);
|
|
1926
|
-
return withLock(files, "artifact-registry", () => {
|
|
1927
|
-
const registry = normalizeArtifactRegistry(
|
|
1928
|
-
readYaml(files.artifactRegistry, options.yaml, "artifact-registry.yaml"),
|
|
1929
|
-
);
|
|
1930
|
-
if (registry.revision !== expected) {
|
|
1931
|
-
throw new Error(`Artifact registry revision conflict: expected ${expected}, actual ${registry.revision}.`);
|
|
1932
|
-
}
|
|
1933
|
-
const artifactId = requiredFlag(flags, "--id");
|
|
1934
|
-
const artifact = registry.artifacts.find((item) => (item.artifact_id || item.id) === artifactId);
|
|
1935
|
-
if (!artifact) throw new Error(`Artifact is not registered: ${artifactId}`);
|
|
1936
|
-
const status = artifactStatus(flags);
|
|
1937
|
-
const evidence = repeatedFlag(flags, "--evidence");
|
|
1938
|
-
if (!evidence.length) throw new Error("artifact status requires at least one --evidence reference.");
|
|
1939
|
-
if (artifact.status === status) throw new Error(`Artifact already has status ${status}: ${artifactId}`);
|
|
1940
|
-
if (!WORKING_DRAFT_STATUSES.has(artifact.status)) {
|
|
1941
|
-
throw new Error(`Immutable artifact revision cannot change status: ${artifact.status}`);
|
|
1942
|
-
}
|
|
1943
|
-
if (WORKING_DRAFT_STATUSES.has(status)) {
|
|
1944
|
-
throw new Error("Legacy draft migration must target an immutable semantic status.");
|
|
1945
|
-
}
|
|
1946
|
-
|
|
1947
|
-
const timestamp = (options.now || (() => new Date().toISOString()))();
|
|
1948
|
-
const previous = artifact.status || "unknown";
|
|
1949
|
-
artifact.status = status;
|
|
1950
|
-
artifact.status_history = [
|
|
1951
|
-
...(Array.isArray(artifact.status_history) ? artifact.status_history : []),
|
|
1952
|
-
{ from: previous, to: status, evidence, changed_at: timestamp },
|
|
1953
|
-
];
|
|
1954
|
-
registry.schema_version = 1;
|
|
1955
|
-
registry.revision += 1;
|
|
1956
|
-
registry.updated_at = timestamp;
|
|
1957
|
-
const event = {
|
|
1958
|
-
type: "artifact-status-changed",
|
|
1959
|
-
occurred_at: timestamp,
|
|
1960
|
-
artifact_id: artifactId,
|
|
1961
|
-
from: previous,
|
|
1962
|
-
to: status,
|
|
1963
|
-
evidence,
|
|
1964
|
-
};
|
|
1965
|
-
commitFormalTransaction(files, [{
|
|
1966
|
-
stream: "artifact-registry",
|
|
1967
|
-
file: files.artifactRegistry,
|
|
1968
|
-
value: registry,
|
|
1969
|
-
event,
|
|
1970
|
-
transaction: artifactRegistryTransaction(event, registry, expected, [artifact]),
|
|
1971
|
-
}], [], options);
|
|
1972
|
-
return {
|
|
1973
|
-
command: "artifact status",
|
|
1974
|
-
revision: registry.revision,
|
|
1975
|
-
artifact,
|
|
1976
|
-
artifact_registry: registry,
|
|
1977
|
-
};
|
|
1978
|
-
}, { waitForActive: true });
|
|
1979
|
-
}
|
|
1980
|
-
|
|
1981
1817
|
function recoverArtifactPrune(files, apply, options) {
|
|
1982
1818
|
if (!fs.existsSync(files.pruneRuntime)) return 0;
|
|
1983
1819
|
const pending = fs.readdirSync(files.pruneRuntime, { withFileTypes: true })
|
|
@@ -2039,490 +1875,6 @@ function recoverArtifactPrune(files, apply, options) {
|
|
|
2039
1875
|
return recovered;
|
|
2040
1876
|
}
|
|
2041
1877
|
|
|
2042
|
-
function legacyArtifactHistoryArchive(files, trusted, options) {
|
|
2043
|
-
verifyFormalState(files, options, { history: true });
|
|
2044
|
-
const anchoredRevision = trusted.anchor.streams["artifact-registry"].revision;
|
|
2045
|
-
const shardDirectory = path.join(files.eventShards, "artifact-registry");
|
|
2046
|
-
const legacyShards = fs.existsSync(shardDirectory)
|
|
2047
|
-
? fs.readdirSync(shardDirectory)
|
|
2048
|
-
.filter((name) => /\.ya?ml$/i.test(name))
|
|
2049
|
-
.map((name) => ({
|
|
2050
|
-
name,
|
|
2051
|
-
file: path.join(shardDirectory, name),
|
|
2052
|
-
record: readEventShardRecord(files, options, "artifact-registry", name),
|
|
2053
|
-
}))
|
|
2054
|
-
.filter(({ record }) => record.after_value && record.revision <= anchoredRevision)
|
|
2055
|
-
.sort((left, right) => left.record.revision - right.record.revision)
|
|
2056
|
-
: [];
|
|
2057
|
-
if (!legacyShards.length) {
|
|
2058
|
-
throw new Error("Legacy artifact history compact requires anchored full-snapshot shards.");
|
|
2059
|
-
}
|
|
2060
|
-
const replayed = replayStreamRecords(
|
|
2061
|
-
"artifact-registry",
|
|
2062
|
-
legacyShards.map(({ record }) => record),
|
|
2063
|
-
options,
|
|
2064
|
-
);
|
|
2065
|
-
const anchoredRegistry = trusted.snapshots["artifact-registry"];
|
|
2066
|
-
if (JSON.stringify(stableValue(normalizeArtifactRegistry(replayed)))
|
|
2067
|
-
!== JSON.stringify(stableValue(normalizeArtifactRegistry(anchoredRegistry)))) {
|
|
2068
|
-
throw new Error("Legacy artifact history replay does not reproduce the anchored registry.");
|
|
2069
|
-
}
|
|
2070
|
-
const finalRecord = legacyShards.at(-1).record;
|
|
2071
|
-
const anchoredChecksum = trusted.anchor.streams["artifact-registry"].view_checksum;
|
|
2072
|
-
if (finalRecord.revision !== anchoredRevision || finalRecord.after_checksum !== anchoredChecksum) {
|
|
2073
|
-
throw new Error("Legacy artifact history replay does not reproduce the anchored registry checksum.");
|
|
2074
|
-
}
|
|
2075
|
-
|
|
2076
|
-
const digest = crypto.createHash("sha256");
|
|
2077
|
-
const entries = legacyShards.map(({ name, file, record }) => {
|
|
2078
|
-
const content = fs.readFileSync(file);
|
|
2079
|
-
digest.update(name);
|
|
2080
|
-
digest.update("\0");
|
|
2081
|
-
digest.update(content);
|
|
2082
|
-
digest.update("\0");
|
|
2083
|
-
return {
|
|
2084
|
-
path: name,
|
|
2085
|
-
revision: record.revision,
|
|
2086
|
-
size_bytes: content.length,
|
|
2087
|
-
sha256: sha256(content),
|
|
2088
|
-
content_base64: content.toString("base64"),
|
|
2089
|
-
};
|
|
2090
|
-
});
|
|
2091
|
-
const sourceHistoryRootDigest = digest.digest("hex");
|
|
2092
|
-
const coverage = {
|
|
2093
|
-
first_revision: legacyShards[0].record.revision,
|
|
2094
|
-
last_revision: finalRecord.revision,
|
|
2095
|
-
event_count: legacyShards.length,
|
|
2096
|
-
};
|
|
2097
|
-
const archiveId = trusted.anchor.anchor_id;
|
|
2098
|
-
const bundle = stableValue({
|
|
2099
|
-
schema_version: 1,
|
|
2100
|
-
archive_id: archiveId,
|
|
2101
|
-
stream: "artifact-registry",
|
|
2102
|
-
source_root: portableReference(files, shardDirectory),
|
|
2103
|
-
source_history_root_digest: sourceHistoryRootDigest,
|
|
2104
|
-
source_history_root_digest_algorithm: "sha256-path-null-bytes-null-v1",
|
|
2105
|
-
coverage,
|
|
2106
|
-
anchor: {
|
|
2107
|
-
anchor_id: trusted.anchor.anchor_id,
|
|
2108
|
-
anchor_checksum: trusted.anchor.anchor_checksum,
|
|
2109
|
-
registry_revision: anchoredRevision,
|
|
2110
|
-
registry_checksum: anchoredChecksum,
|
|
2111
|
-
},
|
|
2112
|
-
entries,
|
|
2113
|
-
});
|
|
2114
|
-
const archiveBytes = zlib.gzipSync(Buffer.from(`${JSON.stringify(bundle, null, 2)}\n`), {
|
|
2115
|
-
level: 9,
|
|
2116
|
-
mtime: 0,
|
|
2117
|
-
});
|
|
2118
|
-
const archiveFile = path.join(files.artifactHistoryArchives, `${archiveId}.json.gz`);
|
|
2119
|
-
const manifestFile = path.join(files.artifactHistoryArchives, `${archiveId}.yaml`);
|
|
2120
|
-
const looseBytes = entries.reduce((total, entry) => total + entry.size_bytes, 0);
|
|
2121
|
-
const compactedAt = (options.now || (() => new Date().toISOString()))();
|
|
2122
|
-
const manifestBase = stableValue({
|
|
2123
|
-
schema_version: 1,
|
|
2124
|
-
archive_id: archiveId,
|
|
2125
|
-
stream: "artifact-registry",
|
|
2126
|
-
anchor_id: trusted.anchor.anchor_id,
|
|
2127
|
-
anchor_checksum: trusted.anchor.anchor_checksum,
|
|
2128
|
-
registry_revision: anchoredRevision,
|
|
2129
|
-
registry_checksum: anchoredChecksum,
|
|
2130
|
-
source_root: bundle.source_root,
|
|
2131
|
-
source_history_root_digest: sourceHistoryRootDigest,
|
|
2132
|
-
source_history_root_digest_algorithm: bundle.source_history_root_digest_algorithm,
|
|
2133
|
-
coverage,
|
|
2134
|
-
loose_file_count: entries.length,
|
|
2135
|
-
loose_bytes: looseBytes,
|
|
2136
|
-
archive_format: "gzip-json-v1",
|
|
2137
|
-
archive_path: portableReference(files, archiveFile),
|
|
2138
|
-
archive_checksum: sha256(archiveBytes),
|
|
2139
|
-
compressed_bundle_bytes: archiveBytes.length,
|
|
2140
|
-
compacted_at: compactedAt,
|
|
2141
|
-
});
|
|
2142
|
-
const manifest = {
|
|
2143
|
-
...manifestBase,
|
|
2144
|
-
manifest_checksum: sha256(options.yaml.stringify(manifestBase)),
|
|
2145
|
-
};
|
|
2146
|
-
const manifestContent = options.yaml.stringify(stableValue(manifest));
|
|
2147
|
-
const archiveStorageBytes = archiveBytes.length + Buffer.byteLength(manifestContent);
|
|
2148
|
-
return {
|
|
2149
|
-
bundle,
|
|
2150
|
-
archiveBytes,
|
|
2151
|
-
archiveFile,
|
|
2152
|
-
entries,
|
|
2153
|
-
manifest,
|
|
2154
|
-
manifestContent,
|
|
2155
|
-
manifestFile,
|
|
2156
|
-
report: {
|
|
2157
|
-
command: "compact",
|
|
2158
|
-
anchor_id: trusted.anchor.anchor_id,
|
|
2159
|
-
anchor_checksum: trusted.anchor.anchor_checksum,
|
|
2160
|
-
source_history_root_digest: sourceHistoryRootDigest,
|
|
2161
|
-
coverage,
|
|
2162
|
-
loose_file_count: entries.length,
|
|
2163
|
-
loose_bytes: looseBytes,
|
|
2164
|
-
compressed_bundle_bytes: archiveBytes.length,
|
|
2165
|
-
manifest_bytes: Buffer.byteLength(manifestContent),
|
|
2166
|
-
archive_bytes: archiveStorageBytes,
|
|
2167
|
-
expected_freed_bytes: looseBytes - archiveStorageBytes,
|
|
2168
|
-
archive_path: portableReference(files, archiveFile),
|
|
2169
|
-
manifest_path: portableReference(files, manifestFile),
|
|
2170
|
-
},
|
|
2171
|
-
};
|
|
2172
|
-
}
|
|
2173
|
-
|
|
2174
|
-
function readArtifactHistoryArchives(files, options) {
|
|
2175
|
-
assertPathInside(files.root, files.artifactHistoryArchives);
|
|
2176
|
-
if (!fs.existsSync(files.artifactHistoryArchives)) return [];
|
|
2177
|
-
return fs.readdirSync(files.artifactHistoryArchives)
|
|
2178
|
-
.filter((name) => /\.ya?ml$/i.test(name))
|
|
2179
|
-
.sort()
|
|
2180
|
-
.map((name) => {
|
|
2181
|
-
const manifestFile = path.join(files.artifactHistoryArchives, name);
|
|
2182
|
-
const manifest = readYaml(manifestFile, options.yaml, `artifact history archive manifest ${name}`);
|
|
2183
|
-
const archiveId = historyAnchorId(manifest.archive_id);
|
|
2184
|
-
const expectedManifestName = `${archiveId}.yaml`;
|
|
2185
|
-
const archiveFile = path.join(files.artifactHistoryArchives, `${archiveId}.json.gz`);
|
|
2186
|
-
const coverage = manifest.coverage;
|
|
2187
|
-
if (name !== expectedManifestName
|
|
2188
|
-
|| manifest.schema_version !== 1
|
|
2189
|
-
|| manifest.stream !== "artifact-registry"
|
|
2190
|
-
|| manifest.anchor_id !== archiveId
|
|
2191
|
-
|| manifest.archive_format !== "gzip-json-v1"
|
|
2192
|
-
|| manifest.archive_path !== portableReference(files, archiveFile)
|
|
2193
|
-
|| !/^[a-f0-9]{64}$/.test(manifest.anchor_checksum || "")
|
|
2194
|
-
|| !/^[a-f0-9]{64}$/.test(manifest.registry_checksum || "")
|
|
2195
|
-
|| !/^[a-f0-9]{64}$/.test(manifest.source_history_root_digest || "")
|
|
2196
|
-
|| manifest.source_history_root_digest_algorithm !== "sha256-path-null-bytes-null-v1"
|
|
2197
|
-
|| !/^[a-f0-9]{64}$/.test(manifest.archive_checksum || "")
|
|
2198
|
-
|| !Number.isSafeInteger(manifest.registry_revision)
|
|
2199
|
-
|| manifest.registry_revision < 1
|
|
2200
|
-
|| !Number.isSafeInteger(coverage?.first_revision)
|
|
2201
|
-
|| !Number.isSafeInteger(coverage?.last_revision)
|
|
2202
|
-
|| !Number.isSafeInteger(coverage?.event_count)
|
|
2203
|
-
|| coverage.first_revision < 1
|
|
2204
|
-
|| coverage.last_revision < coverage.first_revision
|
|
2205
|
-
|| coverage.event_count !== coverage.last_revision - coverage.first_revision + 1
|
|
2206
|
-
|| coverage.last_revision !== manifest.registry_revision
|
|
2207
|
-
|| !Number.isSafeInteger(manifest.loose_file_count)
|
|
2208
|
-
|| manifest.loose_file_count !== coverage.event_count
|
|
2209
|
-
|| !Number.isSafeInteger(manifest.loose_bytes)
|
|
2210
|
-
|| manifest.loose_bytes < 0
|
|
2211
|
-
|| !Number.isSafeInteger(manifest.compressed_bundle_bytes)
|
|
2212
|
-
|| manifest.compressed_bundle_bytes < 1
|
|
2213
|
-
|| structuredDocumentChecksum(manifest, "manifest_checksum", options.yaml.stringify)
|
|
2214
|
-
!== manifest.manifest_checksum
|
|
2215
|
-
|| !fs.existsSync(archiveFile)
|
|
2216
|
-
|| fs.statSync(archiveFile).size !== manifest.compressed_bundle_bytes
|
|
2217
|
-
|| sha256(fs.readFileSync(archiveFile)) !== manifest.archive_checksum) {
|
|
2218
|
-
throw new Error(`Artifact history archive manifest is invalid: ${name}.`);
|
|
2219
|
-
}
|
|
2220
|
-
let bundle;
|
|
2221
|
-
try {
|
|
2222
|
-
bundle = JSON.parse(zlib.gunzipSync(fs.readFileSync(archiveFile)).toString("utf8"));
|
|
2223
|
-
} catch {
|
|
2224
|
-
throw new Error(`Artifact history archive bundle is invalid: ${archiveId}.`);
|
|
2225
|
-
}
|
|
2226
|
-
if (bundle?.schema_version !== 1
|
|
2227
|
-
|| bundle.archive_id !== archiveId
|
|
2228
|
-
|| bundle.stream !== "artifact-registry"
|
|
2229
|
-
|| bundle.source_root !== manifest.source_root
|
|
2230
|
-
|| bundle.source_history_root_digest !== manifest.source_history_root_digest
|
|
2231
|
-
|| bundle.source_history_root_digest_algorithm !== manifest.source_history_root_digest_algorithm
|
|
2232
|
-
|| JSON.stringify(bundle.coverage) !== JSON.stringify(manifest.coverage)
|
|
2233
|
-
|| bundle.anchor?.anchor_id !== manifest.anchor_id
|
|
2234
|
-
|| bundle.anchor?.anchor_checksum !== manifest.anchor_checksum
|
|
2235
|
-
|| bundle.anchor?.registry_revision !== manifest.registry_revision
|
|
2236
|
-
|| bundle.anchor?.registry_checksum !== manifest.registry_checksum
|
|
2237
|
-
|| !Array.isArray(bundle.entries)
|
|
2238
|
-
|| bundle.entries.length !== manifest.loose_file_count) {
|
|
2239
|
-
throw new Error(`Artifact history archive bundle metadata is invalid: ${archiveId}.`);
|
|
2240
|
-
}
|
|
2241
|
-
const digest = crypto.createHash("sha256");
|
|
2242
|
-
const names = new Set();
|
|
2243
|
-
let entryBytes = 0;
|
|
2244
|
-
const records = bundle.entries.map((entry, index) => {
|
|
2245
|
-
if (!/^[^/\\]+\.ya?ml$/i.test(entry?.path || "") || names.has(entry.path)) {
|
|
2246
|
-
throw new Error(`Artifact history archive entry path is invalid: ${archiveId}.`);
|
|
2247
|
-
}
|
|
2248
|
-
names.add(entry.path);
|
|
2249
|
-
if (!Number.isSafeInteger(entry.size_bytes) || entry.size_bytes < 0
|
|
2250
|
-
|| !Number.isSafeInteger(entry.revision)
|
|
2251
|
-
|| entry.revision !== coverage.first_revision + index) {
|
|
2252
|
-
throw new Error(`Artifact history archive entry metadata is invalid: ${archiveId}/${entry.path}.`);
|
|
2253
|
-
}
|
|
2254
|
-
const content = Buffer.from(String(entry.content_base64 || ""), "base64");
|
|
2255
|
-
if (content.length !== entry.size_bytes || sha256(content) !== entry.sha256) {
|
|
2256
|
-
throw new Error(`Artifact history archive entry checksum is invalid: ${archiveId}/${entry.path}.`);
|
|
2257
|
-
}
|
|
2258
|
-
entryBytes += entry.size_bytes;
|
|
2259
|
-
digest.update(entry.path);
|
|
2260
|
-
digest.update("\0");
|
|
2261
|
-
digest.update(content);
|
|
2262
|
-
digest.update("\0");
|
|
2263
|
-
let record;
|
|
2264
|
-
try {
|
|
2265
|
-
record = options.yaml.parse(content.toString("utf8"));
|
|
2266
|
-
} catch {
|
|
2267
|
-
throw new Error(`Artifact history archive entry is unreadable: ${archiveId}/${entry.path}.`);
|
|
2268
|
-
}
|
|
2269
|
-
const validated = validateEventShardRecord(record, "artifact-registry", entry.path);
|
|
2270
|
-
if (validated.revision !== entry.revision) {
|
|
2271
|
-
throw new Error(`Artifact history archive entry revision is invalid: ${archiveId}/${entry.path}.`);
|
|
2272
|
-
}
|
|
2273
|
-
return validated;
|
|
2274
|
-
});
|
|
2275
|
-
if (digest.digest("hex") !== manifest.source_history_root_digest
|
|
2276
|
-
|| entryBytes !== manifest.loose_bytes
|
|
2277
|
-
|| records.length !== manifest.coverage?.event_count
|
|
2278
|
-
|| records[0]?.revision !== manifest.coverage?.first_revision
|
|
2279
|
-
|| records.at(-1)?.revision !== manifest.coverage?.last_revision
|
|
2280
|
-
|| records.some((record, index) => record.revision !== coverage.first_revision + index)) {
|
|
2281
|
-
throw new Error(`Artifact history archive coverage is invalid: ${archiveId}.`);
|
|
2282
|
-
}
|
|
2283
|
-
const replayed = replayStreamRecords("artifact-registry", records, options);
|
|
2284
|
-
const anchorFile = path.join(files.historySnapshots, archiveId, "anchor.yaml");
|
|
2285
|
-
const snapshotFile = path.join(files.historySnapshots, archiveId, "artifact-registry.yaml");
|
|
2286
|
-
const anchor = readYaml(anchorFile, options.yaml, `artifact history archive anchor ${archiveId}`);
|
|
2287
|
-
const snapshot = normalizeArtifactRegistry(readYaml(
|
|
2288
|
-
snapshotFile,
|
|
2289
|
-
options.yaml,
|
|
2290
|
-
`artifact history archive snapshot ${archiveId}`,
|
|
2291
|
-
));
|
|
2292
|
-
if (anchor.anchor_checksum !== manifest.anchor_checksum
|
|
2293
|
-
|| structuredDocumentChecksum(anchor, "anchor_checksum", options.yaml.stringify)
|
|
2294
|
-
!== anchor.anchor_checksum
|
|
2295
|
-
|| anchor.streams?.["artifact-registry"]?.revision !== manifest.registry_revision
|
|
2296
|
-
|| anchor.streams?.["artifact-registry"]?.view_checksum !== manifest.registry_checksum
|
|
2297
|
-
|| sha256(fs.readFileSync(snapshotFile)) !== manifest.registry_checksum
|
|
2298
|
-
|| JSON.stringify(stableValue(normalizeArtifactRegistry(replayed)))
|
|
2299
|
-
!== JSON.stringify(stableValue(snapshot))) {
|
|
2300
|
-
throw new Error(`Artifact history archive replay does not match its anchor: ${archiveId}.`);
|
|
2301
|
-
}
|
|
2302
|
-
return { manifest, manifestFile, archiveFile, bundle };
|
|
2303
|
-
});
|
|
2304
|
-
}
|
|
2305
|
-
|
|
2306
|
-
function restoreCompactionStaging(files, directory, plan) {
|
|
2307
|
-
const shardDirectory = path.join(files.eventShards, "artifact-registry");
|
|
2308
|
-
fs.mkdirSync(shardDirectory, { recursive: true });
|
|
2309
|
-
for (const entry of plan.entries) {
|
|
2310
|
-
const staged = path.join(directory, entry.path);
|
|
2311
|
-
if (!fs.existsSync(staged)) continue;
|
|
2312
|
-
if (sha256(fs.readFileSync(staged)) !== entry.sha256) {
|
|
2313
|
-
throw new Error(`Artifact history compact staged shard checksum mismatch: ${entry.path}.`);
|
|
2314
|
-
}
|
|
2315
|
-
const destination = path.join(shardDirectory, entry.path);
|
|
2316
|
-
if (fs.existsSync(destination)) {
|
|
2317
|
-
if (sha256(fs.readFileSync(destination)) !== entry.sha256) {
|
|
2318
|
-
throw new Error(`Artifact history compact recovery destination conflicts: ${entry.path}.`);
|
|
2319
|
-
}
|
|
2320
|
-
fs.rmSync(staged, { force: true });
|
|
2321
|
-
} else {
|
|
2322
|
-
fs.renameSync(staged, destination);
|
|
2323
|
-
}
|
|
2324
|
-
}
|
|
2325
|
-
}
|
|
2326
|
-
|
|
2327
|
-
function recoverArtifactHistoryCompaction(files, apply, options) {
|
|
2328
|
-
if (!fs.existsSync(files.compactRuntime)) return 0;
|
|
2329
|
-
const pending = fs.readdirSync(files.compactRuntime, { withFileTypes: true })
|
|
2330
|
-
.filter((entry) => entry.isDirectory())
|
|
2331
|
-
.map((entry) => path.join(files.compactRuntime, entry.name));
|
|
2332
|
-
if (pending.length && !apply) {
|
|
2333
|
-
throw new Error("An interrupted artifact history compact requires explicit apply recovery.");
|
|
2334
|
-
}
|
|
2335
|
-
let recovered = 0;
|
|
2336
|
-
for (const directory of pending) {
|
|
2337
|
-
const planFile = path.join(directory, "plan.json");
|
|
2338
|
-
if (!fs.existsSync(planFile)) {
|
|
2339
|
-
const interruptedPlanWrites = fs.readdirSync(directory, { withFileTypes: true });
|
|
2340
|
-
if (interruptedPlanWrites.every((entry) =>
|
|
2341
|
-
entry.isFile() && /^plan\.json\.\d+\.\d+\.tmp$/.test(entry.name))) {
|
|
2342
|
-
fs.rmSync(directory, { recursive: true, force: true });
|
|
2343
|
-
recovered += 1;
|
|
2344
|
-
continue;
|
|
2345
|
-
}
|
|
2346
|
-
throw new Error(`Artifact history compact recovery plan is invalid: ${directory}.`);
|
|
2347
|
-
}
|
|
2348
|
-
let plan;
|
|
2349
|
-
try {
|
|
2350
|
-
plan = JSON.parse(fs.readFileSync(planFile, "utf8"));
|
|
2351
|
-
} catch {
|
|
2352
|
-
throw new Error(`Artifact history compact recovery plan is invalid: ${directory}.`);
|
|
2353
|
-
}
|
|
2354
|
-
if (plan?.schema_version !== 1 || !plan.archive_id
|
|
2355
|
-
|| !/^[a-f0-9]{64}$/.test(plan.archive_checksum || "")
|
|
2356
|
-
|| !Array.isArray(plan.entries) || !plan.entries.length
|
|
2357
|
-
|| plan.entries.some((entry) => !/^[^/\\]+\.ya?ml$/i.test(entry?.path || "")
|
|
2358
|
-
|| !/^[a-f0-9]{64}$/.test(entry?.sha256 || ""))) {
|
|
2359
|
-
throw new Error(`Artifact history compact recovery plan is invalid: ${directory}.`);
|
|
2360
|
-
}
|
|
2361
|
-
const plannedEntries = new Map(plan.entries.map((entry) => [entry.path, entry.sha256]));
|
|
2362
|
-
const stagedEntries = fs.readdirSync(directory, { withFileTypes: true })
|
|
2363
|
-
.filter((entry) => entry.name !== "plan.json");
|
|
2364
|
-
if (plannedEntries.size !== plan.entries.length
|
|
2365
|
-
|| stagedEntries.some((entry) => !entry.isFile() || !plannedEntries.has(entry.name))) {
|
|
2366
|
-
throw new Error(`Artifact history compact recovery plan is invalid: ${directory}.`);
|
|
2367
|
-
}
|
|
2368
|
-
const changedStagedEntry = stagedEntries.find((entry) =>
|
|
2369
|
-
sha256(fs.readFileSync(path.join(directory, entry.name))) !== plannedEntries.get(entry.name));
|
|
2370
|
-
if (changedStagedEntry) {
|
|
2371
|
-
throw new Error(`Artifact history compact staged shard checksum mismatch: ${changedStagedEntry.name}.`);
|
|
2372
|
-
}
|
|
2373
|
-
let archive = null;
|
|
2374
|
-
try {
|
|
2375
|
-
archive = readArtifactHistoryArchives(files, options)
|
|
2376
|
-
.find(({ manifest }) => manifest.archive_id === plan.archive_id) || null;
|
|
2377
|
-
} catch {
|
|
2378
|
-
archive = null;
|
|
2379
|
-
}
|
|
2380
|
-
const archivedEntries = new Map(
|
|
2381
|
-
(archive?.bundle.entries || []).map((entry) => [entry.path, entry.sha256]),
|
|
2382
|
-
);
|
|
2383
|
-
const archiveMatches = archive?.manifest.archive_checksum === plan.archive_checksum
|
|
2384
|
-
&& plan.entries.every((entry) => archivedEntries.get(entry.path) === entry.sha256);
|
|
2385
|
-
if (!archiveMatches) {
|
|
2386
|
-
restoreCompactionStaging(files, directory, plan);
|
|
2387
|
-
fs.rmSync(directory, { recursive: true, force: true });
|
|
2388
|
-
throw new Error(`Artifact history compact recovery plan does not match its verified archive: ${plan.archive_id}.`);
|
|
2389
|
-
}
|
|
2390
|
-
fs.rmSync(directory, { recursive: true, force: true });
|
|
2391
|
-
recovered += 1;
|
|
2392
|
-
}
|
|
2393
|
-
return recovered;
|
|
2394
|
-
}
|
|
2395
|
-
|
|
2396
|
-
function removeCompactedLooseEntries(files, archiveId, archiveChecksum, entries) {
|
|
2397
|
-
if (!entries.length) return;
|
|
2398
|
-
const stagingDirectory = path.join(files.compactRuntime, crypto.randomUUID());
|
|
2399
|
-
fs.mkdirSync(stagingDirectory, { recursive: true });
|
|
2400
|
-
atomicWrite(path.join(stagingDirectory, "plan.json"), `${JSON.stringify({
|
|
2401
|
-
schema_version: 1,
|
|
2402
|
-
archive_id: archiveId,
|
|
2403
|
-
archive_checksum: archiveChecksum,
|
|
2404
|
-
entries: entries.map((entry) => ({ path: entry.path, sha256: entry.sha256 })),
|
|
2405
|
-
}, null, 2)}\n`);
|
|
2406
|
-
const shardDirectory = path.join(files.eventShards, "artifact-registry");
|
|
2407
|
-
try {
|
|
2408
|
-
for (const entry of entries) {
|
|
2409
|
-
const source = path.join(shardDirectory, entry.path);
|
|
2410
|
-
if (sha256(fs.readFileSync(source)) !== entry.sha256) {
|
|
2411
|
-
throw new Error(`Artifact history shard changed during compact: ${entry.path}.`);
|
|
2412
|
-
}
|
|
2413
|
-
fs.renameSync(source, path.join(stagingDirectory, entry.path));
|
|
2414
|
-
}
|
|
2415
|
-
fs.rmSync(stagingDirectory, { recursive: true, force: true });
|
|
2416
|
-
} catch (error) {
|
|
2417
|
-
restoreCompactionStaging(files, stagingDirectory, { entries });
|
|
2418
|
-
fs.rmSync(stagingDirectory, { recursive: true, force: true });
|
|
2419
|
-
throw error;
|
|
2420
|
-
}
|
|
2421
|
-
}
|
|
2422
|
-
|
|
2423
|
-
function compactArtifactHistory(files, flags, options) {
|
|
2424
|
-
const apply = flags.has("--apply");
|
|
2425
|
-
const action = () => {
|
|
2426
|
-
assertPathInside(files.root, files.artifactHistoryArchives);
|
|
2427
|
-
assertPathInside(files.root, files.compactRuntime);
|
|
2428
|
-
assertPathInside(files.root, path.join(files.eventShards, "artifact-registry"));
|
|
2429
|
-
const recoveredCompactionCount = recoverArtifactHistoryCompaction(files, apply, options);
|
|
2430
|
-
const trusted = readLatestHistoryAnchor(files, options);
|
|
2431
|
-
if (!trusted) throw new Error("Legacy artifact history compact requires a trusted history anchor.");
|
|
2432
|
-
const existingArchive = readArtifactHistoryArchives(files, options)
|
|
2433
|
-
.find(({ manifest }) => manifest.archive_id === trusted.anchor.anchor_id);
|
|
2434
|
-
if (existingArchive) {
|
|
2435
|
-
verifyFormalState(files, options, { history: true });
|
|
2436
|
-
const shardDirectory = path.join(files.eventShards, "artifact-registry");
|
|
2437
|
-
const looseEntries = existingArchive.bundle.entries.filter((entry) => {
|
|
2438
|
-
const looseFile = path.join(shardDirectory, entry.path);
|
|
2439
|
-
if (!fs.existsSync(looseFile)) return false;
|
|
2440
|
-
if (sha256(fs.readFileSync(looseFile)) !== entry.sha256) {
|
|
2441
|
-
throw new Error(`Artifact history loose shard conflicts with verified archive: ${entry.path}.`);
|
|
2442
|
-
}
|
|
2443
|
-
return true;
|
|
2444
|
-
});
|
|
2445
|
-
const looseBytes = looseEntries.reduce((total, entry) => total + entry.size_bytes, 0);
|
|
2446
|
-
const manifestBytes = fs.statSync(existingArchive.manifestFile).size;
|
|
2447
|
-
const result = {
|
|
2448
|
-
command: "compact",
|
|
2449
|
-
anchor_id: existingArchive.manifest.anchor_id,
|
|
2450
|
-
anchor_checksum: existingArchive.manifest.anchor_checksum,
|
|
2451
|
-
source_history_root_digest: existingArchive.manifest.source_history_root_digest,
|
|
2452
|
-
coverage: existingArchive.manifest.coverage,
|
|
2453
|
-
loose_file_count: looseEntries.length,
|
|
2454
|
-
loose_bytes: looseBytes,
|
|
2455
|
-
compressed_bundle_bytes: existingArchive.manifest.compressed_bundle_bytes,
|
|
2456
|
-
manifest_bytes: manifestBytes,
|
|
2457
|
-
archive_bytes: existingArchive.manifest.compressed_bundle_bytes + manifestBytes,
|
|
2458
|
-
expected_freed_bytes: looseBytes,
|
|
2459
|
-
archive_path: existingArchive.manifest.archive_path,
|
|
2460
|
-
manifest_path: portableReference(files, existingArchive.manifestFile),
|
|
2461
|
-
mode: apply ? "apply" : "dry-run",
|
|
2462
|
-
applied: false,
|
|
2463
|
-
deduplicated: true,
|
|
2464
|
-
recovered_compaction_count: recoveredCompactionCount,
|
|
2465
|
-
};
|
|
2466
|
-
if (!apply || !looseEntries.length) return result;
|
|
2467
|
-
removeCompactedLooseEntries(
|
|
2468
|
-
files,
|
|
2469
|
-
trusted.anchor.anchor_id,
|
|
2470
|
-
existingArchive.manifest.archive_checksum,
|
|
2471
|
-
looseEntries,
|
|
2472
|
-
);
|
|
2473
|
-
return { ...result, applied: true };
|
|
2474
|
-
}
|
|
2475
|
-
const plan = legacyArtifactHistoryArchive(files, trusted, options);
|
|
2476
|
-
const result = {
|
|
2477
|
-
...plan.report,
|
|
2478
|
-
mode: apply ? "apply" : "dry-run",
|
|
2479
|
-
applied: false,
|
|
2480
|
-
deduplicated: false,
|
|
2481
|
-
recovered_compaction_count: recoveredCompactionCount,
|
|
2482
|
-
};
|
|
2483
|
-
if (!apply) return result;
|
|
2484
|
-
if (fs.existsSync(plan.manifestFile)) {
|
|
2485
|
-
throw new Error(`Artifact history archive already exists: ${trusted.anchor.anchor_id}.`);
|
|
2486
|
-
}
|
|
2487
|
-
fs.mkdirSync(files.artifactHistoryArchives, { recursive: true });
|
|
2488
|
-
const orphanArchiveExists = fs.existsSync(plan.archiveFile);
|
|
2489
|
-
if (orphanArchiveExists
|
|
2490
|
-
&& (fs.statSync(plan.archiveFile).size !== plan.archiveBytes.length
|
|
2491
|
-
|| sha256(fs.readFileSync(plan.archiveFile)) !== plan.manifest.archive_checksum)) {
|
|
2492
|
-
throw new Error(`Artifact history archive conflicts with the compact plan: ${trusted.anchor.anchor_id}.`);
|
|
2493
|
-
}
|
|
2494
|
-
let archiveWritten = false;
|
|
2495
|
-
let manifestWritten = false;
|
|
2496
|
-
try {
|
|
2497
|
-
if (!orphanArchiveExists) {
|
|
2498
|
-
atomicWrite(plan.archiveFile, plan.archiveBytes);
|
|
2499
|
-
archiveWritten = true;
|
|
2500
|
-
}
|
|
2501
|
-
atomicWrite(plan.manifestFile, plan.manifestContent);
|
|
2502
|
-
manifestWritten = true;
|
|
2503
|
-
const verified = readArtifactHistoryArchives(files, options);
|
|
2504
|
-
if (!verified.some(({ manifest }) => manifest.archive_id === trusted.anchor.anchor_id)) {
|
|
2505
|
-
throw new Error(`Artifact history archive verification failed: ${trusted.anchor.anchor_id}.`);
|
|
2506
|
-
}
|
|
2507
|
-
} catch (error) {
|
|
2508
|
-
if (manifestWritten) fs.rmSync(plan.manifestFile, { force: true });
|
|
2509
|
-
if (archiveWritten) fs.rmSync(plan.archiveFile, { force: true });
|
|
2510
|
-
throw error;
|
|
2511
|
-
}
|
|
2512
|
-
|
|
2513
|
-
removeCompactedLooseEntries(
|
|
2514
|
-
files,
|
|
2515
|
-
trusted.anchor.anchor_id,
|
|
2516
|
-
plan.manifest.archive_checksum,
|
|
2517
|
-
plan.entries,
|
|
2518
|
-
);
|
|
2519
|
-
return { ...result, applied: true };
|
|
2520
|
-
};
|
|
2521
|
-
return apply
|
|
2522
|
-
? withLock(files, "artifact-history-compact", action)
|
|
2523
|
-
: withZeroWriteFormalState(files, action);
|
|
2524
|
-
}
|
|
2525
|
-
|
|
2526
1878
|
function pruneArtifactHistory(files, flags, options) {
|
|
2527
1879
|
const apply = flags.has("--apply");
|
|
2528
1880
|
const action = () => {
|
|
@@ -2621,7 +1973,7 @@ function pruneArtifactHistory(files, flags, options) {
|
|
|
2621
1973
|
}
|
|
2622
1974
|
|
|
2623
1975
|
function allowedCompletionStatuses(node) {
|
|
2624
|
-
if (node === "06-version-closeout"
|
|
1976
|
+
if (node === "06-version-closeout") {
|
|
2625
1977
|
return new Set(["release-ready"]);
|
|
2626
1978
|
}
|
|
2627
1979
|
if (node === "R08-production-deployment") return new Set(["deployed"]);
|
|
@@ -2632,9 +1984,8 @@ function allowedCompletionStatuses(node) {
|
|
|
2632
1984
|
|
|
2633
1985
|
function nodeModel(node) {
|
|
2634
1986
|
if (SEVEN_NODE_DEVELOPMENT_ID_SET.has(node)) return "development-seven-node";
|
|
2635
|
-
if (LEGACY_DEVELOPMENT_ID_SET.has(node)) return "development-legacy-13";
|
|
2636
1987
|
if (RELEASE_NODE_ID_SET.has(node)) return "release-r00-r10";
|
|
2637
|
-
return "
|
|
1988
|
+
return "unknown";
|
|
2638
1989
|
}
|
|
2639
1990
|
|
|
2640
1991
|
function nodeContractCatalog() {
|
|
@@ -2730,7 +2081,6 @@ function persistScopeMutation(files, context, options) {
|
|
|
2730
2081
|
const handoffMutation = artifactHandoffMutation(files, transition, timestamp, options);
|
|
2731
2082
|
if (handoffMutation) mutations.push(handoffMutation);
|
|
2732
2083
|
commitFormalTransaction(files, mutations, extraWrites, options);
|
|
2733
|
-
exposeLegacyScopeAliases(state);
|
|
2734
2084
|
}
|
|
2735
2085
|
|
|
2736
2086
|
function scopeManifestFromFlags(flags, scope, timestamp, amendment) {
|
|
@@ -2782,7 +2132,7 @@ function mutateScopeManifest(files, flags, options, amendment = false) {
|
|
|
2782
2132
|
&& ![...SCOPE_FREEZE_NODES].some((node) =>
|
|
2783
2133
|
scope.active_nodes.includes(node) || scope.completed_nodes.includes(node))) {
|
|
2784
2134
|
throw new Error(
|
|
2785
|
-
"scope freeze requires Delivery Readiness (03-delivery-readiness
|
|
2135
|
+
"scope freeze requires Delivery Readiness (03-delivery-readiness) to be active or completed.",
|
|
2786
2136
|
);
|
|
2787
2137
|
}
|
|
2788
2138
|
const timestamp = (options.now || (() => new Date().toISOString()))();
|
|
@@ -2881,7 +2231,7 @@ function recordScopeWarning(files, flags, options) {
|
|
|
2881
2231
|
function completionReceiptFromProjection(scope, node, projection, evidence, timestamp) {
|
|
2882
2232
|
const manifest = currentScopeManifest(scope);
|
|
2883
2233
|
const receipt = stableValue({
|
|
2884
|
-
receipt_id: `RECEIPT-${sha256(`${scope.scope_id}:${node}:${projection.node_attempt}:${manifest?.manifest_id || "
|
|
2234
|
+
receipt_id: `RECEIPT-${sha256(`${scope.scope_id}:${node}:${projection.node_attempt}:${manifest?.manifest_id || "none"}`).slice(0, 24)}`,
|
|
2885
2235
|
iteration_id: scope.scope_id,
|
|
2886
2236
|
node,
|
|
2887
2237
|
node_attempt: projection.node_attempt,
|
|
@@ -2958,13 +2308,13 @@ function transitionWorkflow(files, flags, options) {
|
|
|
2958
2308
|
const registry = normalizeArtifactRegistry(readYaml(files.artifactRegistry, options.yaml, "artifact-registry.yaml"));
|
|
2959
2309
|
const artifact = registry.artifacts.find((item) => (item.artifact_id || item.id) === artifactId);
|
|
2960
2310
|
if (!artifact) throw new Error(`Selected artifact is not registered: ${artifactId}`);
|
|
2961
|
-
|
|
2311
|
+
assertWritableNode(artifact.node);
|
|
2962
2312
|
if (artifactSuperseder(registry, artifact)) {
|
|
2963
2313
|
throw new Error(`Selected artifact is superseded: ${artifactId}`);
|
|
2964
2314
|
}
|
|
2965
2315
|
const expectedNodes = event === "candidate-selected"
|
|
2966
|
-
? new Set(["04-implementation", "
|
|
2967
|
-
: new Set(["
|
|
2316
|
+
? new Set(["04-implementation", "R04-release-candidate"])
|
|
2317
|
+
: new Set(["R06-release-preparation"]);
|
|
2968
2318
|
if (!expectedNodes.has(artifact.node)) {
|
|
2969
2319
|
throw new Error(`Selected artifact ${artifactId} belongs to ${artifact.node}, not ${[...expectedNodes].join(" or ")}.`);
|
|
2970
2320
|
}
|
|
@@ -2975,8 +2325,7 @@ function transitionWorkflow(files, flags, options) {
|
|
|
2975
2325
|
if (scope.scope_id !== iterationId) {
|
|
2976
2326
|
throw new Error(`Iteration mismatch for ${scopeName} scope: state is ${scope.scope_id}, command is ${iterationId}.`);
|
|
2977
2327
|
}
|
|
2978
|
-
if (artifact.iteration_id && artifact.iteration_id !== iterationId
|
|
2979
|
-
&& !(event === "release-selected" && artifact.node === "12-release-preparation")) {
|
|
2328
|
+
if (artifact.iteration_id && artifact.iteration_id !== iterationId) {
|
|
2980
2329
|
throw new Error(`Selected artifact ${artifactId} belongs to iteration ${artifact.iteration_id}, not ${iterationId}.`);
|
|
2981
2330
|
}
|
|
2982
2331
|
const allowed = event === "candidate-selected" ? new Set(["immutable"]) : new Set(["immutable", "approved"]);
|
|
@@ -2997,7 +2346,7 @@ function transitionWorkflow(files, flags, options) {
|
|
|
2997
2346
|
const registry = normalizeArtifactRegistry(readYaml(files.artifactRegistry, options.yaml, "artifact-registry.yaml"));
|
|
2998
2347
|
const artifact = registry.artifacts.find((item) => (item.artifact_id || item.id) === artifactId);
|
|
2999
2348
|
if (!artifact) throw new Error(`Milestone artifact is not registered: ${artifactId}`);
|
|
3000
|
-
|
|
2349
|
+
assertWritableNode(artifact.node);
|
|
3001
2350
|
const expectedNodes = event === "version-closed"
|
|
3002
2351
|
? ["06-version-closeout"]
|
|
3003
2352
|
: ["R10-release-archive"];
|
|
@@ -3033,10 +2382,7 @@ function transitionWorkflow(files, flags, options) {
|
|
|
3033
2382
|
}
|
|
3034
2383
|
|
|
3035
2384
|
const node = requiredFlag(flags, "--node");
|
|
3036
|
-
|
|
3037
|
-
if (NODE_IDS.has(node)) throw new Error(`Legacy workflow node is read-only: ${node}`);
|
|
3038
|
-
throw new Error(`Unknown workflow node: ${node}`);
|
|
3039
|
-
}
|
|
2385
|
+
assertWritableNode(node);
|
|
3040
2386
|
const scopeName = scopeNameForNode(node);
|
|
3041
2387
|
assertWorkflowRevision(flags, state, scopeName);
|
|
3042
2388
|
const scope = scopeState(state, scopeName);
|
|
@@ -3208,166 +2554,15 @@ function registeredEvidence(files, evidence, options, node = null) {
|
|
|
3208
2554
|
});
|
|
3209
2555
|
}
|
|
3210
2556
|
|
|
3211
|
-
function retiredGateContractPlan(state, gateId) {
|
|
3212
|
-
assertRetiredGateContract(gateId);
|
|
3213
|
-
const affectedIterations = [];
|
|
3214
|
-
const expectedScopeRevisions = {};
|
|
3215
|
-
for (const [scopeName, scope] of [
|
|
3216
|
-
["development", state.development_state],
|
|
3217
|
-
["release", state.release_state],
|
|
3218
|
-
]) {
|
|
3219
|
-
const requests = scope.pending_gates
|
|
3220
|
-
.map((gate) => typeof gate === "string" ? { gate_id: gate, node: null } : gate)
|
|
3221
|
-
.filter((gate) => gate?.gate_id === gateId);
|
|
3222
|
-
if (!requests.length) continue;
|
|
3223
|
-
expectedScopeRevisions[scopeName] = scope.revision;
|
|
3224
|
-
for (const request of requests) {
|
|
3225
|
-
affectedIterations.push({
|
|
3226
|
-
iteration_id: request.iteration_id || scope.scope_id,
|
|
3227
|
-
scope: scopeName,
|
|
3228
|
-
node: request.node || null,
|
|
3229
|
-
node_attempt: Number(request.node_attempt || scope.node_attempts[request.node] || 1),
|
|
3230
|
-
scope_revision: scope.revision,
|
|
3231
|
-
});
|
|
3232
|
-
}
|
|
3233
|
-
}
|
|
3234
|
-
return {
|
|
3235
|
-
command: "contract retire",
|
|
3236
|
-
dry_run: true,
|
|
3237
|
-
applied: false,
|
|
3238
|
-
gate_id: gateId,
|
|
3239
|
-
replacement_gate_id: gateId === "UX-LF" ? "UX-UI" : null,
|
|
3240
|
-
fresh_approval_required: gateId === "UX-LF",
|
|
3241
|
-
affected_iterations: affectedIterations,
|
|
3242
|
-
expected_scope_revisions: expectedScopeRevisions,
|
|
3243
|
-
};
|
|
3244
|
-
}
|
|
3245
|
-
|
|
3246
|
-
function retireGateContract(files, flags, options) {
|
|
3247
|
-
const gateId = requiredFlag(flags, "--gate-id");
|
|
3248
|
-
if (flags.has("--dry-run")) return withReadableFormalState(files, () => {
|
|
3249
|
-
const state = normalizeWorkflowState(
|
|
3250
|
-
readYaml(files.workflowState, options.yaml, "workflow-state.yaml"),
|
|
3251
|
-
);
|
|
3252
|
-
return retiredGateContractPlan(state, gateId);
|
|
3253
|
-
});
|
|
3254
|
-
return withLock(files, "workflow-state", () => {
|
|
3255
|
-
const state = normalizeWorkflowState(
|
|
3256
|
-
readYaml(files.workflowState, options.yaml, "workflow-state.yaml"),
|
|
3257
|
-
);
|
|
3258
|
-
const plan = retiredGateContractPlan(state, gateId);
|
|
3259
|
-
if (!plan.affected_iterations.length) {
|
|
3260
|
-
return { ...plan, dry_run: false, applied: false, receipts: [], workflow_state: state };
|
|
3261
|
-
}
|
|
3262
|
-
const affectedScopes = Object.keys(plan.expected_scope_revisions);
|
|
3263
|
-
if (affectedScopes.length !== 1) {
|
|
3264
|
-
throw new Error(`${gateId} retirement must affect exactly one formal scope per transaction.`);
|
|
3265
|
-
}
|
|
3266
|
-
const scopeName = affectedScopes[0];
|
|
3267
|
-
assertWorkflowRevision(flags, state, scopeName);
|
|
3268
|
-
const actor = requiredFlag(flags, "--actor");
|
|
3269
|
-
if (actor !== "system:package-upgrade") {
|
|
3270
|
-
throw new Error("Contract retirement must be executed by system:package-upgrade.");
|
|
3271
|
-
}
|
|
3272
|
-
const reason = requiredFlag(flags, "--reason");
|
|
3273
|
-
const contractVersion = requiredFlag(flags, "--contract-version");
|
|
3274
|
-
const scope = scopeState(state, scopeName);
|
|
3275
|
-
const pending = scope.pending_gates
|
|
3276
|
-
.map((gate) => typeof gate === "string" ? { gate_id: gate, node: null } : gate);
|
|
3277
|
-
const retiredRequests = pending.filter((gate) => gate?.gate_id === gateId);
|
|
3278
|
-
const timestamp = (options.now || (() => new Date().toISOString()))();
|
|
3279
|
-
const receipts = retiredRequests.map((request) => {
|
|
3280
|
-
const requestChecksum = sha256(JSON.stringify(stableValue(request)));
|
|
3281
|
-
const receipt = stableValue({
|
|
3282
|
-
receipt_id: `RETIRE-${gateId}-${requestChecksum.slice(0, 20).toUpperCase()}`,
|
|
3283
|
-
gate_id: gateId,
|
|
3284
|
-
iteration_id: request.iteration_id || scope.scope_id,
|
|
3285
|
-
scope: scopeName,
|
|
3286
|
-
node: request.node || null,
|
|
3287
|
-
node_attempt: Number(request.node_attempt || scope.node_attempts[request.node] || 1),
|
|
3288
|
-
request_checksum: requestChecksum,
|
|
3289
|
-
request_evidence: [...new Set(request.evidence || [])].sort(),
|
|
3290
|
-
contract_version: contractVersion,
|
|
3291
|
-
disposition: "contract-retired",
|
|
3292
|
-
human_approval_created: false,
|
|
3293
|
-
renewed_gate_id: gateId === "UX-LF" ? "UX-UI" : null,
|
|
3294
|
-
retired_by: actor,
|
|
3295
|
-
reason,
|
|
3296
|
-
retired_at: timestamp,
|
|
3297
|
-
});
|
|
3298
|
-
receipt.receipt_checksum = contractRetirementReceiptChecksum(receipt);
|
|
3299
|
-
return receipt;
|
|
3300
|
-
});
|
|
3301
|
-
const remaining = pending.filter((gate) => gate?.gate_id !== gateId);
|
|
3302
|
-
const existingUxUiRequests = remaining.filter((gate) => gate?.gate_id === "UX-UI");
|
|
3303
|
-
const renewalSources = existingUxUiRequests.length ? existingUxUiRequests : retiredRequests;
|
|
3304
|
-
const renewedRequests = gateId === "UX-LF"
|
|
3305
|
-
? renewalSources.map((request, index) => {
|
|
3306
|
-
const retiredIndex = Math.max(0, retiredRequests.findIndex((candidate) =>
|
|
3307
|
-
(candidate.iteration_id || scope.scope_id) === (request.iteration_id || scope.scope_id)));
|
|
3308
|
-
const retiredRequest = retiredRequests[retiredIndex] || retiredRequests[index] || request;
|
|
3309
|
-
const receipt = receipts[retiredIndex] || receipts[index];
|
|
3310
|
-
return {
|
|
3311
|
-
...request,
|
|
3312
|
-
gate_id: "UX-UI",
|
|
3313
|
-
renewed_from_gate_id: gateId,
|
|
3314
|
-
iteration_id: request.iteration_id || scope.scope_id,
|
|
3315
|
-
scope: scopeName,
|
|
3316
|
-
node: "02-solution-design",
|
|
3317
|
-
node_attempt: Number(retiredRequest.node_attempt || request.node_attempt
|
|
3318
|
-
|| scope.node_attempts[retiredRequest.node] || 1),
|
|
3319
|
-
evidence: [],
|
|
3320
|
-
prototype_revision_required: true,
|
|
3321
|
-
required_evidence_type: "prototype-revision",
|
|
3322
|
-
retirement_receipt_checksum: receipt.receipt_checksum,
|
|
3323
|
-
requested_at: request.requested_at || timestamp,
|
|
3324
|
-
};
|
|
3325
|
-
})
|
|
3326
|
-
: [];
|
|
3327
|
-
scope.pending_gates = [
|
|
3328
|
-
...remaining.filter((gate) => gate?.gate_id !== "UX-UI"),
|
|
3329
|
-
...renewedRequests,
|
|
3330
|
-
];
|
|
3331
|
-
scope.contract_retirement_receipts = [
|
|
3332
|
-
...scope.contract_retirement_receipts,
|
|
3333
|
-
...receipts,
|
|
3334
|
-
];
|
|
3335
|
-
const transition = {
|
|
3336
|
-
event: "gate-contract-retired",
|
|
3337
|
-
scope: scopeName,
|
|
3338
|
-
gate_id: gateId,
|
|
3339
|
-
iteration_id: scope.scope_id,
|
|
3340
|
-
retirement_receipt_ids: receipts.map((receipt) => receipt.receipt_id),
|
|
3341
|
-
renewed_gate_ids: renewedRequests.map((request) => request.gate_id),
|
|
3342
|
-
occurred_at: timestamp,
|
|
3343
|
-
};
|
|
3344
|
-
persistScopeMutation(files, { state, scope, timestamp, transition }, options);
|
|
3345
|
-
return {
|
|
3346
|
-
...plan,
|
|
3347
|
-
dry_run: false,
|
|
3348
|
-
applied: true,
|
|
3349
|
-
receipts,
|
|
3350
|
-
renewed_requests: renewedRequests,
|
|
3351
|
-
revision: state.revision,
|
|
3352
|
-
scope_revision: scope.revision,
|
|
3353
|
-
workflow_state: state,
|
|
3354
|
-
};
|
|
3355
|
-
}, { waitForActive: flags.has("--expected-scope-revision") });
|
|
3356
|
-
}
|
|
3357
|
-
|
|
3358
2557
|
function requestGate(files, flags, options) {
|
|
3359
2558
|
return withLock(files, "workflow-state", () => {
|
|
3360
2559
|
const state = normalizeWorkflowState(
|
|
3361
2560
|
readYaml(files.workflowState, options.yaml, "workflow-state.yaml"),
|
|
3362
2561
|
);
|
|
3363
2562
|
const gateId = requiredFlag(flags, "--gate-id");
|
|
3364
|
-
assertGateContractWritable(gateId);
|
|
3365
2563
|
const iterationId = requiredFlag(flags, "--iteration-id");
|
|
3366
2564
|
const node = requiredFlag(flags, "--node");
|
|
3367
|
-
|
|
3368
|
-
if (NODE_IDS.has(node)) throw new Error(`Legacy workflow node is read-only: ${node}`);
|
|
3369
|
-
throw new Error(`Unknown workflow node: ${node}`);
|
|
3370
|
-
}
|
|
2565
|
+
assertWritableNode(node);
|
|
3371
2566
|
const scopeName = scopeNameForNode(node);
|
|
3372
2567
|
assertWorkflowRevision(flags, state, scopeName);
|
|
3373
2568
|
const scope = scopeState(state, scopeName);
|
|
@@ -3418,7 +2613,6 @@ function decideGate(files, flags, options) {
|
|
|
3418
2613
|
readYaml(files.workflowState, options.yaml, "workflow-state.yaml"),
|
|
3419
2614
|
);
|
|
3420
2615
|
const gateId = requiredFlag(flags, "--gate-id");
|
|
3421
|
-
assertGateContractWritable(gateId);
|
|
3422
2616
|
const scopes = [state.development_state, state.release_state];
|
|
3423
2617
|
let owningScope = null;
|
|
3424
2618
|
let pending = [];
|
|
@@ -3435,7 +2629,7 @@ function decideGate(files, flags, options) {
|
|
|
3435
2629
|
}
|
|
3436
2630
|
}
|
|
3437
2631
|
if (!request) throw new Error(`Gate is not pending: ${gateId}`);
|
|
3438
|
-
|
|
2632
|
+
assertWritableNode(request.node);
|
|
3439
2633
|
const scopeName = request.scope || scopeNameForNode(request.node);
|
|
3440
2634
|
assertWorkflowRevision(flags, state, scopeName);
|
|
3441
2635
|
const decision = requiredFlag(flags, "--decision").toLowerCase().replaceAll("_", "-");
|
|
@@ -3749,27 +2943,6 @@ function verifyScopeContracts(scope, scopeName, errors) {
|
|
|
3749
2943
|
errors.push(`Node completion receipt ${receipt.receipt_id} retains blocking defects.`);
|
|
3750
2944
|
}
|
|
3751
2945
|
}
|
|
3752
|
-
const retirementReceiptIds = new Set();
|
|
3753
|
-
for (const receipt of scope.contract_retirement_receipts) {
|
|
3754
|
-
if (!receipt?.receipt_id || retirementReceiptIds.has(receipt.receipt_id)) {
|
|
3755
|
-
errors.push(`Workflow ${scopeName} contract retirement receipt ID is missing or duplicated.`);
|
|
3756
|
-
continue;
|
|
3757
|
-
}
|
|
3758
|
-
retirementReceiptIds.add(receipt.receipt_id);
|
|
3759
|
-
if (receipt.receipt_checksum !== contractRetirementReceiptChecksum(receipt)) {
|
|
3760
|
-
errors.push(`Contract retirement receipt checksum mismatch: ${receipt.receipt_id}.`);
|
|
3761
|
-
}
|
|
3762
|
-
if (!RETIRED_GATE_IDS.has(receipt.gate_id)
|
|
3763
|
-
|| receipt.iteration_id !== scope.scope_id
|
|
3764
|
-
|| receipt.scope !== scopeName
|
|
3765
|
-
|| receipt.disposition !== "contract-retired"
|
|
3766
|
-
|| receipt.human_approval_created !== false
|
|
3767
|
-
|| receipt.retired_by !== "system:package-upgrade"
|
|
3768
|
-
|| !receipt.contract_version
|
|
3769
|
-
|| !/^[a-f0-9]{64}$/.test(receipt.request_checksum || "")) {
|
|
3770
|
-
errors.push(`Contract retirement receipt is invalid: ${receipt.receipt_id}.`);
|
|
3771
|
-
}
|
|
3772
|
-
}
|
|
3773
2946
|
if (scope.scope_contract_version === 1 && scope.current_scope_manifest_id) {
|
|
3774
2947
|
const current = manifestsById.get(scope.current_scope_manifest_id);
|
|
3775
2948
|
for (const node of scope.completed_nodes.filter((item) => SCOPED_COMPLETION_NODES.has(item))) {
|
|
@@ -3801,31 +2974,17 @@ function verifyFormalState(files, options, verification = {}) {
|
|
|
3801
2974
|
const registry = views.get("artifact-registry").value;
|
|
3802
2975
|
const errors = [];
|
|
3803
2976
|
let retentionReceipts = [];
|
|
3804
|
-
let historyArchives = [];
|
|
3805
2977
|
try {
|
|
3806
2978
|
retentionReceipts = readArtifactRetentionReceipts(files, options);
|
|
3807
2979
|
} catch (error) {
|
|
3808
2980
|
errors.push(error.message);
|
|
3809
2981
|
}
|
|
3810
|
-
if (verifyHistory) {
|
|
3811
|
-
try {
|
|
3812
|
-
historyArchives = readArtifactHistoryArchives(files, options);
|
|
3813
|
-
} catch (error) {
|
|
3814
|
-
errors.push(error.message);
|
|
3815
|
-
}
|
|
3816
|
-
}
|
|
3817
|
-
const compactedHistory = verifyHistory && !sinceAnchor
|
|
3818
|
-
&& historyArchives.length > 0 && retentionReceipts.length === 0;
|
|
3819
2982
|
const retainedHistory = verifyHistory && !sinceAnchor
|
|
3820
|
-
&&
|
|
2983
|
+
&& retentionReceipts.length > 0;
|
|
3821
2984
|
const verificationMode = verifyHistory
|
|
3822
2985
|
? (sinceAnchor
|
|
3823
2986
|
? "incremental-history"
|
|
3824
|
-
:
|
|
3825
|
-
? "compacted-history"
|
|
3826
|
-
: retainedHistory
|
|
3827
|
-
? "retained-history"
|
|
3828
|
-
: "full-history")
|
|
2987
|
+
: retainedHistory ? "retained-history" : "full-history")
|
|
3829
2988
|
: "routine";
|
|
3830
2989
|
let trustedAnchor = null;
|
|
3831
2990
|
if (sinceAnchor || !verifyHistory || retainedHistory) {
|
|
@@ -3897,7 +3056,7 @@ function verifyFormalState(files, options, verification = {}) {
|
|
|
3897
3056
|
continue;
|
|
3898
3057
|
}
|
|
3899
3058
|
const indexedEvent = streamEvents.find((event) => event.revision === revision);
|
|
3900
|
-
const
|
|
3059
|
+
const shardEvent = revisionShards[0];
|
|
3901
3060
|
if (!indexedEvent?.record_checksum) delete shardEvent.record_checksum;
|
|
3902
3061
|
if (!indexedEvent
|
|
3903
3062
|
|| JSON.stringify(stableValue(shardEvent)) !== JSON.stringify(stableValue(indexedEvent))) {
|
|
@@ -4007,7 +3166,6 @@ function verifyFormalState(files, options, verification = {}) {
|
|
|
4007
3166
|
event_shard_count: shards.length,
|
|
4008
3167
|
history_verified: verifyHistory,
|
|
4009
3168
|
retention_receipt_count: retentionReceipts.length,
|
|
4010
|
-
history_archive_count: historyArchives.length,
|
|
4011
3169
|
};
|
|
4012
3170
|
}
|
|
4013
3171
|
|
|
@@ -4017,10 +3175,7 @@ export function executeDeliveryStateCommand(argv, options = {}) {
|
|
|
4017
3175
|
const command = positional[0];
|
|
4018
3176
|
const projectRoot = path.resolve(String(flags.get("--project") || options.projectRoot || process.cwd()));
|
|
4019
3177
|
const files = statePaths(projectRoot);
|
|
4020
|
-
|
|
4021
|
-
if (command === "gate" && ["request", "decide"].includes(positional[1])) {
|
|
4022
|
-
assertGateContractWritable(requiredFlag(flags, "--gate-id"));
|
|
4023
|
-
}
|
|
3178
|
+
assertCurrentNodeInputs(files, command, positional, flags, options);
|
|
4024
3179
|
const descriptor = lifecycleDescriptor(command, positional, flags);
|
|
4025
3180
|
const before = dispatchLifecycle(options, files, descriptor, "before", flags);
|
|
4026
3181
|
if (before.decision === "block") {
|
|
@@ -4032,18 +3187,12 @@ export function executeDeliveryStateCommand(argv, options = {}) {
|
|
|
4032
3187
|
let result;
|
|
4033
3188
|
if (command === "artifact" && positional[1] === "register") {
|
|
4034
3189
|
result = registerArtifact(files, flags, options);
|
|
4035
|
-
} else if (command === "artifact" && positional[1] === "status") {
|
|
4036
|
-
result = changeArtifactStatus(files, flags, options);
|
|
4037
3190
|
} else if (command === "artifact" && positional[1] === "prune") {
|
|
4038
3191
|
result = pruneArtifactHistory(files, flags, options);
|
|
4039
|
-
} else if (command === "artifact" && positional[1] === "compact") {
|
|
4040
|
-
result = compactArtifactHistory(files, flags, options);
|
|
4041
3192
|
} else if (command === "gate" && positional[1] === "request") {
|
|
4042
3193
|
result = requestGate(files, flags, options);
|
|
4043
3194
|
} else if (command === "gate" && positional[1] === "decide") {
|
|
4044
3195
|
result = decideGate(files, flags, options);
|
|
4045
|
-
} else if (command === "contract" && positional[1] === "retire") {
|
|
4046
|
-
result = retireGateContract(files, flags, options);
|
|
4047
3196
|
} else if (command === "scope" && positional[1] === "freeze") {
|
|
4048
3197
|
result = mutateScopeManifest(files, flags, options, false);
|
|
4049
3198
|
} else if (command === "scope" && positional[1] === "amend") {
|
|
@@ -4088,8 +3237,8 @@ export function executeDeliveryStateCommand(argv, options = {}) {
|
|
|
4088
3237
|
});
|
|
4089
3238
|
} else {
|
|
4090
3239
|
throw new Error(
|
|
4091
|
-
"Use inspect, verify, rebuild, snapshot create, artifact register/
|
|
4092
|
-
+ "transition, gate request/decide
|
|
3240
|
+
"Use inspect, verify, rebuild, snapshot create, artifact register/prune, scope freeze/amend/warn, "
|
|
3241
|
+
+ "transition, or gate request/decide.",
|
|
4093
3242
|
);
|
|
4094
3243
|
}
|
|
4095
3244
|
dispatchLifecycle(options, files, descriptor, "after", flags, result);
|