ultra-builder-pro-cli 0.25.1 → 0.26.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (243) hide show
  1. package/.ultra-template/changes/abandoned/.gitkeep +0 -0
  2. package/.ultra-template/contexts/TEMPLATE.md +94 -0
  3. package/.ultra-template/decisions/.gitkeep +1 -0
  4. package/.ultra-template/evidence/.gitkeep +1 -0
  5. package/.ultra-template/north-star.md +46 -0
  6. package/.ultra-template/research/.gitkeep +1 -0
  7. package/.ultra-template/specs/discovery.md +2 -2
  8. package/.ultra-template/tasks.json +3 -0
  9. package/.ultra-template/test-report.json +27 -0
  10. package/CHANGELOG.md +107 -1
  11. package/README.md +217 -579
  12. package/adapters/_shared/delegate-profiles/kimi-read-only.md +15 -0
  13. package/adapters/_shared/delegate-profiles/kimi-write.md +17 -0
  14. package/adapters/_shared/file-ops.cjs +76 -2
  15. package/adapters/_shared/host-profile.cjs +97 -0
  16. package/adapters/_shared/plugin-core.cjs +204 -0
  17. package/adapters/_shared/runtime-assets.cjs +31 -96
  18. package/adapters/claude.js +92 -291
  19. package/adapters/codex.js +309 -842
  20. package/adapters/grok.js +143 -1469
  21. package/adapters/kimi.js +124 -1053
  22. package/adapters/opencode.js +230 -742
  23. package/bin/delegate-worker.cjs +328 -0
  24. package/bin/delegate.cjs +463 -0
  25. package/bin/install.js +38 -11
  26. package/docs/ARCHITECTURE.md +125 -216
  27. package/docs/ARTIFACT-AUTHORITY.md +90 -268
  28. package/docs/DECISIONS.md +77 -87
  29. package/docs/PHILOSOPHY.md +208 -0
  30. package/docs/PLUGIN-ISOLATION-CONTRACT.md +65 -39
  31. package/docs/RUNTIME-COMPAT-MATRIX.md +140 -155
  32. package/docs/SKILL-AUTHORING.md +94 -73
  33. package/docs/WORKFLOW-LIFECYCLE.md +97 -189
  34. package/hooks/README.md +19 -37
  35. package/hooks/_common.py +113 -0
  36. package/hooks/adapters/codex.py +9 -16
  37. package/hooks/adapters/grok.py +5 -5
  38. package/hooks/adapters/kimi.py +8 -14
  39. package/hooks/block_dangerous_commands.py +201 -0
  40. package/hooks/compact_context.py +65 -0
  41. package/hooks/mid_workflow_recall.py +25 -0
  42. package/hooks/post_edit_guard.py +105 -0
  43. package/hooks/session_context.py +30 -0
  44. package/package.json +17 -52
  45. package/skills/ultra-change/SKILL.md +115 -50
  46. package/skills/ultra-change/references/change-contract.md +68 -3
  47. package/skills/ultra-delegate/SKILL.md +89 -0
  48. package/skills/ultra-delegate/references/delegation-contract.md +59 -0
  49. package/skills/ultra-delegate/scripts/delegate_wait.py +86 -0
  50. package/skills/ultra-deliver/SKILL.md +98 -42
  51. package/skills/ultra-deliver/references/baseline-reconciliation.md +39 -20
  52. package/skills/ultra-dev/SKILL.md +115 -62
  53. package/skills/ultra-dev/references/debugging.md +22 -0
  54. package/skills/ultra-domain-modeling/SKILL.md +79 -0
  55. package/skills/ultra-grilling/SKILL.md +118 -0
  56. package/skills/ultra-grilling/references/reframing.md +72 -0
  57. package/skills/ultra-init/SKILL.md +102 -37
  58. package/skills/ultra-init/references/brownfield-adoption.md +33 -83
  59. package/skills/ultra-init/scripts/init_project.cjs +84 -0
  60. package/skills/ultra-plan/SKILL.md +101 -68
  61. package/skills/ultra-research/SKILL.md +68 -48
  62. package/skills/ultra-research/references/00-problem-validation.md +5 -5
  63. package/skills/ultra-research/references/01-opportunity-discovery.md +3 -3
  64. package/skills/ultra-research/references/02-market-assessment.md +5 -4
  65. package/skills/ultra-research/references/03-competitive-landscape.md +3 -3
  66. package/skills/ultra-research/references/04-product-strategy.md +3 -3
  67. package/skills/ultra-research/references/05-assumptions-validation.md +5 -5
  68. package/skills/ultra-research/references/10-user-personas.md +3 -2
  69. package/skills/ultra-research/references/11-user-scenarios.md +3 -3
  70. package/skills/ultra-research/references/20-user-stories.md +4 -3
  71. package/skills/ultra-research/references/21-features-scope.md +3 -2
  72. package/skills/ultra-research/references/22-success-metrics.md +3 -3
  73. package/skills/ultra-research/references/30-architecture-context.md +3 -3
  74. package/skills/ultra-research/references/31-solution-strategy.md +3 -3
  75. package/skills/ultra-research/references/32-building-blocks.md +3 -3
  76. package/skills/ultra-research/references/40-deployment.md +3 -3
  77. package/skills/ultra-research/references/41-quality-risks.md +3 -3
  78. package/skills/ultra-research/references/99-synthesis.md +7 -7
  79. package/skills/ultra-review/SKILL.md +84 -32
  80. package/skills/ultra-review/references/code.md +17 -0
  81. package/skills/ultra-review/references/comments.md +12 -0
  82. package/skills/ultra-review/references/design.md +12 -0
  83. package/skills/ultra-review/references/errors.md +13 -0
  84. package/skills/ultra-review/references/spec.md +13 -0
  85. package/skills/ultra-review/references/tests.md +12 -0
  86. package/skills/ultra-review/references/worker-packet.md +40 -0
  87. package/skills/ultra-review/scripts/review_wait.py +26 -15
  88. package/skills/ultra-status/SKILL.md +61 -36
  89. package/skills/ultra-tdd/SKILL.md +81 -0
  90. package/skills/ultra-tdd/references/templates/README.md +20 -0
  91. package/skills/ultra-tdd/references/templates/feature-flag-default-audit.sh +79 -0
  92. package/skills/ultra-tdd/references/templates/persistence-real.ts +78 -0
  93. package/skills/ultra-tdd/references/templates/testcontainer-postgres.py +78 -0
  94. package/skills/ultra-tdd/references/templates/testcontainer-postgres.ts +73 -0
  95. package/skills/ultra-tdd/references/templates/vertical-slice.ts +66 -0
  96. package/skills/ultra-tdd/references/test-execution.md +18 -0
  97. package/skills/ultra-test/SKILL.md +75 -30
  98. package/skills/ultra-test/references/export-syntax.md +18 -0
  99. package/skills/ultra-test/scripts/worktree_digest.cjs +54 -0
  100. package/skills/ultra-think/SKILL.md +49 -27
  101. package/skills/ultra-think/references/autonomy-boundary.md +48 -0
  102. package/skills/ultra-think/references/interaction-boundary.md +18 -33
  103. package/.ultra-template/docs/research/README.md +0 -55
  104. package/.ultra-template/reports/templates/delivery-report.json +0 -17
  105. package/.ultra-template/reports/templates/test-report.json +0 -18
  106. package/.ultra-template/tasks/tasks.json +0 -15
  107. package/.ultra-template/templates/task-context.md +0 -19
  108. package/adapters/_shared/codex-assets.cjs +0 -725
  109. package/adapters/_shared/codex-ultra-tools-entry.cjs +0 -11
  110. package/adapters/_shared/interaction-contract.cjs +0 -129
  111. package/adapters/_shared/path-rewrite.cjs +0 -79
  112. package/adapters/_shared/runtime-native.cjs +0 -132
  113. package/adapters/_shared/settings-merge.cjs +0 -76
  114. package/agents/code-reviewer.md +0 -42
  115. package/agents/debugger.md +0 -40
  116. package/agents/review-code.md +0 -49
  117. package/agents/review-comments.md +0 -38
  118. package/agents/review-coordinator.md +0 -56
  119. package/agents/review-design.md +0 -40
  120. package/agents/review-errors.md +0 -43
  121. package/agents/review-spec.md +0 -39
  122. package/agents/review-tests.md +0 -40
  123. package/agents/tdd-runner.md +0 -38
  124. package/bin/orchestrator.js +0 -383
  125. package/commands/ultra-change.md +0 -10
  126. package/commands/ultra-deliver.md +0 -10
  127. package/commands/ultra-dev.md +0 -10
  128. package/commands/ultra-doctor.md +0 -10
  129. package/commands/ultra-init.md +0 -10
  130. package/commands/ultra-plan.md +0 -10
  131. package/commands/ultra-research.md +0 -10
  132. package/commands/ultra-review.md +0 -10
  133. package/commands/ultra-status.md +0 -10
  134. package/commands/ultra-test.md +0 -10
  135. package/commands/ultra-think.md +0 -10
  136. package/docs/AGENT-CONTEXT.md +0 -191
  137. package/docs/COMMIT-HASH-BACKFILL.md +0 -51
  138. package/docs/LEGACY-CLI-CROSSWALK.md +0 -175
  139. package/docs/ROADMAP.md +0 -101
  140. package/docs/STATE-DB-ACCESS-POLICY.md +0 -161
  141. package/hooks/active_task_context.py +0 -152
  142. package/hooks/context_envelope.py +0 -100
  143. package/hooks/health_check.py +0 -196
  144. package/hooks/pre_stop_check.py +0 -66
  145. package/hooks/runtime_paths.py +0 -396
  146. package/hooks/subagent_tracker.py +0 -88
  147. package/hooks/workflow_checkpoint.py +0 -76
  148. package/hooks/workflow_context.py +0 -48
  149. package/hooks/workflow_resume.py +0 -84
  150. package/mcp-server/hook-context.cjs +0 -38
  151. package/mcp-server/hook-event.cjs +0 -34
  152. package/mcp-server/lib/archive-journal.cjs +0 -1119
  153. package/mcp-server/lib/archive-mutation-worker.py +0 -365
  154. package/mcp-server/lib/artifact-registry.cjs +0 -1232
  155. package/mcp-server/lib/baseline-workflow.cjs +0 -1193
  156. package/mcp-server/lib/canonical-json.cjs +0 -19
  157. package/mcp-server/lib/change-authority.cjs +0 -26
  158. package/mcp-server/lib/change-packet.cjs +0 -790
  159. package/mcp-server/lib/change-workflow.cjs +0 -65
  160. package/mcp-server/lib/context-envelope.cjs +0 -581
  161. package/mcp-server/lib/context-paths.cjs +0 -142
  162. package/mcp-server/lib/decision-records.cjs +0 -423
  163. package/mcp-server/lib/delivery-transaction.cjs +0 -471
  164. package/mcp-server/lib/doctor-backup-worker.cjs +0 -328
  165. package/mcp-server/lib/doctor.cjs +0 -627
  166. package/mcp-server/lib/git-bootstrap.cjs +0 -651
  167. package/mcp-server/lib/init-project.cjs +0 -1021
  168. package/mcp-server/lib/kernel-change-workflow.cjs +0 -2029
  169. package/mcp-server/lib/legacy-change-workflow.cjs +0 -3032
  170. package/mcp-server/lib/managed-file-write.cjs +0 -172
  171. package/mcp-server/lib/plan-checkpoint.cjs +0 -177
  172. package/mcp-server/lib/plan-publication-worker.cjs +0 -830
  173. package/mcp-server/lib/plan-store.cjs +0 -794
  174. package/mcp-server/lib/prd-parser.cjs +0 -83
  175. package/mcp-server/lib/pricing.cjs +0 -44
  176. package/mcp-server/lib/project-context.cjs +0 -178
  177. package/mcp-server/lib/projector.cjs +0 -386
  178. package/mcp-server/lib/provider-refs.cjs +0 -42
  179. package/mcp-server/lib/runtime-paths.cjs +0 -1714
  180. package/mcp-server/lib/runtime-state.cjs +0 -222
  181. package/mcp-server/lib/safe-project-file.cjs +0 -385
  182. package/mcp-server/lib/stage-checkpoints.cjs +0 -463
  183. package/mcp-server/lib/state-authority.cjs +0 -95
  184. package/mcp-server/lib/state-db.cjs +0 -1580
  185. package/mcp-server/lib/state-ops.cjs +0 -1428
  186. package/mcp-server/lib/task-contract.cjs +0 -404
  187. package/mcp-server/lib/task-expander.cjs +0 -105
  188. package/mcp-server/lib/task-ledger.cjs +0 -1964
  189. package/mcp-server/lib/telemetry.cjs +0 -85
  190. package/mcp-server/lib/topo.cjs +0 -131
  191. package/mcp-server/lib/ultra-facade.cjs +0 -2684
  192. package/mcp-server/lib/worker-packet.cjs +0 -358
  193. package/mcp-server/server.cjs +0 -437
  194. package/orchestrator/auto-merge.cjs +0 -128
  195. package/orchestrator/daemon.cjs +0 -264
  196. package/orchestrator/dispatch-rules.cjs +0 -78
  197. package/orchestrator/parallel-orchestrator.cjs +0 -903
  198. package/orchestrator/planner/plan-builder.cjs +0 -125
  199. package/orchestrator/recovery.cjs +0 -188
  200. package/orchestrator/session-close-journal-worker.cjs +0 -5
  201. package/orchestrator/session-close-journal.cjs +0 -1014
  202. package/orchestrator/session-runner.cjs +0 -1153
  203. package/orchestrator/worktree-manager.cjs +0 -282
  204. package/settings.json +0 -7
  205. package/skills/cc-collab/SKILL.md +0 -42
  206. package/skills/code-review-expert/SKILL.md +0 -43
  207. package/skills/code-review-expert/references/correctness-reliability.md +0 -30
  208. package/skills/code-review-expert/references/design-boundaries.md +0 -26
  209. package/skills/code-review-expert/references/removal-plan.md +0 -24
  210. package/skills/codex-collab/SKILL.md +0 -49
  211. package/skills/codex-collab/evals/evals.json +0 -35
  212. package/skills/integration-rules/SKILL.md +0 -32
  213. package/skills/security-rules/SKILL.md +0 -38
  214. package/skills/testing-rules/SKILL.md +0 -32
  215. package/skills/ultra-doctor/SKILL.md +0 -41
  216. package/skills/ultra-plan/references/semantic-preflight.md +0 -20
  217. package/skills/ultra-verify/SKILL.md +0 -46
  218. package/skills/ultra-verify/evals/evals.json +0 -46
  219. package/skills/ultra-verify/references/cross-verify-modes.md +0 -28
  220. package/skills/ultra-verify/references/evidence-status.md +0 -21
  221. package/skills/ultra-verify/scripts/verify_wait.py +0 -94
  222. package/spec/cli-protocol.md +0 -102
  223. package/spec/mcp-tools.yaml +0 -312
  224. package/spec/schemas/baseline-reconciliation.v1.schema.json +0 -73
  225. package/spec/schemas/change-delta.v1.schema.json +0 -153
  226. package/spec/schemas/command-manifest.schema.json +0 -29
  227. package/spec/schemas/context-file.v4.5.schema.json +0 -54
  228. package/spec/schemas/delivery-report.v1.schema.json +0 -66
  229. package/spec/schemas/documentation-reconciliation.v1.schema.json +0 -106
  230. package/spec/schemas/mcp-tools.schema.json +0 -136
  231. package/spec/schemas/skill-manifest.schema.json +0 -24
  232. package/spec/schemas/state-db.sql +0 -641
  233. package/spec/schemas/task-ledger.v2.schema.json +0 -296
  234. package/spec/schemas/tasks.v4.5.schema.json +0 -96
  235. package/spec/schemas/test-report.v1.schema.json +0 -225
  236. package/ultra-tools/cli.cjs +0 -129
  237. package/ultra-tools/commands/db.cjs +0 -226
  238. package/ultra-tools/commands/legacy-memory.cjs +0 -159
  239. package/ultra-tools/commands/migrate.cjs +0 -1386
  240. package/ultra-tools/commands/session.cjs +0 -213
  241. package/ultra-tools/commands/status.cjs +0 -559
  242. package/ultra-tools/commands/system.cjs +0 -498
  243. package/ultra-tools/commands/task.cjs +0 -105
