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
|
@@ -8,11 +8,10 @@ import { fileURLToPath } from "node:url";
|
|
|
8
8
|
import { isSeq, parse as parseYaml, parseDocument, stringify as stringifyYaml } from "yaml";
|
|
9
9
|
import { assertPathInside, atomicWrite, filesUnder } from "./fs-utils.mjs";
|
|
10
10
|
import { inspectCodeGraph } from "./codegraph.mjs";
|
|
11
|
+
import { inspectCodebaseDocumentation, inspectConfirmationSummaries } from "./project-bootstrap.mjs";
|
|
11
12
|
import { executeDeliveryStateCommand } from "./delivery-state.mjs";
|
|
12
|
-
import { planPrototypeMigration } from "./prototype-migration.mjs";
|
|
13
13
|
import { inspectHookConfig, managedHookConfigProjection, mergeHookConfig } from "./toml-hooks.mjs";
|
|
14
14
|
import {
|
|
15
|
-
LEGACY_WORKSPACE_CONFIG,
|
|
16
15
|
WORKSPACE_CONFIG,
|
|
17
16
|
gitCommonDirectory,
|
|
18
17
|
gitTopLevel,
|
|
@@ -39,7 +38,6 @@ const GIT_ATTRIBUTE_ENTRIES = [
|
|
|
39
38
|
"/.gitattributes text eol=lf",
|
|
40
39
|
...RAW_GIT_ATTRIBUTE_TARGETS.map((file) => `/${file} -text`),
|
|
41
40
|
];
|
|
42
|
-
const OBSOLETE_IGNORE_ENTRIES = new Set([".ai-delivery-task", ".ai-delivery/backups/"]);
|
|
43
41
|
const INSTALL_MANIFEST = path.join(".workflow", "config", "install-manifest.json");
|
|
44
42
|
const UPGRADE_RUNTIME = path.join(".workflow", "delivery", "runtime", "upgrades");
|
|
45
43
|
const PRESERVED_DETAIL_LIMIT = 200;
|
|
@@ -48,32 +46,18 @@ const DOCTOR_ATTRIBUTE_BATCH_SIZE = 5_000;
|
|
|
48
46
|
const DOCTOR_DETAIL_LIMIT = 200;
|
|
49
47
|
const FORMAL_STATE_INSPECT_MAX_BUFFER = 16 * 1024 * 1024;
|
|
50
48
|
const INSTALL_MANIFEST_SCHEMA = 6;
|
|
51
|
-
const UI_CONTRACT_VERSION = "prototype-first-ui-v1";
|
|
52
|
-
const FORMAL_STATE_YAML = {
|
|
53
|
-
parse: parseYaml,
|
|
54
|
-
stringify: (value) => stringifyYaml(value, { lineWidth: 0 }),
|
|
55
|
-
};
|
|
56
49
|
const MERGED_MANAGED_SURFACES = Object.freeze([
|
|
57
50
|
{ key: "agents", path: "AGENTS.md" },
|
|
58
51
|
{ key: "hook_config", path: ".codex/config.toml" },
|
|
59
52
|
{ key: "gitignore", path: ".gitignore" },
|
|
60
53
|
{ key: "gitattributes", path: ".gitattributes" },
|
|
61
54
|
]);
|
|
62
|
-
const ACTIVE_TASK = path.join(".workflow", "delivery", "runtime", "active-task.json");
|
|
63
|
-
const LEGACY_ACTIVE_TASK = ".ai-delivery-task";
|
|
64
55
|
const WORKFLOW_CONTENT_DIRECTORIES = ["product", "architecture", "experience", "iterations", "releases"];
|
|
65
56
|
const CONTROL_DIRECTORIES = [
|
|
66
57
|
"tasks", "checkpoints", "artifacts", "gates", "versions", "releases", "events", "archives",
|
|
67
|
-
"
|
|
58
|
+
"repairs",
|
|
68
59
|
];
|
|
69
|
-
const ENSURED_CONTROL_DIRECTORIES = ["
|
|
70
|
-
const RETIRED_MANAGED_ASSETS = Object.freeze([
|
|
71
|
-
{
|
|
72
|
-
path: ".workflow/tools/viewer",
|
|
73
|
-
kind: "retired-directory",
|
|
74
|
-
manifest_key: "viewer",
|
|
75
|
-
},
|
|
76
|
-
]);
|
|
60
|
+
const ENSURED_CONTROL_DIRECTORIES = ["repairs"];
|
|
77
61
|
const ESSENTIAL_STARTUP_SKILLS = [
|
|
78
62
|
"ai-delivery-assemble-release",
|
|
79
63
|
"ai-delivery-bootstrap",
|
|
@@ -138,11 +122,9 @@ const V2_PROTOTYPE_DELIVERY_SURFACE_FILES = Object.freeze([
|
|
|
138
122
|
const REQUIRED_PACKAGE_SURFACE_FILES = Object.freeze([
|
|
139
123
|
"LICENSE",
|
|
140
124
|
"bin/ai-delivery.mjs",
|
|
141
|
-
"docs/PROTOTYPE-MIGRATION.zh-CN.md",
|
|
142
125
|
"lib/mock-scenario-pack.mjs",
|
|
143
126
|
"lib/mock-system.mjs",
|
|
144
|
-
"lib/project-
|
|
145
|
-
"lib/prototype-migration.mjs",
|
|
127
|
+
"lib/project-repair.mjs",
|
|
146
128
|
"lib/prototype-page-review.mjs",
|
|
147
129
|
"lib/prototype-review-target.mjs",
|
|
148
130
|
"skills/ai-delivery-checkpoint-task/scripts/hook-event.mjs",
|
|
@@ -344,8 +326,7 @@ function mergeManagedMarkdown(existing, managedTemplate) {
|
|
|
344
326
|
}
|
|
345
327
|
|
|
346
328
|
function mergeGitignore(existing, requiredEntries = []) {
|
|
347
|
-
const lines = existing.split(/\r?\n/)
|
|
348
|
-
.filter((line) => !OBSOLETE_IGNORE_ENTRIES.has(line.trim()));
|
|
329
|
+
const lines = existing.split(/\r?\n/);
|
|
349
330
|
const additions = [...IGNORE_ENTRIES, ...requiredEntries]
|
|
350
331
|
.filter((entry) => !lines.some((line) => line.trim() === entry));
|
|
351
332
|
const retained = lines.join("\n").trimEnd();
|
|
@@ -542,7 +523,7 @@ function bootstrapRuntimeSources(packageRoot, sourceTemplate) {
|
|
|
542
523
|
relative: "bootstrap.mjs",
|
|
543
524
|
},
|
|
544
525
|
...[
|
|
545
|
-
"project-bootstrap.mjs", "workspace.mjs", "yaml-runtime.mjs", "delivery-state.mjs",
|
|
526
|
+
"project-bootstrap.mjs", "communication-confirmation.mjs", "workspace.mjs", "yaml-runtime.mjs", "delivery-state.mjs",
|
|
546
527
|
"history-anchor.mjs", "fs-utils.mjs",
|
|
547
528
|
].map((name) => ({
|
|
548
529
|
source: path.join(packageRoot, "lib", name),
|
|
@@ -706,7 +687,7 @@ function mergedSurfaceManagedDigest(key, text, codePath) {
|
|
|
706
687
|
if (key === "hook_config") return contentDigest(JSON.stringify(managedHookConfigProjection(text)));
|
|
707
688
|
const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
708
689
|
if (key === "gitignore") {
|
|
709
|
-
const managedEntries = new Set([...IGNORE_ENTRIES, `/${codePath}
|
|
690
|
+
const managedEntries = new Set([...IGNORE_ENTRIES, `/${codePath}/`]);
|
|
710
691
|
return contentDigest(JSON.stringify(lines.filter((line) => managedEntries.has(line)).sort()));
|
|
711
692
|
}
|
|
712
693
|
return contentDigest(JSON.stringify(lines.filter((line) => GIT_ATTRIBUTE_ENTRIES.includes(line)).sort()));
|
|
@@ -868,10 +849,11 @@ export function initProject(packageRoot, target, options = {}) {
|
|
|
868
849
|
"backups",
|
|
869
850
|
new Date().toISOString().replaceAll(":", "-"),
|
|
870
851
|
);
|
|
871
|
-
const
|
|
872
|
-
const
|
|
873
|
-
|
|
874
|
-
|
|
852
|
+
const unsupportedRoot = path.join(target, ".ai-delivery");
|
|
853
|
+
const unsupportedActiveTask = path.join(target, ".ai-delivery-task");
|
|
854
|
+
if (fs.existsSync(unsupportedRoot) || fs.existsSync(unsupportedActiveTask)) {
|
|
855
|
+
throw new Error("Unsupported workflow layout detected; install requires the current .workflow contract.");
|
|
856
|
+
}
|
|
875
857
|
let previousManifest = null;
|
|
876
858
|
try {
|
|
877
859
|
previousManifest = JSON.parse(fs.readFileSync(path.join(target, INSTALL_MANIFEST), "utf8"));
|
|
@@ -889,24 +871,9 @@ export function initProject(packageRoot, target, options = {}) {
|
|
|
889
871
|
throw new Error(`Refusing package downgrade from ${previousManifest.version} to ${packageVersion}; use --force only after explicit approval.`);
|
|
890
872
|
}
|
|
891
873
|
const packageVersionUpgrade = packageVersionComparison > 0;
|
|
892
|
-
const reinstallManagedRuntime = force ||
|
|
893
|
-
|| packageVersionUpgrade || (previousManifest && previousManifest.schema_version < 5);
|
|
894
|
-
if (!dryRun && hasLegacyRoot) {
|
|
895
|
-
const legacyBackup = path.join(backupRoot, "legacy-ai-delivery");
|
|
896
|
-
fs.mkdirSync(path.dirname(legacyBackup), { recursive: true });
|
|
897
|
-
fs.cpSync(legacyRoot, legacyBackup, { recursive: true });
|
|
898
|
-
}
|
|
899
|
-
if (!dryRun && hasLegacyActiveTask) {
|
|
900
|
-
const taskBackup = path.join(backupRoot, "legacy-active-task.json");
|
|
901
|
-
fs.mkdirSync(path.dirname(taskBackup), { recursive: true });
|
|
902
|
-
fs.copyFileSync(legacyActiveTask, taskBackup);
|
|
903
|
-
}
|
|
874
|
+
const reinstallManagedRuntime = force || packageVersionUpgrade;
|
|
904
875
|
const currentWorkspace = readWorkspaceConfiguration(target, { optional: true });
|
|
905
|
-
const
|
|
906
|
-
const legacyWorkspace = !currentWorkspace && fs.existsSync(legacyWorkspaceFile)
|
|
907
|
-
? readWorkspaceConfiguration(target, { configurationPath: LEGACY_WORKSPACE_CONFIG })
|
|
908
|
-
: null;
|
|
909
|
-
const existingWorkspace = (currentWorkspace || legacyWorkspace)?.configuration;
|
|
876
|
+
const existingWorkspace = currentWorkspace?.configuration;
|
|
910
877
|
const codePath = options.codePath || existingWorkspace?.code_repository?.path || "code";
|
|
911
878
|
const codeId = options.codeId || existingWorkspace?.code_repository?.repo_id || "app";
|
|
912
879
|
const sharedRemote = options.sharedRemote || null;
|
|
@@ -942,23 +909,23 @@ export function initProject(packageRoot, target, options = {}) {
|
|
|
942
909
|
const sourceStandardsIndex = path.join(sourceWorkflow, "standards", "index.yaml");
|
|
943
910
|
const workflowRoot = path.join(target, ".workflow");
|
|
944
911
|
const standardsIndexFile = path.join(workflowRoot, "standards", "index.yaml");
|
|
945
|
-
const
|
|
912
|
+
const unsupportedIndicators = [
|
|
946
913
|
path.join(target, "delivery", "project.yaml"),
|
|
947
914
|
path.join(target, "delivery", "workflow-state.yaml"),
|
|
948
915
|
path.join(target, "delivery", "artifact-registry.yaml"),
|
|
949
916
|
path.join(target, "delivery", "standards-baseline.yaml"),
|
|
950
917
|
path.join(target, "standards", "index.yaml"),
|
|
951
918
|
].filter((candidate) => fs.existsSync(candidate));
|
|
952
|
-
if (fs.existsSync(workflowRoot) &&
|
|
919
|
+
if (fs.existsSync(workflowRoot) && unsupportedIndicators.length) {
|
|
953
920
|
throw new Error(
|
|
954
|
-
"Detected both .workflow and
|
|
921
|
+
"Detected both .workflow and unsupported delivery/standards directories. "
|
|
955
922
|
+ "Resolve the split workflow state before initialization.",
|
|
956
923
|
);
|
|
957
924
|
}
|
|
958
|
-
if (
|
|
925
|
+
if (unsupportedIndicators.length) {
|
|
959
926
|
throw new Error(
|
|
960
|
-
"
|
|
961
|
-
+ "
|
|
927
|
+
"Unsupported workflow directories detected at delivery/ or standards/. "
|
|
928
|
+
+ "Remove them before initialization; refusing to create split workflow state.",
|
|
962
929
|
);
|
|
963
930
|
}
|
|
964
931
|
for (const managedPath of [
|
|
@@ -986,8 +953,6 @@ export function initProject(packageRoot, target, options = {}) {
|
|
|
986
953
|
const hookConflict = fs.existsSync(hookDestination)
|
|
987
954
|
&& !managedFilesEqual(hookDestination, sourceHook);
|
|
988
955
|
if (hookConflict && !reinstallManagedRuntime) conflicts.push(path.relative(target, hookDestination));
|
|
989
|
-
const retiredViewerDestination = path.join(target, ".workflow", "tools", "viewer");
|
|
990
|
-
const retireLegacyViewer = hasLegacyRoot && fs.existsSync(retiredViewerDestination);
|
|
991
956
|
const designReferenceDestination = path.join(target, "ref", "ui");
|
|
992
957
|
let designReferencePlan = null;
|
|
993
958
|
if (!fs.existsSync(designReferenceDestination)) {
|
|
@@ -1064,19 +1029,6 @@ export function initProject(packageRoot, target, options = {}) {
|
|
|
1064
1029
|
? fs.readFileSync(gitattributesFile, "utf8")
|
|
1065
1030
|
: "";
|
|
1066
1031
|
const gitattributesText = mergeGitattributes(existingGitattributes);
|
|
1067
|
-
if (hasLegacyRoot || hasLegacyActiveTask) {
|
|
1068
|
-
actions.push("migrate legacy .ai-delivery runtime into .workflow");
|
|
1069
|
-
if (!dryRun) {
|
|
1070
|
-
if (hasLegacyActiveTask) {
|
|
1071
|
-
const activeTask = path.join(target, ACTIVE_TASK);
|
|
1072
|
-
if (!fs.existsSync(activeTask)) {
|
|
1073
|
-
fs.mkdirSync(path.dirname(activeTask), { recursive: true });
|
|
1074
|
-
fs.copyFileSync(legacyActiveTask, activeTask);
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
1032
|
for (const plan of skillPlans) {
|
|
1081
1033
|
if (plan.type === "copy") {
|
|
1082
1034
|
actions.push(`install .agents/skills/${plan.name}`);
|
|
@@ -1098,9 +1050,6 @@ export function initProject(packageRoot, target, options = {}) {
|
|
|
1098
1050
|
} else if (hookConflict) {
|
|
1099
1051
|
backupAndReplace(target, hookDestination, sourceHook, backupRoot, actions, dryRun);
|
|
1100
1052
|
}
|
|
1101
|
-
if (retireLegacyViewer) {
|
|
1102
|
-
backupAndRemove(target, retiredViewerDestination, backupRoot, actions, dryRun);
|
|
1103
|
-
}
|
|
1104
1053
|
if (designReferencePlan?.type === "copy") {
|
|
1105
1054
|
actions.push("install ref/ui Design Reference Library");
|
|
1106
1055
|
if (!dryRun) {
|
|
@@ -1200,21 +1149,6 @@ export function initProject(packageRoot, target, options = {}) {
|
|
|
1200
1149
|
if (!dryRun) atomicWrite(manifestFile, manifestText);
|
|
1201
1150
|
}
|
|
1202
1151
|
|
|
1203
|
-
if (!dryRun && (hasLegacyRoot || hasLegacyActiveTask)) {
|
|
1204
|
-
for (const required of [
|
|
1205
|
-
path.join(target, WORKSPACE_CONFIG),
|
|
1206
|
-
path.join(target, INSTALL_MANIFEST),
|
|
1207
|
-
path.join(target, ".workflow", "tools", "state", "state.mjs"),
|
|
1208
|
-
path.join(target, ".workflow", "tools", "hooks", "hook-event.mjs"),
|
|
1209
|
-
]) {
|
|
1210
|
-
if (!fs.existsSync(required)) {
|
|
1211
|
-
throw new Error(`Legacy migration is incomplete; preserved .ai-delivery for recovery: ${required}`);
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
|
-
if (hasLegacyRoot) fs.rmSync(legacyRoot, { recursive: true, force: true });
|
|
1215
|
-
if (hasLegacyActiveTask) fs.rmSync(legacyActiveTask, { force: true });
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
1152
|
return {
|
|
1219
1153
|
actions: [...workspace.actions, ...actions],
|
|
1220
1154
|
summary: `${dryRun ? "Dry run" : "Initialized"} ${skillNames.length} workspace-scoped AI delivery skills in ${target}`,
|
|
@@ -1456,11 +1390,9 @@ function planUpgrade(packageRoot, target, options) {
|
|
|
1456
1390
|
validateManagedPath(target, path.join(target, managedPath));
|
|
1457
1391
|
}
|
|
1458
1392
|
const { manifest, manifestFile } = readInstallManifest(target);
|
|
1459
|
-
if (
|
|
1460
|
-
|| manifest.schema_version < 5
|
|
1461
|
-
|| manifest.schema_version > INSTALL_MANIFEST_SCHEMA) {
|
|
1393
|
+
if (manifest.schema_version !== INSTALL_MANIFEST_SCHEMA) {
|
|
1462
1394
|
throw new Error(
|
|
1463
|
-
`Installation manifest schema ${manifest.schema_version} is
|
|
1395
|
+
`Installation manifest schema ${manifest.schema_version} is unsupported; expected current schema ${INSTALL_MANIFEST_SCHEMA}.`,
|
|
1464
1396
|
);
|
|
1465
1397
|
}
|
|
1466
1398
|
const comparison = comparePackageVersions(packageVersion, manifest.version);
|
|
@@ -1471,10 +1403,6 @@ function planUpgrade(packageRoot, target, options) {
|
|
|
1471
1403
|
throw new Error(`Refusing package downgrade from ${manifest.version} to ${packageVersion}; use --force only after explicit approval.`);
|
|
1472
1404
|
}
|
|
1473
1405
|
const workspace = readWorkspaceConfiguration(target);
|
|
1474
|
-
const contractRetirement = executeDeliveryStateCommand([
|
|
1475
|
-
"contract", "retire", "--project", target, "--gate-id", "UX-LF", "--dry-run",
|
|
1476
|
-
], { yaml: FORMAL_STATE_YAML });
|
|
1477
|
-
const prototypeMigration = planPrototypeMigration(target);
|
|
1478
1406
|
const codeRepository = workspace.configuration.code_repository;
|
|
1479
1407
|
if (["path", "repo_id", "remote", "default_branch"].some((key) =>
|
|
1480
1408
|
(manifest.code_repository?.[key] ?? null) !== (codeRepository[key] ?? null))) {
|
|
@@ -1484,18 +1412,12 @@ function planUpgrade(packageRoot, target, options) {
|
|
|
1484
1412
|
}
|
|
1485
1413
|
const sources = installationSources(packageRoot);
|
|
1486
1414
|
const exactAssets = managedAssetInventory(sources, manifest);
|
|
1487
|
-
const retiredAssets = RETIRED_MANAGED_ASSETS
|
|
1488
|
-
.filter((asset) => manifest.managed_assets?.[asset.manifest_key])
|
|
1489
|
-
.map((asset) => ({
|
|
1490
|
-
...asset,
|
|
1491
|
-
previous_digest: manifest.managed_assets[asset.manifest_key].sha256 || null,
|
|
1492
|
-
}));
|
|
1493
1415
|
const mergedAssets = upgradeMergedAssets(
|
|
1494
1416
|
target,
|
|
1495
1417
|
sources,
|
|
1496
1418
|
{ ...manifest, code_repository: codeRepository },
|
|
1497
1419
|
);
|
|
1498
|
-
for (const asset of [...exactAssets, ...
|
|
1420
|
+
for (const asset of [...exactAssets, ...mergedAssets]) {
|
|
1499
1421
|
validateManagedPath(target, installPath(target, asset.path));
|
|
1500
1422
|
}
|
|
1501
1423
|
const additions = [];
|
|
@@ -1552,32 +1474,12 @@ function planUpgrade(packageRoot, target, options) {
|
|
|
1552
1474
|
replacements.push({ path: asset.path, kind: "merged-file", forced: false });
|
|
1553
1475
|
}
|
|
1554
1476
|
}
|
|
1555
|
-
for (const asset of retiredAssets) {
|
|
1556
|
-
const destination = installPath(target, asset.path);
|
|
1557
|
-
asset.destination = destination;
|
|
1558
|
-
const currentDigest = pathDigest(destination);
|
|
1559
|
-
asset.current_digest = currentDigest;
|
|
1560
|
-
if (currentDigest === null) {
|
|
1561
|
-
preserved.push({ path: asset.path, reason: "retired managed asset is already absent" });
|
|
1562
|
-
} else if (asset.previous_digest && currentDigest === asset.previous_digest) {
|
|
1563
|
-
removals.push({ path: asset.path, kind: asset.kind, forced: false });
|
|
1564
|
-
} else {
|
|
1565
|
-
conflicts.push({
|
|
1566
|
-
path: asset.path,
|
|
1567
|
-
reason: asset.previous_digest
|
|
1568
|
-
? "retired managed asset differs from the installed manifest digest"
|
|
1569
|
-
: "installed manifest does not prove ownership of the retired managed asset",
|
|
1570
|
-
});
|
|
1571
|
-
if (force) removals.push({ path: asset.path, kind: asset.kind, forced: true });
|
|
1572
|
-
}
|
|
1573
|
-
}
|
|
1574
1477
|
const preservedWorkflow = preservedWorkflowFiles(
|
|
1575
1478
|
target,
|
|
1576
1479
|
[
|
|
1577
1480
|
...exactAssets
|
|
1578
1481
|
.filter((asset) => asset.kind !== "ensure-directory")
|
|
1579
1482
|
.map((asset) => asset.path),
|
|
1580
|
-
...retiredAssets.map((asset) => asset.path),
|
|
1581
1483
|
...mergedAssets.map((asset) => asset.path),
|
|
1582
1484
|
],
|
|
1583
1485
|
);
|
|
@@ -1599,7 +1501,6 @@ function planUpgrade(packageRoot, target, options) {
|
|
|
1599
1501
|
codeRepository,
|
|
1600
1502
|
sources,
|
|
1601
1503
|
exactAssets,
|
|
1602
|
-
retiredAssets,
|
|
1603
1504
|
mergedAssets,
|
|
1604
1505
|
additions,
|
|
1605
1506
|
replacements,
|
|
@@ -1609,8 +1510,6 @@ function planUpgrade(packageRoot, target, options) {
|
|
|
1609
1510
|
conflicts,
|
|
1610
1511
|
backups,
|
|
1611
1512
|
comparison,
|
|
1612
|
-
contractRetirement,
|
|
1613
|
-
prototypeMigration,
|
|
1614
1513
|
};
|
|
1615
1514
|
}
|
|
1616
1515
|
|
|
@@ -1704,17 +1603,6 @@ function applyUpgradePlan(target, plan, packageVersion) {
|
|
|
1704
1603
|
apply: () => atomicWrite(asset.destination, asset.desired),
|
|
1705
1604
|
});
|
|
1706
1605
|
}
|
|
1707
|
-
for (const asset of plan.retiredAssets) {
|
|
1708
|
-
if (!changes.has(asset.path)) continue;
|
|
1709
|
-
applyReceiptBackedAsset({
|
|
1710
|
-
target,
|
|
1711
|
-
upgradeRoot,
|
|
1712
|
-
asset,
|
|
1713
|
-
operation: "remove",
|
|
1714
|
-
receiptAssets,
|
|
1715
|
-
apply: () => fs.rmSync(asset.destination, { recursive: true, force: true }),
|
|
1716
|
-
});
|
|
1717
|
-
}
|
|
1718
1606
|
const manifestRelative = INSTALL_MANIFEST.replaceAll("\\", "/");
|
|
1719
1607
|
const manifestBackup = backupUpgradePath(target, upgradeRoot, manifestRelative);
|
|
1720
1608
|
const nextManifest = buildInstallManifest(
|
|
@@ -1742,30 +1630,10 @@ function applyUpgradePlan(target, plan, packageVersion) {
|
|
|
1742
1630
|
to_version: packageVersion,
|
|
1743
1631
|
applied_at: new Date().toISOString(),
|
|
1744
1632
|
formal_event_tree_fingerprint: formalEventFingerprint,
|
|
1745
|
-
contract_retirement: {
|
|
1746
|
-
gate_id: plan.contractRetirement.gate_id,
|
|
1747
|
-
contract_version: UI_CONTRACT_VERSION,
|
|
1748
|
-
affected_iterations: plan.contractRetirement.affected_iterations,
|
|
1749
|
-
expected_scope_revisions: plan.contractRetirement.expected_scope_revisions,
|
|
1750
|
-
},
|
|
1751
1633
|
assets: receiptAssets,
|
|
1752
1634
|
};
|
|
1753
1635
|
atomicWrite(path.join(upgradeRoot, "receipt.json"), `${JSON.stringify(receipt, null, 2)}\n`);
|
|
1754
|
-
|
|
1755
|
-
"contract", "retire", "--project", target,
|
|
1756
|
-
"--gate-id", "UX-LF",
|
|
1757
|
-
"--actor", "system:package-upgrade",
|
|
1758
|
-
"--reason", "prototype-first UI contract",
|
|
1759
|
-
"--contract-version", UI_CONTRACT_VERSION,
|
|
1760
|
-
];
|
|
1761
|
-
const expectedRevisions = Object.values(plan.contractRetirement.expected_scope_revisions);
|
|
1762
|
-
if (expectedRevisions.length === 1) {
|
|
1763
|
-
retirementArgs.push("--expected-scope-revision", String(expectedRevisions[0]));
|
|
1764
|
-
}
|
|
1765
|
-
const contractRetirement = executeDeliveryStateCommand(retirementArgs, {
|
|
1766
|
-
yaml: FORMAL_STATE_YAML,
|
|
1767
|
-
});
|
|
1768
|
-
return { upgradeId, contractRetirement };
|
|
1636
|
+
return { upgradeId };
|
|
1769
1637
|
} catch (error) {
|
|
1770
1638
|
restoreUpgradeAssets(target, upgradeRoot, receiptAssets);
|
|
1771
1639
|
fs.rmSync(upgradeRoot, { recursive: true, force: true });
|
|
@@ -1796,7 +1664,6 @@ function rollbackUpgrade(packageRoot, target, upgradeId) {
|
|
|
1796
1664
|
const sources = installationSources(packageRoot);
|
|
1797
1665
|
const allowedPaths = new Set([
|
|
1798
1666
|
...managedAssetInventory(sources).map((asset) => asset.path),
|
|
1799
|
-
...RETIRED_MANAGED_ASSETS.map((asset) => asset.path),
|
|
1800
1667
|
...MERGED_MANAGED_SURFACES.map((surface) => surface.path),
|
|
1801
1668
|
INSTALL_MANIFEST.replaceAll("\\", "/"),
|
|
1802
1669
|
]);
|
|
@@ -1866,26 +1733,10 @@ export function upgradeProject(packageRoot, target, options = {}) {
|
|
|
1866
1733
|
conflicts: plan.conflicts,
|
|
1867
1734
|
backups: plan.backups,
|
|
1868
1735
|
},
|
|
1869
|
-
compatibility: {
|
|
1870
|
-
installation_manifest: "compatible",
|
|
1871
|
-
package_version: plan.comparison > 0 ? "upgrade" : plan.comparison === 0 ? "same-version repair" : "forced downgrade",
|
|
1872
|
-
},
|
|
1873
1736
|
code_repository: {
|
|
1874
1737
|
path: plan.codeRepository.path,
|
|
1875
1738
|
write_planned: false,
|
|
1876
1739
|
},
|
|
1877
|
-
contract_retirement: {
|
|
1878
|
-
...plan.contractRetirement,
|
|
1879
|
-
contract_version: UI_CONTRACT_VERSION,
|
|
1880
|
-
write_planned: plan.contractRetirement.affected_iterations.length > 0,
|
|
1881
|
-
},
|
|
1882
|
-
prototype_migration: {
|
|
1883
|
-
...plan.prototypeMigration,
|
|
1884
|
-
required_follow_up: {
|
|
1885
|
-
command: "ai-delivery migrate",
|
|
1886
|
-
write_planned: false,
|
|
1887
|
-
},
|
|
1888
|
-
},
|
|
1889
1740
|
};
|
|
1890
1741
|
if (dryRun) return report;
|
|
1891
1742
|
const blockingConflicts = plan.conflicts.filter((item) => !force || item.forceable === false);
|
|
@@ -1897,12 +1748,6 @@ export function upgradeProject(packageRoot, target, options = {}) {
|
|
|
1897
1748
|
}
|
|
1898
1749
|
const applied = applyUpgradePlan(target, plan, packageVersion);
|
|
1899
1750
|
report.upgrade_id = applied.upgradeId;
|
|
1900
|
-
report.contract_retirement = {
|
|
1901
|
-
...report.contract_retirement,
|
|
1902
|
-
...applied.contractRetirement,
|
|
1903
|
-
contract_version: UI_CONTRACT_VERSION,
|
|
1904
|
-
write_planned: false,
|
|
1905
|
-
};
|
|
1906
1751
|
report.applied = true;
|
|
1907
1752
|
return report;
|
|
1908
1753
|
}
|
|
@@ -2074,6 +1919,28 @@ export function doctorProject(target, options = {}) {
|
|
|
2074
1919
|
|
|
2075
1920
|
const workflowGit = gitTopLevel(target);
|
|
2076
1921
|
const codeGit = workspace ? gitTopLevel(workspace.codeRoot) : null;
|
|
1922
|
+
if (workspace) {
|
|
1923
|
+
const documentation = inspectCodebaseDocumentation(workspace.codeRoot);
|
|
1924
|
+
checks.push({
|
|
1925
|
+
ok: documentation.status !== "invalid",
|
|
1926
|
+
required: false,
|
|
1927
|
+
scope: "codebase-documentation",
|
|
1928
|
+
message: documentation.status === "missing"
|
|
1929
|
+
? "code repository handbook/index are not installed; Bootstrap or maintainers may add them"
|
|
1930
|
+
: `code repository handbook/index status: ${documentation.status}`,
|
|
1931
|
+
});
|
|
1932
|
+
const confirmations = inspectConfirmationSummaries(target);
|
|
1933
|
+
checks.push({
|
|
1934
|
+
ok: confirmations.status !== "invalid",
|
|
1935
|
+
required: false,
|
|
1936
|
+
scope: "communication-confirmation",
|
|
1937
|
+
message: confirmations.status === "absent"
|
|
1938
|
+
? "no communication confirmation summaries are present"
|
|
1939
|
+
: confirmations.status === "invalid"
|
|
1940
|
+
? `communication confirmation summaries are invalid: ${confirmations.errors.join("; ")}`
|
|
1941
|
+
: `${confirmations.summaries.length} communication confirmation summary(ies) validate; frontier=${confirmations.summaries.flatMap((item) => item.frontier).join(",") || "empty"}`,
|
|
1942
|
+
});
|
|
1943
|
+
}
|
|
2077
1944
|
const workflowCommonGit = workflowGit ? gitCommonDirectory(target) : null;
|
|
2078
1945
|
const codeCommonGit = codeGit && workspace ? gitCommonDirectory(workspace.codeRoot) : null;
|
|
2079
1946
|
checks.push({
|
|
@@ -2231,6 +2098,7 @@ export function doctorProject(target, options = {}) {
|
|
|
2231
2098
|
const bootstrapFiles = [
|
|
2232
2099
|
"bootstrap.mjs",
|
|
2233
2100
|
"project-bootstrap.mjs",
|
|
2101
|
+
"communication-confirmation.mjs",
|
|
2234
2102
|
"workspace.mjs",
|
|
2235
2103
|
"yaml-runtime.mjs",
|
|
2236
2104
|
"delivery-state.mjs",
|
|
@@ -2496,8 +2364,8 @@ export function doctorProject(target, options = {}) {
|
|
|
2496
2364
|
});
|
|
2497
2365
|
}
|
|
2498
2366
|
checks.push({
|
|
2499
|
-
ok: !fs.existsSync(path.join(target, ".ai-delivery")) && !fs.existsSync(path.join(target,
|
|
2500
|
-
message: "
|
|
2367
|
+
ok: !fs.existsSync(path.join(target, ".ai-delivery")) && !fs.existsSync(path.join(target, ".ai-delivery-task")),
|
|
2368
|
+
message: "unsupported workflow paths are absent",
|
|
2501
2369
|
});
|
|
2502
2370
|
checks.push({ ok: fs.existsSync(path.join(target, "AGENTS.md")), message: "project AGENTS.md is present" });
|
|
2503
2371
|
return { checks, ok: checks.filter((check) => check.required !== false).every((check) => check.ok) };
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { parse as parseYaml } from "yaml";
|
|
5
|
+
import { atomicWrite, filesUnder } from "./fs-utils.mjs";
|
|
6
|
+
|
|
7
|
+
function toPosix(value) {
|
|
8
|
+
return value.replaceAll("\\", "/");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function sha256(value) {
|
|
12
|
+
return crypto.createHash("sha256").update(value).digest("hex");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function treeDigest(root) {
|
|
16
|
+
const hash = crypto.createHash("sha256");
|
|
17
|
+
for (const file of filesUnder(root)) {
|
|
18
|
+
hash.update(toPosix(path.relative(root, file)));
|
|
19
|
+
hash.update("\0");
|
|
20
|
+
hash.update(fs.readFileSync(file));
|
|
21
|
+
hash.update("\0");
|
|
22
|
+
}
|
|
23
|
+
return hash.digest("hex");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function readStructured(file, label) {
|
|
27
|
+
try {
|
|
28
|
+
const text = fs.readFileSync(file, "utf8");
|
|
29
|
+
const value = path.extname(file).toLowerCase() === ".json" ? JSON.parse(text) : parseYaml(text);
|
|
30
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("expected a mapping");
|
|
31
|
+
return value;
|
|
32
|
+
} catch (error) {
|
|
33
|
+
throw new Error(`Cannot read ${label} (${toPosix(file)}): ${error.message}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function stableValue(value) {
|
|
38
|
+
if (Array.isArray(value)) return value.map(stableValue);
|
|
39
|
+
if (!value || typeof value !== "object") return value;
|
|
40
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stableValue(value[key])]));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function stableJson(value) {
|
|
44
|
+
return JSON.stringify(stableValue(value));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function uniqueSorted(values) {
|
|
48
|
+
return [...new Set((Array.isArray(values) ? values : []).map(String))].sort();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function unsignedRepairRecord(record) {
|
|
52
|
+
const unsigned = { ...record };
|
|
53
|
+
delete unsigned.record_checksum;
|
|
54
|
+
return unsigned;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function readRepairTransactions(project) {
|
|
58
|
+
const directory = path.join(project, ".workflow", "control", "repairs", "transactions");
|
|
59
|
+
const records = [];
|
|
60
|
+
let previousChecksum = null;
|
|
61
|
+
for (const file of filesUnder(directory).filter((item) => item.endsWith(".json"))) {
|
|
62
|
+
const record = readStructured(file, "repair transaction");
|
|
63
|
+
if (record.schema_version !== 1
|
|
64
|
+
|| !Number.isInteger(record.sequence)
|
|
65
|
+
|| record.sequence !== records.length + 1
|
|
66
|
+
|| record.previous_checksum !== previousChecksum
|
|
67
|
+
|| record.record_checksum !== sha256(stableJson(unsignedRepairRecord(record)))) {
|
|
68
|
+
throw new Error(`Repair transaction chain is invalid: ${toPosix(path.relative(project, file))}.`);
|
|
69
|
+
}
|
|
70
|
+
records.push(record);
|
|
71
|
+
previousChecksum = record.record_checksum;
|
|
72
|
+
}
|
|
73
|
+
return records;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function archiveIterationId(value) {
|
|
77
|
+
const iterationId = String(value || "");
|
|
78
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(iterationId)) {
|
|
79
|
+
throw new Error(`Invalid archive iteration ID: ${iterationId || "missing"}.`);
|
|
80
|
+
}
|
|
81
|
+
return iterationId;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function verifiedArchiveInvalidations(project, records) {
|
|
85
|
+
for (const record of records.filter((item) => item.transaction_type === "archive-invalidation")) {
|
|
86
|
+
const iterationId = archiveIterationId(record.target?.iteration_id);
|
|
87
|
+
const expectedArchivePath = `.workflow/control/archives/versions/${iterationId}`;
|
|
88
|
+
const expectedManifestPath = `${expectedArchivePath}/version-manifest.json`;
|
|
89
|
+
if (record.target.archive_path !== expectedArchivePath
|
|
90
|
+
|| record.target.manifest_path !== expectedManifestPath
|
|
91
|
+
|| !/^[a-f0-9]{64}$/.test(record.target.archive_sha256 || "")
|
|
92
|
+
|| !/^[a-f0-9]{64}$/.test(record.target.manifest_sha256 || "")) {
|
|
93
|
+
throw new Error(`Archive invalidation target is invalid: ${record.transaction_id}.`);
|
|
94
|
+
}
|
|
95
|
+
const archiveDirectory = path.join(project, ...expectedArchivePath.split("/"));
|
|
96
|
+
const manifestFile = path.join(project, ...expectedManifestPath.split("/"));
|
|
97
|
+
if (!fs.existsSync(manifestFile)
|
|
98
|
+
|| treeDigest(archiveDirectory) !== record.target.archive_sha256
|
|
99
|
+
|| sha256(fs.readFileSync(manifestFile)) !== record.target.manifest_sha256) {
|
|
100
|
+
throw new Error(`Invalidated archive ${iterationId} bytes changed after ${record.transaction_id}.`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function repairTransactionPath(sequence, transactionId) {
|
|
106
|
+
return `.workflow/control/repairs/transactions/${String(sequence).padStart(6, "0")}-${transactionId}.json`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function repairProject(project, options = {}) {
|
|
110
|
+
const root = path.resolve(project);
|
|
111
|
+
if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) {
|
|
112
|
+
throw new Error(`Project directory does not exist: ${root}`);
|
|
113
|
+
}
|
|
114
|
+
const iterationId = archiveIterationId(options.invalidateArchive);
|
|
115
|
+
const reason = String(options.reason || "").trim();
|
|
116
|
+
const actor = String(options.actor || "").trim();
|
|
117
|
+
const evidence = uniqueSorted(options.evidence);
|
|
118
|
+
if (!reason) throw new Error("Archive invalidation requires --reason.");
|
|
119
|
+
if (!actor.startsWith("human:")) {
|
|
120
|
+
throw new Error("Archive invalidation requires an explicit human actor through --actor human:<identity>.");
|
|
121
|
+
}
|
|
122
|
+
if (evidence.length < 2) {
|
|
123
|
+
throw new Error("Archive invalidation requires at least two --evidence values for RCA and recovery evidence.");
|
|
124
|
+
}
|
|
125
|
+
const archiveDirectory = path.join(root, ".workflow", "control", "archives", "versions", iterationId);
|
|
126
|
+
const manifestFile = path.join(archiveDirectory, "version-manifest.json");
|
|
127
|
+
if (!fs.existsSync(manifestFile)) throw new Error(`Version archive not found: ${iterationId}.`);
|
|
128
|
+
const manifest = readStructured(manifestFile, "version archive manifest");
|
|
129
|
+
if (manifest.iteration_id !== iterationId) {
|
|
130
|
+
throw new Error(`Version archive identity mismatch: expected ${iterationId}, found ${manifest.iteration_id || "missing"}.`);
|
|
131
|
+
}
|
|
132
|
+
const target = {
|
|
133
|
+
iteration_id: iterationId,
|
|
134
|
+
archive_path: toPosix(path.relative(root, archiveDirectory)),
|
|
135
|
+
manifest_path: toPosix(path.relative(root, manifestFile)),
|
|
136
|
+
manifest_sha256: sha256(fs.readFileSync(manifestFile)),
|
|
137
|
+
archive_sha256: treeDigest(archiveDirectory),
|
|
138
|
+
original_bytes_preserved: true,
|
|
139
|
+
};
|
|
140
|
+
const identity = { transaction_type: "archive-invalidation", target, correction: { actor, reason, evidence } };
|
|
141
|
+
const idempotencyKey = `archive-invalidation:${iterationId}:${sha256(stableJson(identity))}`;
|
|
142
|
+
const existingTransactions = readRepairTransactions(root);
|
|
143
|
+
verifiedArchiveInvalidations(root, existingTransactions);
|
|
144
|
+
const existing = existingTransactions.find((record) => record.idempotency_key === idempotencyKey);
|
|
145
|
+
const sequence = existing?.sequence || existingTransactions.length + 1;
|
|
146
|
+
const transactionId = existing?.transaction_id || `REPAIR-${sha256(idempotencyKey).slice(0, 16).toUpperCase()}`;
|
|
147
|
+
const transactionPath = repairTransactionPath(sequence, transactionId);
|
|
148
|
+
const report = {
|
|
149
|
+
command: "repair",
|
|
150
|
+
action: "archive-invalidation",
|
|
151
|
+
dry_run: Boolean(options.dryRun),
|
|
152
|
+
applied: false,
|
|
153
|
+
forward_only: true,
|
|
154
|
+
original_bytes_preserved: true,
|
|
155
|
+
target,
|
|
156
|
+
actor,
|
|
157
|
+
reason,
|
|
158
|
+
evidence,
|
|
159
|
+
transaction: {
|
|
160
|
+
transaction_id: transactionId,
|
|
161
|
+
path: transactionPath,
|
|
162
|
+
previous_checksum: existing?.previous_checksum || existingTransactions.at(-1)?.record_checksum || null,
|
|
163
|
+
},
|
|
164
|
+
eligibility: existing
|
|
165
|
+
? { iteration_id: iterationId, current: "invalidated", source_transaction_id: transactionId }
|
|
166
|
+
: { iteration_id: iterationId, current: "eligible", source_transaction_id: null },
|
|
167
|
+
};
|
|
168
|
+
if (report.dry_run || existing) return report;
|
|
169
|
+
|
|
170
|
+
const repairRoot = path.join(root, ".workflow", "control", "repairs");
|
|
171
|
+
const lockFile = path.join(repairRoot, ".repair.lock");
|
|
172
|
+
fs.mkdirSync(repairRoot, { recursive: true });
|
|
173
|
+
let lock;
|
|
174
|
+
try {
|
|
175
|
+
lock = fs.openSync(lockFile, "wx");
|
|
176
|
+
} catch (error) {
|
|
177
|
+
throw new Error(`Repair transaction is locked by another writer: ${error.message}`);
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
const currentTransactions = readRepairTransactions(root);
|
|
181
|
+
const duplicate = currentTransactions.find((record) => record.idempotency_key === idempotencyKey);
|
|
182
|
+
if (duplicate) {
|
|
183
|
+
report.transaction.transaction_id = duplicate.transaction_id;
|
|
184
|
+
report.transaction.path = repairTransactionPath(duplicate.sequence, duplicate.transaction_id);
|
|
185
|
+
report.eligibility = { iteration_id: iterationId, current: "invalidated", source_transaction_id: duplicate.transaction_id };
|
|
186
|
+
return report;
|
|
187
|
+
}
|
|
188
|
+
if (currentTransactions.length !== existingTransactions.length
|
|
189
|
+
|| (currentTransactions.at(-1)?.record_checksum || null) !== (existingTransactions.at(-1)?.record_checksum || null)) {
|
|
190
|
+
throw new Error("Repair transaction revision changed during apply; rerun the command.");
|
|
191
|
+
}
|
|
192
|
+
if (treeDigest(archiveDirectory) !== target.archive_sha256
|
|
193
|
+
|| sha256(fs.readFileSync(manifestFile)) !== target.manifest_sha256) {
|
|
194
|
+
throw new Error(`Version archive changed during repair apply: ${iterationId}.`);
|
|
195
|
+
}
|
|
196
|
+
const transaction = {
|
|
197
|
+
schema_version: 1,
|
|
198
|
+
sequence,
|
|
199
|
+
transaction_id: transactionId,
|
|
200
|
+
transaction_type: "archive-invalidation",
|
|
201
|
+
idempotency_key: idempotencyKey,
|
|
202
|
+
occurred_at: new Date().toISOString(),
|
|
203
|
+
target,
|
|
204
|
+
correction: { actor, reason, evidence },
|
|
205
|
+
previous_checksum: existingTransactions.at(-1)?.record_checksum || null,
|
|
206
|
+
};
|
|
207
|
+
transaction.record_checksum = sha256(stableJson(transaction));
|
|
208
|
+
atomicWrite(path.join(root, ...transactionPath.split("/")), `${JSON.stringify(transaction, null, 2)}\n`);
|
|
209
|
+
report.applied = true;
|
|
210
|
+
report.transaction.previous_checksum = transaction.previous_checksum;
|
|
211
|
+
report.eligibility = { iteration_id: iterationId, current: "invalidated", source_transaction_id: transactionId };
|
|
212
|
+
return report;
|
|
213
|
+
} finally {
|
|
214
|
+
if (lock !== undefined) fs.closeSync(lock);
|
|
215
|
+
fs.rmSync(lockFile, { force: true });
|
|
216
|
+
}
|
|
217
|
+
}
|