@@ -1,1119 +0,0 @@
1
- 'use strict';
2
-
3
- const fs = require('node:fs');
4
- const path = require('node:path');
5
- const crypto = require('node:crypto');
6
-
7
- const { readStableProjectFile } = require('./safe-project-file.cjs');
8
-
9
- const INTENT_FILE = '.archive-intent.json';
10
- const REBIND_FILE = '.archive-rebind.json';
11
- const INTENT_VERSION = '2.0';
12
- const REBIND_VERSION = '1.0';
13
- const MUTATION_WORKER = path.join(__dirname, 'archive-mutation-worker.py');
14
- const MAX_MUTATION_REQUEST_BYTES = 16 * 1024 * 1024;
15
- const MUTATION_PIN_FIELDS = Object.freeze({
16
- mkdir_dir: Object.freeze(['directory_fd']),
17
- write_atomic: Object.freeze(['directory_fd']),
18
- write_rebind_atomic: Object.freeze(['directory_fd']),
19
- unlink_regular: Object.freeze(['directory_fd']),
20
- rename_dir: Object.freeze([
21
- 'source_parent_fd',
22
- 'destination_parent_fd',
23
- 'guard_parent_fd',
24
- ]),
25
- });
26
- const ACTIVE_ROOT = '.ultra/changes/active';
27
- const ARCHIVE_ROOT = '.ultra/changes/archive';
28
- const CORE_DIRECTORIES = Object.freeze([
29
- ['project', '.'],
30
- ['ultra', '.ultra'],
31
- ['changes', '.ultra/changes'],
32
- ['active', ACTIVE_ROOT],
33
- ['archive', ARCHIVE_ROOT],
34
- ]);
35
-
36
- class ArchiveJournalError extends Error {
37
- constructor(code, message, details = undefined) {
38
- super(message);
39
- this.name = 'ArchiveJournalError';
40
- this.code = code;
41
- if (details !== undefined) this.details = details;
42
- }
43
- }
44
-
45
- function unsafe(message, details = undefined) {
46
- throw new ArchiveJournalError('ARCHIVE_PATH_UNSAFE', message, details);
47
- }
48
-
49
- function identity(stat) {
50
- return {
51
- dev: String(stat.dev),
52
- ino: String(stat.ino),
53
- mode: String(stat.mode),
54
- };
55
- }
56
-
57
- function sameIdentity(left, right) {
58
- return Boolean(left && right)
59
- && left.dev === right.dev
60
- && left.ino === right.ino
61
- && left.mode === right.mode;
62
- }
63
-
64
- function normalizeRelative(value, field) {
65
- if (typeof value !== 'string' || !value.trim()) {
66
- throw new ArchiveJournalError('ARCHIVE_INTENT_INVALID', `${field} must be project-relative`);
67
- }
68
- const raw = value.trim().replaceAll('\\', '/');
69
- if (path.posix.isAbsolute(raw) || path.win32.isAbsolute(raw)) {
70
- throw new ArchiveJournalError('ARCHIVE_INTENT_INVALID', `${field} must be project-relative`);
71
- }
72
- const relative = path.posix.normalize(raw).replace(/^\.\//, '');
73
- if (relative === '.' || relative === '..' || relative.startsWith('../')) {
74
- throw new ArchiveJournalError('ARCHIVE_INTENT_INVALID', `${field} escapes project root`);
75
- }
76
- return relative;
77
- }
78
-
79
- function inspectProject(rootDir) {
80
- const root = path.resolve(rootDir);
81
- let stat;
82
- try { stat = fs.lstatSync(root); } catch (cause) {
83
- unsafe(`project root is unavailable: ${root}`, { cause: cause.message });
84
- }
85
- if (stat.isSymbolicLink() || !stat.isDirectory()) {
86
- unsafe(`project root must be a real directory: ${root}`);
87
- }
88
- let physicalRoot;
89
- try { physicalRoot = fs.realpathSync.native(root); } catch (cause) {
90
- unsafe(`project root cannot be resolved: ${root}`, { cause: cause.message });
91
- }
92
- return { root, physicalRoot, rootIdentity: identity(stat) };
93
- }
94
-
95
- function absoluteInside(project, relative) {
96
- const normalized = relative === '.' ? '.' : normalizeRelative(relative, 'path');
97
- if (normalized === '.') return project.physicalRoot;
98
- return path.join(project.physicalRoot, ...normalized.split('/'));
99
- }
100
-
101
- function inspectDirectory(project, relative, { missing = false } = {}) {
102
- const normalized = relative === '.' ? '.' : normalizeRelative(relative, 'directory');
103
- const components = normalized === '.' ? [] : normalized.split('/');
104
- let current = project.physicalRoot;
105
- let currentIdentity = project.rootIdentity;
106
- for (let index = 0; index < components.length; index += 1) {
107
- current = path.join(current, components[index]);
108
- let stat;
109
- try { stat = fs.lstatSync(current); } catch (cause) {
110
- if (missing && cause?.code === 'ENOENT') return null;
111
- unsafe(`archive directory is unavailable: ${normalized}`, {
112
- component: components.slice(0, index + 1).join('/'),
113
- cause: cause.message,
114
- });
115
- }
116
- if (stat.isSymbolicLink() || !stat.isDirectory()) {
117
- unsafe(`archive directory must not contain links or non-directories: ${normalized}`, {
118
- component: components.slice(0, index + 1).join('/'),
119
- });
120
- }
121
- currentIdentity = identity(stat);
122
- }
123
- return { relative: normalized, path: current, identity: currentIdentity };
124
- }
125
-
126
- function openDirectoryPin(project, relative, { missing = false } = {}) {
127
- const inspected = inspectDirectory(project, relative, { missing });
128
- if (!inspected) return null;
129
- let fd;
130
- try {
131
- fd = fs.openSync(
132
- inspected.path,
133
- fs.constants.O_RDONLY
134
- | (fs.constants.O_DIRECTORY || 0)
135
- | (fs.constants.O_NOFOLLOW || 0),
136
- );
137
- const opened = fs.fstatSync(fd);
138
- if (!opened.isDirectory() || !sameIdentity(identity(opened), inspected.identity)) {
139
- unsafe(`archive directory changed while it was pinned: ${inspected.relative}`);
140
- }
141
- } catch (error) {
142
- if (fd !== undefined) fs.closeSync(fd);
143
- if (error instanceof ArchiveJournalError) throw error;
144
- unsafe(`archive directory cannot be pinned: ${inspected.relative}`, {
145
- cause: error.message,
146
- });
147
- }
148
- return {
149
- ...inspected,
150
- fd,
151
- verify() {
152
- let named;
153
- let opened;
154
- try {
155
- named = fs.lstatSync(inspected.path);
156
- opened = fs.fstatSync(fd);
157
- } catch (cause) {
158
- unsafe(`archive directory pin is no longer valid: ${inspected.relative}`, {
159
- cause: cause.message,
160
- });
161
- }
162
- if (named.isSymbolicLink() || !named.isDirectory() || !opened.isDirectory()
163
- || !sameIdentity(identity(named), inspected.identity)
164
- || !sameIdentity(identity(opened), inspected.identity)) {
165
- unsafe(`archive directory changed identity: ${inspected.relative}`);
166
- }
167
- },
168
- verifyFd() {
169
- let opened;
170
- try { opened = fs.fstatSync(fd); } catch (cause) {
171
- unsafe(`archive directory descriptor is unavailable: ${inspected.relative}`, {
172
- cause: cause.message,
173
- });
174
- }
175
- if (!opened.isDirectory() || !sameIdentity(identity(opened), inspected.identity)) {
176
- unsafe(`archive directory descriptor changed identity: ${inspected.relative}`);
177
- }
178
- },
179
- close() {
180
- if (fd !== undefined) {
181
- fs.closeSync(fd);
182
- fd = undefined;
183
- }
184
- },
185
- };
186
- }
187
-
188
- function inspectChild(parent, name, { allowMissing = false } = {}) {
189
- parent.verify();
190
- const child = path.join(parent.path, name);
191
- let stat;
192
- try { stat = fs.lstatSync(child); } catch (cause) {
193
- parent.verify();
194
- if (allowMissing && cause?.code === 'ENOENT') return null;
195
- unsafe(`archive entry is unavailable: ${child}`, { cause: cause.message });
196
- }
197
- parent.verify();
198
- if (stat.isSymbolicLink()) unsafe(`archive entry is a symbolic link: ${child}`);
199
- return { path: child, stat, identity: identity(stat) };
200
- }
201
-
202
- function pinChildDirectory(parent, name, { allowMissing = false } = {}) {
203
- const entry = inspectChild(parent, name, { allowMissing });
204
- if (!entry) return null;
205
- if (!entry.stat.isDirectory()) unsafe(`archive entry is not a directory: ${entry.path}`);
206
- const pin = openDirectoryPin(
207
- { physicalRoot: parent.path, rootIdentity: parent.identity },
208
- name,
209
- );
210
- parent.verify();
211
- return pin;
212
- }
213
-
214
- function runPinnedMutation(request, pins) {
215
- const expectedPinFields = MUTATION_PIN_FIELDS[request.operation];
216
- if (!expectedPinFields
217
- || Object.keys(pins).length !== expectedPinFields.length
218
- || expectedPinFields.some((field) => !Object.hasOwn(pins, field))) {
219
- throw new ArchiveJournalError(
220
- 'ARCHIVE_MUTATION_INVALID',
221
- `archive mutation has an invalid operation or descriptor contract: ${request.operation}`,
222
- );
223
- }
224
- const stdio = ['pipe', 'pipe', 'pipe'];
225
- const payload = { ...request };
226
- for (const field of expectedPinFields) {
227
- const pin = pins[field];
228
- if (!pin || !Number.isInteger(pin.fd)) {
229
- unsafe(`archive mutation is missing a pinned directory: ${field}`);
230
- }
231
- payload[field] = stdio.length;
232
- stdio.push(pin.fd);
233
- }
234
- const serialized = JSON.stringify(payload);
235
- if (Buffer.byteLength(serialized) > MAX_MUTATION_REQUEST_BYTES) {
236
- throw new ArchiveJournalError(
237
- 'ARCHIVE_MUTATION_INVALID',
238
- 'archive mutation request exceeds the bounded worker protocol',
239
- );
240
- }
241
- const result = require('node:child_process').spawnSync(
242
- 'python3',
243
- [MUTATION_WORKER],
244
- {
245
- input: serialized,
246
- encoding: 'utf8',
247
- maxBuffer: 1024 * 1024,
248
- stdio,
249
- },
250
- );
251
- if (result.error || result.status !== 0) {
252
- let workerPayload = null;
253
- const workerStderr = String(result.stderr || '').trim();
254
- try {
255
- workerPayload = JSON.parse(workerStderr);
256
- } catch { /* preserve the worker's exact stderr */ }
257
- const workerError = workerPayload?.message || workerStderr;
258
- if (result.error?.code === 'ENOENT'
259
- || result.error?.code === 'EACCES'
260
- || result.error?.code === 'ENOEXEC'
261
- || workerPayload?.code === 'ARCHIVE_RUNTIME_UNAVAILABLE') {
262
- throw new ArchiveJournalError(
263
- 'ARCHIVE_RUNTIME_UNAVAILABLE',
264
- 'inode-pinned archive mutation requires Python 3 with POSIX dir_fd support',
265
- {
266
- operation: request.operation,
267
- cause: result.error?.message || workerError || 'archive runtime unavailable',
268
- },
269
- );
270
- }
271
- unsafe('archive pinned mutation failed', {
272
- operation: request.operation,
273
- cause: result.error?.message || workerError || `worker exited ${result.status}`,
274
- signal: result.signal || null,
275
- });
276
- }
277
- }
278
-
279
- function ensureArchiveRoot(project) {
280
- const changes = openDirectoryPin(project, '.ultra/changes');
281
- try {
282
- const existing = inspectChild(changes, 'archive', { allowMissing: true });
283
- if (existing) {
284
- if (!existing.stat.isDirectory()) unsafe(`archive root is not a directory: ${existing.path}`);
285
- return;
286
- }
287
- changes.verify();
288
- runPinnedMutation({
289
- operation: 'mkdir_dir',
290
- directory_identity: changes.identity,
291
- name: 'archive',
292
- }, { directory_fd: changes });
293
- changes.verify();
294
- const created = inspectChild(changes, 'archive');
295
- if (!created.stat.isDirectory()) unsafe('created archive root is not a directory');
296
- } finally {
297
- changes.close();
298
- }
299
- }
300
-
301
- function openCoreLayout(rootDir, { ensureArchive = false } = {}) {
302
- const project = inspectProject(rootDir);
303
- if (ensureArchive) ensureArchiveRoot(project);
304
- const pins = {};
305
- try {
306
- for (const [name, relative] of CORE_DIRECTORIES) {
307
- pins[name] = openDirectoryPin(project, relative);
308
- }
309
- } catch (error) {
310
- for (const pin of Object.values(pins)) pin?.close();
311
- throw error;
312
- }
313
- return {
314
- project,
315
- pins,
316
- identities: Object.fromEntries(
317
- Object.entries(pins).map(([name, pin]) => [name, pin.identity]),
318
- ),
319
- verify() {
320
- for (const pin of Object.values(pins)) pin.verify();
321
- },
322
- close() {
323
- for (const pin of Object.values(pins).reverse()) pin.close();
324
- },
325
- };
326
- }
327
-
328
- function verifyStoredLayout(intent, layout) {
329
- for (const [name] of CORE_DIRECTORIES) {
330
- if (!sameIdentity(intent.path_identities?.[name], layout.identities[name])) {
331
- unsafe(`archive ${name} directory no longer matches the prepared intent`);
332
- }
333
- }
334
- }
335
-
336
- function assertIntent(intent, rootDir) {
337
- if (!intent || typeof intent !== 'object' || Array.isArray(intent)
338
- || intent.schema_version !== INTENT_VERSION
339
- || typeof intent.change_id !== 'string' || !intent.change_id
340
- || !Array.isArray(intent.baseline_updates)
341
- || typeof intent.reconciliation_path !== 'string' || !intent.reconciliation_path
342
- || !/^[0-9a-f]{64}$/.test(String(intent.reconciliation_digest || ''))
343
- || !intent.reconciliation_manifest || typeof intent.reconciliation_manifest !== 'object'
344
- || typeof intent.summary !== 'string' || intent.summary.trim().length < 3
345
- || !intent.path_identities || typeof intent.path_identities !== 'object'
346
- || !intent.source_identity || typeof intent.source_identity !== 'object') {
347
- throw new ArchiveJournalError('ARCHIVE_INTENT_INVALID', 'archive intent shape is invalid');
348
- }
349
- const sourceRelative = normalizeRelative(intent.source, 'source');
350
- const destinationRelative = normalizeRelative(intent.destination, 'destination');
351
- if (path.posix.dirname(sourceRelative) !== ACTIVE_ROOT
352
- || path.posix.basename(sourceRelative) !== intent.change_id) {
353
- throw new ArchiveJournalError(
354
- 'ARCHIVE_INTENT_INVALID', 'archive intent source is not the active change root',
355
- );
356
- }
357
- if (path.posix.dirname(destinationRelative) !== ARCHIVE_ROOT
358
- || !path.posix.basename(destinationRelative).endsWith(`-${intent.change_id}`)) {
359
- throw new ArchiveJournalError(
360
- 'ARCHIVE_INTENT_INVALID', 'archive intent destination is not the change archive root',
361
- );
362
- }
363
- for (const update of intent.baseline_updates) normalizeRelative(update, 'baseline update');
364
- normalizeRelative(intent.reconciliation_path, 'reconciliation path');
365
- if (intent.previous_summary !== null && typeof intent.previous_summary !== 'string') {
366
- throw new ArchiveJournalError('ARCHIVE_INTENT_INVALID', 'previous_summary must be string or null');
367
- }
368
- const project = inspectProject(rootDir);
369
- return {
370
- intent,
371
- sourceRelative,
372
- destinationRelative,
373
- source: absoluteInside(project, sourceRelative),
374
- destination: absoluteInside(project, destinationRelative),
375
- };
376
- }
377
-
378
- function readStableText(rootDir, relative, code) {
379
- try {
380
- return readStableProjectFile(rootDir, relative, { encoding: 'utf8' });
381
- } catch (error) {
382
- throw new ArchiveJournalError(code, `cannot read ${relative} safely: ${error.message}`, {
383
- cause: error.code || error.message,
384
- });
385
- }
386
- }
387
-
388
- function rebindJournalRelative(intent, location = 'destination') {
389
- const root = location === 'source' ? intent.source : intent.destination;
390
- return `${root}/${REBIND_FILE}`;
391
- }
392
-
393
- function normalizeRebindEntry(entry, index) {
394
- if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
395
- throw new ArchiveJournalError(
396
- 'ARCHIVE_REBIND_INVALID', `archive rebind entry ${index} is invalid`,
397
- );
398
- }
399
- const relativePath = normalizeRelative(entry.relative_path, `rebind entry ${index} path`);
400
- if (relativePath === INTENT_FILE || relativePath === REBIND_FILE
401
- || relativePath.startsWith(`${INTENT_FILE}/`)
402
- || relativePath.startsWith(`${REBIND_FILE}/`)
403
- || !/^[0-9a-f]{64}$/.test(String(entry.before_digest || ''))
404
- || !/^[0-9a-f]{64}$/.test(String(entry.after_digest || ''))
405
- || typeof entry.before_base64 !== 'string'
406
- || typeof entry.after_base64 !== 'string') {
407
- throw new ArchiveJournalError(
408
- 'ARCHIVE_REBIND_INVALID', `archive rebind entry ${index} is invalid`,
409
- );
410
- }
411
- const before = Buffer.from(entry.before_base64, 'base64');
412
- const after = Buffer.from(entry.after_base64, 'base64');
413
- if (before.toString('base64') !== entry.before_base64
414
- || after.toString('base64') !== entry.after_base64
415
- || crypto.createHash('sha256').update(before).digest('hex') !== entry.before_digest
416
- || crypto.createHash('sha256').update(after).digest('hex') !== entry.after_digest) {
417
- throw new ArchiveJournalError(
418
- 'ARCHIVE_REBIND_INVALID', `archive rebind entry ${index} payload is invalid`,
419
- );
420
- }
421
- return {
422
- relative_path: relativePath,
423
- before_digest: entry.before_digest,
424
- after_digest: entry.after_digest,
425
- before_base64: entry.before_base64,
426
- after_base64: entry.after_base64,
427
- };
428
- }
429
-
430
- function normalizeRebindJournal(value, intent) {
431
- if (!value || typeof value !== 'object' || Array.isArray(value)
432
- || value.schema_version !== REBIND_VERSION
433
- || value.change_id !== intent.change_id
434
- || value.source !== intent.source
435
- || value.destination !== intent.destination
436
- || !Array.isArray(value.entries)) {
437
- throw new ArchiveJournalError(
438
- 'ARCHIVE_REBIND_INVALID', 'archive rebind journal shape is invalid',
439
- );
440
- }
441
- const entries = value.entries.map(normalizeRebindEntry);
442
- if (new Set(entries.map((entry) => entry.relative_path)).size !== entries.length) {
443
- throw new ArchiveJournalError(
444
- 'ARCHIVE_REBIND_INVALID', 'archive rebind journal contains duplicate paths',
445
- );
446
- }
447
- return { ...value, entries };
448
- }
449
-
450
- function readArchiveRebind(rootDir, intent, { location = 'destination' } = {}) {
451
- const relative = rebindJournalRelative(intent, location);
452
- const file = path.join(path.resolve(rootDir), ...relative.split('/'));
453
- if (!fs.existsSync(file)) return null;
454
- const stable = readStableText(rootDir, relative, 'ARCHIVE_REBIND_INVALID');
455
- let value;
456
- try { value = JSON.parse(stable.text); } catch (error) {
457
- throw new ArchiveJournalError(
458
- 'ARCHIVE_REBIND_INVALID', `cannot parse ${relative}: ${error.message}`,
459
- );
460
- }
461
- return normalizeRebindJournal(value, intent);
462
- }
463
-
464
- function writeRebindTarget(rootDir, rootRelative, entry, bytes) {
465
- const parentRelative = path.posix.dirname(`${rootRelative}/${entry.relative_path}`);
466
- const name = path.posix.basename(entry.relative_path);
467
- const project = inspectProject(rootDir);
468
- const parent = openDirectoryPin(project, parentRelative);
469
- try {
470
- const current = inspectChild(parent, name);
471
- if (!current.stat.isFile()) {
472
- unsafe(`archive rebind target is not a regular file: ${entry.relative_path}`);
473
- }
474
- writeAtomicRebindEntry(parent, name, bytes);
475
- } finally {
476
- parent.close();
477
- }
478
- }
479
-
480
- function verifyRebindTarget(rootDir, rootRelative, entry, expectedDigest) {
481
- const stable = readStableText(
482
- rootDir,
483
- `${rootRelative}/${entry.relative_path}`,
484
- 'ARCHIVE_REBIND_INVALID',
485
- );
486
- if (stable.digest !== expectedDigest) {
487
- throw new ArchiveJournalError(
488
- 'ARCHIVE_REBIND_CONFLICT',
489
- `archive rebind target has unexpected bytes: ${entry.relative_path}`,
490
- { expected: expectedDigest, actual: stable.digest },
491
- );
492
- }
493
- }
494
-
495
- function prepareArchiveRebind(rootDir, intent, entries = null) {
496
- verifyArchiveIntent(rootDir, intent, { location: 'destination' });
497
- let journal = readArchiveRebind(rootDir, intent);
498
- if (!journal) {
499
- if (!Array.isArray(entries)) {
500
- throw new ArchiveJournalError(
501
- 'ARCHIVE_REBIND_INVALID', 'archive rebind entries are required for a new journal',
502
- );
503
- }
504
- const normalized = entries.map(normalizeRebindEntry);
505
- journal = normalizeRebindJournal({
506
- schema_version: REBIND_VERSION,
507
- change_id: intent.change_id,
508
- source: intent.source,
509
- destination: intent.destination,
510
- entries: normalized,
511
- created_at: new Date().toISOString(),
512
- }, intent);
513
- for (const entry of journal.entries) {
514
- verifyRebindTarget(rootDir, intent.destination, entry, entry.before_digest);
515
- }
516
- const project = inspectProject(rootDir);
517
- const destination = openDirectoryPin(project, intent.destination);
518
- try {
519
- writeAtomicEntry(
520
- destination,
521
- REBIND_FILE,
522
- `${JSON.stringify(journal, null, 2)}\n`,
523
- );
524
- } finally {
525
- destination.close();
526
- }
527
- } else if (entries) {
528
- const requested = entries.map(normalizeRebindEntry);
529
- const comparable = (values) => values.map((entry) => ({
530
- relative_path: entry.relative_path,
531
- before_digest: entry.before_digest,
532
- after_digest: entry.after_digest,
533
- }));
534
- if (JSON.stringify(comparable(journal.entries))
535
- !== JSON.stringify(comparable(requested))) {
536
- throw new ArchiveJournalError(
537
- 'ARCHIVE_REBIND_CONFLICT', 'existing archive rebind journal differs from retry',
538
- );
539
- }
540
- }
541
- for (const entry of journal.entries) {
542
- const relative = `${intent.destination}/${entry.relative_path}`;
543
- const current = readStableText(rootDir, relative, 'ARCHIVE_REBIND_INVALID');
544
- if (current.digest === entry.after_digest) continue;
545
- if (current.digest !== entry.before_digest) {
546
- throw new ArchiveJournalError(
547
- 'ARCHIVE_REBIND_CONFLICT',
548
- `archive rebind target changed outside the transaction: ${entry.relative_path}`,
549
- );
550
- }
551
- writeRebindTarget(
552
- rootDir,
553
- intent.destination,
554
- entry,
555
- Buffer.from(entry.after_base64, 'base64'),
556
- );
557
- verifyRebindTarget(rootDir, intent.destination, entry, entry.after_digest);
558
- }
559
- return journal;
560
- }
561
-
562
- function rollbackArchiveRebind(rootDir, intent, { location = 'destination' } = {}) {
563
- const journal = readArchiveRebind(rootDir, intent, { location });
564
- if (!journal) return;
565
- const rootRelative = location === 'source' ? intent.source : intent.destination;
566
- for (const entry of [...journal.entries].reverse()) {
567
- const current = readStableText(
568
- rootDir,
569
- `${rootRelative}/${entry.relative_path}`,
570
- 'ARCHIVE_REBIND_INVALID',
571
- );
572
- if (current.digest !== entry.before_digest) {
573
- if (current.digest !== entry.after_digest) {
574
- throw new ArchiveJournalError(
575
- 'ARCHIVE_REBIND_CONFLICT',
576
- `archive rebind rollback found unknown bytes: ${entry.relative_path}`,
577
- );
578
- }
579
- writeRebindTarget(
580
- rootDir,
581
- rootRelative,
582
- entry,
583
- Buffer.from(entry.before_base64, 'base64'),
584
- );
585
- }
586
- verifyRebindTarget(rootDir, rootRelative, entry, entry.before_digest);
587
- }
588
- const project = inspectProject(rootDir);
589
- const parent = openDirectoryPin(project, rootRelative);
590
- try { removePinnedRegularFile(parent, REBIND_FILE); } finally { parent.close(); }
591
- }
592
-
593
- function readIntentAt(rootDir, relative, expectedLocation = null) {
594
- const stable = readStableText(rootDir, relative, 'ARCHIVE_INTENT_CORRUPT');
595
- let intent;
596
- try { intent = JSON.parse(stable.text); } catch (error) {
597
- throw new ArchiveJournalError(
598
- 'ARCHIVE_INTENT_CORRUPT', `cannot parse ${relative}: ${error.message}`,
599
- );
600
- }
601
- const asserted = assertIntent(intent, rootDir);
602
- const layout = openCoreLayout(rootDir);
603
- let child;
604
- try {
605
- verifyStoredLayout(intent, layout);
606
- const activeName = path.posix.basename(asserted.sourceRelative);
607
- const archiveName = path.posix.basename(asserted.destinationRelative);
608
- const sourceEntry = inspectChild(layout.pins.active, activeName, { allowMissing: true });
609
- const destinationEntry = inspectChild(
610
- layout.pins.archive, archiveName, { allowMissing: true },
611
- );
612
- const sourceExists = Boolean(sourceEntry);
613
- const destinationExists = Boolean(destinationEntry);
614
- if (sourceEntry && !sourceEntry.stat.isDirectory()) {
615
- unsafe(`active archive source is not a directory: ${asserted.sourceRelative}`);
616
- }
617
- if (destinationEntry && !destinationEntry.stat.isDirectory()) {
618
- unsafe(`archive destination is not a directory: ${asserted.destinationRelative}`);
619
- }
620
- if ((sourceEntry && !sameIdentity(sourceEntry.identity, intent.source_identity))
621
- || (destinationEntry && !sameIdentity(destinationEntry.identity, intent.source_identity))) {
622
- unsafe(`archive change directory does not match the prepared inode: ${intent.change_id}`);
623
- }
624
- const location = sourceExists && !destinationExists
625
- ? 'source'
626
- : destinationExists && !sourceExists
627
- ? 'destination'
628
- : 'conflict';
629
- if (expectedLocation && location !== expectedLocation) {
630
- unsafe(`archive intent is not at the expected ${expectedLocation} location`);
631
- }
632
- child = {
633
- ...asserted,
634
- source_exists: sourceExists,
635
- destination_exists: destinationExists,
636
- location,
637
- };
638
- } finally {
639
- layout.close();
640
- }
641
- return child;
642
- }
643
-
644
- function writeAtomicEntry(parent, name, contents, { replace = false } = {}) {
645
- const current = inspectChild(parent, name, { allowMissing: true });
646
- if (current && (!replace || !current.stat.isFile())) {
647
- unsafe(`archive file cannot be replaced safely: ${current.path}`);
648
- }
649
- parent.verify();
650
- runPinnedMutation({
651
- operation: 'write_atomic',
652
- directory_identity: parent.identity,
653
- name,
654
- temp_name: `.${name}.${process.pid}.${crypto.randomUUID()}.tmp`,
655
- replace,
656
- data_base64: Buffer.from(contents).toString('base64'),
657
- }, { directory_fd: parent });
658
- parent.verify();
659
- const published = inspectChild(parent, name);
660
- if (!published.stat.isFile()) {
661
- unsafe(`archive file is not regular after publication: ${published.path}`);
662
- }
663
- }
664
-
665
- function writeAtomicRebindEntry(parent, name, contents) {
666
- const current = inspectChild(parent, name);
667
- if (!current.stat.isFile()) {
668
- unsafe(`archive rebind target is not a regular file: ${current.path}`);
669
- }
670
- parent.verify();
671
- runPinnedMutation({
672
- operation: 'write_rebind_atomic',
673
- directory_identity: parent.identity,
674
- name,
675
- temp_name: `.${name}.${process.pid}.${crypto.randomUUID()}.tmp`,
676
- replace: true,
677
- data_base64: Buffer.from(contents).toString('base64'),
678
- }, { directory_fd: parent });
679
- parent.verify();
680
- const published = inspectChild(parent, name);
681
- if (!published.stat.isFile()) {
682
- unsafe(`archive rebind target is not regular after publication: ${published.path}`);
683
- }
684
- }
685
-
686
- function renderSummary(change, summary, baselineUpdates, noBaselineChangeReason) {
687
- return [
688
- `# Archived change: ${change.title}`, '', summary.trim(), '',
689
- '## Baseline reconciliation', '',
690
- ...(baselineUpdates.length > 0
691
- ? baselineUpdates.map((file) => `- ${file}`)
692
- : [noBaselineChangeReason]), '',
693
- ].join('\n');
694
- }
695
-
696
- function sameOperation(intent, {
697
- change, summary, baselineUpdates, noBaselineChangeReason, reconciliationPath,
698
- reconciliationDigest,
699
- }) {
700
- return intent.change_id === change.id
701
- && intent.source === normalizeRelative(change.artifact_root, 'source')
702
- && intent.summary === summary.trim()
703
- && JSON.stringify(intent.baseline_updates) === JSON.stringify(baselineUpdates)
704
- && (intent.no_baseline_change_reason || null) === (noBaselineChangeReason || null)
705
- && intent.reconciliation_path === normalizeRelative(reconciliationPath, 'reconciliation path')
706
- && intent.reconciliation_digest === reconciliationDigest;
707
- }
708
-
709
- function prepareArchiveMove({
710
- rootDir, change, summary, baselineUpdates, noBaselineChangeReason,
711
- reconciliationPath, reconciliationDigest, reconciliationManifest, now = new Date(),
712
- }) {
713
- const sourceRelative = normalizeRelative(change.artifact_root, 'source');
714
- const date = now.toISOString().slice(0, 10);
715
- const destinationRelative = `${ARCHIVE_ROOT}/${date}-${change.id}`;
716
- const sourceName = path.posix.basename(sourceRelative);
717
- const destinationName = path.posix.basename(destinationRelative);
718
- const layout = openCoreLayout(rootDir, { ensureArchive: true });
719
- let sourcePin;
720
- try {
721
- if (path.posix.dirname(sourceRelative) !== ACTIVE_ROOT || sourceName !== change.id) {
722
- throw new ArchiveJournalError(
723
- 'ARCHIVE_INTENT_INVALID', 'change artifact root is not the active change root',
724
- );
725
- }
726
- const sourceEntry = inspectChild(layout.pins.active, sourceName, { allowMissing: true });
727
- const destinationEntry = inspectChild(
728
- layout.pins.archive, destinationName, { allowMissing: true },
729
- );
730
- if (!sourceEntry) {
731
- if (!destinationEntry) {
732
- throw new ArchiveJournalError(
733
- 'ARCHIVE_SOURCE_MISSING', `active change root missing: ${sourceRelative}`,
734
- );
735
- }
736
- const resumed = readIntentAt(
737
- rootDir, `${destinationRelative}/${INTENT_FILE}`, 'destination',
738
- );
739
- if (!sameOperation(resumed.intent, {
740
- change, summary, baselineUpdates, noBaselineChangeReason,
741
- reconciliationPath, reconciliationDigest,
742
- })) {
743
- throw new ArchiveJournalError(
744
- 'ARCHIVE_INTENT_CONFLICT', 'existing archive intent differs from retry input',
745
- );
746
- }
747
- return {
748
- ...resumed,
749
- source: path.resolve(rootDir, sourceRelative),
750
- destination: path.resolve(rootDir, destinationRelative),
751
- resumed: true,
752
- };
753
- }
754
- if (!sourceEntry.stat.isDirectory()) unsafe(`active change root is not a directory: ${sourceRelative}`);
755
- if (destinationEntry) {
756
- if (!destinationEntry.stat.isDirectory()) {
757
- unsafe(`archive destination is not a directory: ${destinationRelative}`);
758
- }
759
- throw new ArchiveJournalError('ARCHIVE_EXISTS', `archive already exists: ${destinationRelative}`);
760
- }
761
- sourcePin = pinChildDirectory(layout.pins.active, sourceName);
762
- const sourceIdentity = sourcePin.identity;
763
- const sourceIntentRelative = `${sourceRelative}/${INTENT_FILE}`;
764
- const existingIntent = inspectChild(sourcePin, INTENT_FILE, { allowMissing: true });
765
- let prepared;
766
- if (existingIntent) {
767
- if (!existingIntent.stat.isFile()) unsafe(`archive intent is not a regular file: ${sourceIntentRelative}`);
768
- prepared = readIntentAt(rootDir, sourceIntentRelative, 'source');
769
- if (!sameOperation(prepared.intent, {
770
- change, summary, baselineUpdates, noBaselineChangeReason,
771
- reconciliationPath, reconciliationDigest,
772
- })) {
773
- throw new ArchiveJournalError(
774
- 'ARCHIVE_INTENT_CONFLICT', 'prepared archive intent differs from retry input',
775
- );
776
- }
777
- } else {
778
- const summaryEntry = inspectChild(sourcePin, 'archive-summary.md', { allowMissing: true });
779
- if (summaryEntry && !summaryEntry.stat.isFile()) {
780
- unsafe('archive summary is not a regular file');
781
- }
782
- const previousSummary = summaryEntry
783
- ? readStableText(
784
- rootDir, `${sourceRelative}/archive-summary.md`, 'ARCHIVE_PATH_UNSAFE',
785
- ).text
786
- : null;
787
- const intent = {
788
- schema_version: INTENT_VERSION,
789
- change_id: change.id,
790
- source: sourceRelative,
791
- destination: destinationRelative,
792
- summary: summary.trim(),
793
- baseline_updates: baselineUpdates,
794
- no_baseline_change_reason: noBaselineChangeReason || null,
795
- reconciliation_path: normalizeRelative(reconciliationPath, 'reconciliation path'),
796
- reconciliation_digest: reconciliationDigest,
797
- reconciliation_manifest: reconciliationManifest,
798
- previous_summary: previousSummary,
799
- path_identities: layout.identities,
800
- source_identity: sourceIdentity,
801
- created_at: now.toISOString(),
802
- };
803
- writeAtomicEntry(sourcePin, INTENT_FILE, `${JSON.stringify(intent, null, 2)}\n`);
804
- layout.verify();
805
- sourcePin.verify();
806
- prepared = { ...assertIntent(intent, rootDir), location: 'source', intent };
807
- }
808
- writeAtomicEntry(
809
- sourcePin,
810
- 'archive-summary.md',
811
- renderSummary(change, summary, baselineUpdates, noBaselineChangeReason),
812
- { replace: true },
813
- );
814
- layout.verify();
815
- sourcePin.verify();
816
- if (inspectChild(layout.pins.archive, destinationName, { allowMissing: true })) {
817
- throw new ArchiveJournalError(
818
- 'ARCHIVE_EXISTS', `archive appeared before rename: ${destinationRelative}`,
819
- );
820
- }
821
- runPinnedMutation({
822
- operation: 'rename_dir',
823
- source_parent_identity: layout.pins.active.identity,
824
- destination_parent_identity: layout.pins.archive.identity,
825
- guard_parent_identity: layout.pins.changes.identity,
826
- source_parent_name: 'active',
827
- destination_parent_name: 'archive',
828
- source_name: sourceName,
829
- destination_name: destinationName,
830
- source_identity: sourceIdentity,
831
- }, {
832
- source_parent_fd: layout.pins.active,
833
- destination_parent_fd: layout.pins.archive,
834
- guard_parent_fd: layout.pins.changes,
835
- });
836
- layout.verify();
837
- sourcePin.verifyFd();
838
- if (inspectChild(layout.pins.active, sourceName, { allowMissing: true })) {
839
- unsafe(`active source still exists after archive rename: ${sourceRelative}`);
840
- }
841
- const moved = inspectChild(layout.pins.archive, destinationName);
842
- if (!moved.stat.isDirectory() || !sameIdentity(moved.identity, sourceIdentity)) {
843
- unsafe(`archive destination does not match the prepared source: ${destinationRelative}`);
844
- }
845
- return {
846
- ...prepared,
847
- source: path.resolve(rootDir, sourceRelative),
848
- destination: path.resolve(rootDir, destinationRelative),
849
- sourceRelative,
850
- destinationRelative,
851
- location: 'destination',
852
- resumed: false,
853
- };
854
- } finally {
855
- sourcePin?.close();
856
- layout.close();
857
- }
858
- }
859
-
860
- function listArchiveIntents(rootDir) {
861
- let project;
862
- try { project = inspectProject(rootDir); } catch (error) {
863
- return [{ file: path.resolve(rootDir), error }];
864
- }
865
- const found = [];
866
- for (const relative of [ACTIVE_ROOT, ARCHIVE_ROOT]) {
867
- let parent;
868
- try { parent = openDirectoryPin(project, relative, { missing: true }); } catch (error) {
869
- found.push({ file: absoluteInside(project, relative), error });
870
- continue;
871
- }
872
- if (!parent) continue;
873
- try {
874
- parent.verify();
875
- const entries = fs.readdirSync(parent.path, { withFileTypes: true });
876
- parent.verify();
877
- for (const entry of entries) {
878
- if (['.gitkeep', '.DS_Store'].includes(entry.name)) continue;
879
- let child;
880
- try {
881
- child = inspectChild(parent, entry.name);
882
- if (!child.stat.isDirectory()) {
883
- found.push({
884
- file: child.path,
885
- error: new ArchiveJournalError(
886
- 'ARCHIVE_PATH_UNSAFE', `archive recovery residue is not a directory: ${child.path}`,
887
- ),
888
- });
889
- continue;
890
- }
891
- } catch (error) {
892
- found.push({ file: path.join(parent.path, entry.name), error });
893
- continue;
894
- }
895
- const intentRelative = `${relative}/${entry.name}/${INTENT_FILE}`;
896
- let intentEntry;
897
- const childPin = pinChildDirectory(parent, entry.name);
898
- try {
899
- intentEntry = inspectChild(childPin, INTENT_FILE, { allowMissing: true });
900
- } catch (error) {
901
- found.push({ file: path.join(child.path, INTENT_FILE), error });
902
- childPin.close();
903
- continue;
904
- }
905
- childPin.close();
906
- if (!intentEntry) continue;
907
- if (!intentEntry.stat.isFile()) {
908
- found.push({
909
- file: intentEntry.path,
910
- error: new ArchiveJournalError(
911
- 'ARCHIVE_PATH_UNSAFE', `archive intent residue is not a regular file: ${intentEntry.path}`,
912
- ),
913
- });
914
- continue;
915
- }
916
- try {
917
- const record = readIntentAt(rootDir, intentRelative);
918
- found.push({ ...record, file: intentEntry.path, error: null });
919
- } catch (error) {
920
- found.push({ file: intentEntry.path, error });
921
- }
922
- }
923
- } catch (error) {
924
- found.push({ file: parent.path, error });
925
- } finally {
926
- parent.close();
927
- }
928
- }
929
- return found;
930
- }
931
-
932
- function inspectArchiveIntentState(rootDir, intent) {
933
- const asserted = assertIntent(intent, rootDir);
934
- const layout = openCoreLayout(rootDir);
935
- try {
936
- verifyStoredLayout(intent, layout);
937
- const source = inspectChild(
938
- layout.pins.active, path.posix.basename(asserted.sourceRelative), { allowMissing: true },
939
- );
940
- const destination = inspectChild(
941
- layout.pins.archive,
942
- path.posix.basename(asserted.destinationRelative),
943
- { allowMissing: true },
944
- );
945
- for (const entry of [source, destination].filter(Boolean)) {
946
- if (!entry.stat.isDirectory() || !sameIdentity(entry.identity, intent.source_identity)) {
947
- unsafe(`archive change directory has an unexpected identity: ${entry.path}`);
948
- }
949
- }
950
- return {
951
- ...asserted,
952
- source_exists: Boolean(source),
953
- destination_exists: Boolean(destination),
954
- location: source && !destination ? 'source' : destination && !source ? 'destination' : 'conflict',
955
- };
956
- } finally {
957
- layout.close();
958
- }
959
- }
960
-
961
- function verifyArchiveIntent(rootDir, intent, { location = 'destination' } = {}) {
962
- const state = inspectArchiveIntentState(rootDir, intent);
963
- if (state.location !== location) {
964
- unsafe(`archive intent is not at the expected ${location} location`);
965
- }
966
- const relative = location === 'source' ? state.sourceRelative : state.destinationRelative;
967
- const record = readIntentAt(rootDir, `${relative}/${INTENT_FILE}`, location);
968
- if (JSON.stringify(record.intent) !== JSON.stringify(intent)) {
969
- throw new ArchiveJournalError(
970
- 'ARCHIVE_INTENT_CONFLICT', 'archive intent bytes differ from the supplied operation',
971
- );
972
- }
973
- return state;
974
- }
975
-
976
- function removePinnedRegularFile(parent, name, { allowMissing = false } = {}) {
977
- const entry = inspectChild(parent, name, { allowMissing });
978
- if (!entry) return;
979
- if (!entry.stat.isFile()) unsafe(`archive cleanup target is not a regular file: ${entry.path}`);
980
- parent.verify();
981
- runPinnedMutation({
982
- operation: 'unlink_regular',
983
- directory_identity: parent.identity,
984
- name,
985
- allow_missing: allowMissing,
986
- }, { directory_fd: parent });
987
- parent.verify();
988
- if (inspectChild(parent, name, { allowMissing: true })) {
989
- unsafe(`archive cleanup target still exists: ${entry.path}`);
990
- }
991
- }
992
-
993
- function completeArchiveIntent(rootDir, intent) {
994
- const state = verifyArchiveIntent(rootDir, intent, { location: 'destination' });
995
- const rebind = readArchiveRebind(rootDir, intent);
996
- if (rebind) {
997
- for (const entry of rebind.entries) {
998
- verifyRebindTarget(
999
- rootDir, intent.destination, entry, entry.after_digest,
1000
- );
1001
- }
1002
- }
1003
- const layout = openCoreLayout(rootDir);
1004
- let destination;
1005
- try {
1006
- verifyStoredLayout(intent, layout);
1007
- destination = pinChildDirectory(
1008
- layout.pins.archive, path.posix.basename(state.destinationRelative),
1009
- );
1010
- if (!sameIdentity(destination.identity, intent.source_identity)) {
1011
- unsafe('archive destination changed before journal cleanup');
1012
- }
1013
- removePinnedRegularFile(destination, REBIND_FILE, { allowMissing: true });
1014
- removePinnedRegularFile(destination, INTENT_FILE);
1015
- layout.verify();
1016
- } finally {
1017
- destination?.close();
1018
- layout.close();
1019
- }
1020
- }
1021
-
1022
- function rollbackArchiveIntent(rootDir, intent) {
1023
- const asserted = assertIntent(intent, rootDir);
1024
- let state = inspectArchiveIntentState(rootDir, intent);
1025
- if (state.location === 'destination') {
1026
- rollbackArchiveRebind(rootDir, intent, { location: 'destination' });
1027
- const layout = openCoreLayout(rootDir);
1028
- let destination;
1029
- try {
1030
- verifyStoredLayout(intent, layout);
1031
- destination = pinChildDirectory(
1032
- layout.pins.archive, path.posix.basename(asserted.destinationRelative),
1033
- );
1034
- if (!sameIdentity(destination.identity, intent.source_identity)) {
1035
- unsafe('archive destination changed before rollback');
1036
- }
1037
- if (inspectChild(
1038
- layout.pins.active, path.posix.basename(asserted.sourceRelative), { allowMissing: true },
1039
- )) {
1040
- throw new ArchiveJournalError(
1041
- 'ARCHIVE_ROLLBACK_FAILED', 'archive source already exists during rollback',
1042
- );
1043
- }
1044
- layout.verify();
1045
- destination.verify();
1046
- runPinnedMutation({
1047
- operation: 'rename_dir',
1048
- source_parent_identity: layout.pins.archive.identity,
1049
- destination_parent_identity: layout.pins.active.identity,
1050
- guard_parent_identity: layout.pins.changes.identity,
1051
- source_parent_name: 'archive',
1052
- destination_parent_name: 'active',
1053
- source_name: path.posix.basename(asserted.destinationRelative),
1054
- destination_name: path.posix.basename(asserted.sourceRelative),
1055
- source_identity: intent.source_identity,
1056
- }, {
1057
- source_parent_fd: layout.pins.archive,
1058
- destination_parent_fd: layout.pins.active,
1059
- guard_parent_fd: layout.pins.changes,
1060
- });
1061
- layout.verify();
1062
- destination.verifyFd();
1063
- const restored = inspectChild(
1064
- layout.pins.active, path.posix.basename(asserted.sourceRelative),
1065
- );
1066
- if (!restored.stat.isDirectory()
1067
- || !sameIdentity(restored.identity, intent.source_identity)) {
1068
- unsafe('restored archive source does not match the prepared inode');
1069
- }
1070
- } finally {
1071
- destination?.close();
1072
- layout.close();
1073
- }
1074
- state = inspectArchiveIntentState(rootDir, intent);
1075
- }
1076
- if (state.location !== 'source') {
1077
- throw new ArchiveJournalError(
1078
- 'ARCHIVE_ROLLBACK_FAILED', 'archive roots are not in a recoverable state',
1079
- );
1080
- }
1081
- rollbackArchiveRebind(rootDir, intent, { location: 'source' });
1082
- const layout = openCoreLayout(rootDir);
1083
- let source;
1084
- try {
1085
- verifyStoredLayout(intent, layout);
1086
- source = pinChildDirectory(
1087
- layout.pins.active, path.posix.basename(asserted.sourceRelative),
1088
- );
1089
- if (!sameIdentity(source.identity, intent.source_identity)) {
1090
- unsafe('archive source changed before rollback cleanup');
1091
- }
1092
- if (intent.previous_summary === null) {
1093
- removePinnedRegularFile(source, 'archive-summary.md', { allowMissing: true });
1094
- } else {
1095
- writeAtomicEntry(
1096
- source, 'archive-summary.md', intent.previous_summary, { replace: true },
1097
- );
1098
- }
1099
- removePinnedRegularFile(source, INTENT_FILE);
1100
- layout.verify();
1101
- } finally {
1102
- source?.close();
1103
- layout.close();
1104
- }
1105
- }
1106
-
1107
- module.exports = {
1108
- ArchiveJournalError,
1109
- INTENT_FILE,
1110
- REBIND_FILE,
1111
- prepareArchiveMove,
1112
- prepareArchiveRebind,
1113
- readArchiveRebind,
1114
- listArchiveIntents,
1115
- inspectArchiveIntentState,
1116
- verifyArchiveIntent,
1117
- completeArchiveIntent,
1118
- rollbackArchiveIntent,
1119
- };