ultra-builder-pro-cli 0.25.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (241) hide show
  1. package/.ultra-template/contexts/TEMPLATE.md +87 -0
  2. package/.ultra-template/decisions/.gitkeep +1 -0
  3. package/.ultra-template/evidence/.gitkeep +1 -0
  4. package/.ultra-template/north-star.md +46 -0
  5. package/.ultra-template/research/.gitkeep +1 -0
  6. package/.ultra-template/specs/discovery.md +2 -2
  7. package/.ultra-template/tasks.json +3 -0
  8. package/.ultra-template/test-report.json +27 -0
  9. package/CHANGELOG.md +49 -0
  10. package/README.md +217 -579
  11. package/adapters/_shared/delegate-profiles/kimi-read-only.md +15 -0
  12. package/adapters/_shared/delegate-profiles/kimi-write.md +17 -0
  13. package/adapters/_shared/file-ops.cjs +76 -2
  14. package/adapters/_shared/host-profile.cjs +97 -0
  15. package/adapters/_shared/plugin-core.cjs +204 -0
  16. package/adapters/_shared/runtime-assets.cjs +31 -96
  17. package/adapters/claude.js +92 -291
  18. package/adapters/codex.js +309 -842
  19. package/adapters/grok.js +143 -1469
  20. package/adapters/kimi.js +124 -1053
  21. package/adapters/opencode.js +230 -742
  22. package/bin/delegate-worker.cjs +323 -0
  23. package/bin/delegate.cjs +456 -0
  24. package/bin/install.js +38 -11
  25. package/docs/ARCHITECTURE.md +122 -216
  26. package/docs/ARTIFACT-AUTHORITY.md +90 -268
  27. package/docs/DECISIONS.md +75 -87
  28. package/docs/PHILOSOPHY.md +177 -0
  29. package/docs/PLUGIN-ISOLATION-CONTRACT.md +62 -39
  30. package/docs/RUNTIME-COMPAT-MATRIX.md +99 -156
  31. package/docs/SKILL-AUTHORING.md +94 -73
  32. package/docs/WORKFLOW-LIFECYCLE.md +93 -189
  33. package/hooks/README.md +17 -37
  34. package/hooks/_common.py +113 -0
  35. package/hooks/adapters/codex.py +9 -16
  36. package/hooks/adapters/grok.py +5 -5
  37. package/hooks/adapters/kimi.py +8 -14
  38. package/hooks/block_dangerous_commands.py +95 -0
  39. package/hooks/compact_context.py +65 -0
  40. package/hooks/mid_workflow_recall.py +25 -0
  41. package/hooks/post_edit_guard.py +105 -0
  42. package/hooks/session_context.py +30 -0
  43. package/package.json +17 -52
  44. package/skills/ultra-change/SKILL.md +97 -50
  45. package/skills/ultra-change/references/change-contract.md +40 -1
  46. package/skills/ultra-delegate/SKILL.md +82 -0
  47. package/skills/ultra-delegate/references/delegation-contract.md +51 -0
  48. package/skills/ultra-delegate/scripts/delegate_wait.py +86 -0
  49. package/skills/ultra-deliver/SKILL.md +61 -42
  50. package/skills/ultra-deliver/references/baseline-reconciliation.md +39 -20
  51. package/skills/ultra-dev/SKILL.md +96 -62
  52. package/skills/ultra-dev/references/debugging.md +22 -0
  53. package/skills/ultra-domain-modeling/SKILL.md +79 -0
  54. package/skills/ultra-grilling/SKILL.md +79 -0
  55. package/skills/ultra-init/SKILL.md +94 -37
  56. package/skills/ultra-init/references/brownfield-adoption.md +33 -83
  57. package/skills/ultra-init/scripts/init_project.cjs +84 -0
  58. package/skills/ultra-plan/SKILL.md +88 -68
  59. package/skills/ultra-research/SKILL.md +62 -48
  60. package/skills/ultra-research/references/00-problem-validation.md +5 -5
  61. package/skills/ultra-research/references/01-opportunity-discovery.md +3 -3
  62. package/skills/ultra-research/references/02-market-assessment.md +5 -4
  63. package/skills/ultra-research/references/03-competitive-landscape.md +3 -3
  64. package/skills/ultra-research/references/04-product-strategy.md +3 -3
  65. package/skills/ultra-research/references/05-assumptions-validation.md +5 -5
  66. package/skills/ultra-research/references/10-user-personas.md +3 -2
  67. package/skills/ultra-research/references/11-user-scenarios.md +3 -3
  68. package/skills/ultra-research/references/20-user-stories.md +4 -3
  69. package/skills/ultra-research/references/21-features-scope.md +3 -2
  70. package/skills/ultra-research/references/22-success-metrics.md +3 -3
  71. package/skills/ultra-research/references/30-architecture-context.md +3 -3
  72. package/skills/ultra-research/references/31-solution-strategy.md +3 -3
  73. package/skills/ultra-research/references/32-building-blocks.md +3 -3
  74. package/skills/ultra-research/references/40-deployment.md +3 -3
  75. package/skills/ultra-research/references/41-quality-risks.md +3 -3
  76. package/skills/ultra-research/references/99-synthesis.md +7 -7
  77. package/skills/ultra-review/SKILL.md +59 -32
  78. package/skills/ultra-review/references/code.md +17 -0
  79. package/skills/ultra-review/references/comments.md +12 -0
  80. package/skills/ultra-review/references/design.md +12 -0
  81. package/skills/ultra-review/references/errors.md +13 -0
  82. package/skills/ultra-review/references/spec.md +13 -0
  83. package/skills/ultra-review/references/tests.md +12 -0
  84. package/skills/ultra-review/references/worker-packet.md +40 -0
  85. package/skills/ultra-review/scripts/review_wait.py +26 -15
  86. package/skills/ultra-status/SKILL.md +61 -36
  87. package/skills/ultra-tdd/SKILL.md +81 -0
  88. package/skills/ultra-tdd/references/templates/README.md +20 -0
  89. package/skills/ultra-tdd/references/templates/feature-flag-default-audit.sh +79 -0
  90. package/skills/ultra-tdd/references/templates/persistence-real.ts +78 -0
  91. package/skills/ultra-tdd/references/templates/testcontainer-postgres.py +78 -0
  92. package/skills/ultra-tdd/references/templates/testcontainer-postgres.ts +73 -0
  93. package/skills/ultra-tdd/references/templates/vertical-slice.ts +66 -0
  94. package/skills/ultra-tdd/references/test-execution.md +18 -0
  95. package/skills/ultra-test/SKILL.md +70 -30
  96. package/skills/ultra-test/references/export-syntax.md +18 -0
  97. package/skills/ultra-test/scripts/worktree_digest.cjs +54 -0
  98. package/skills/ultra-think/SKILL.md +49 -27
  99. package/skills/ultra-think/references/autonomy-boundary.md +48 -0
  100. package/skills/ultra-think/references/interaction-boundary.md +18 -33
  101. package/.ultra-template/docs/research/README.md +0 -55
  102. package/.ultra-template/reports/templates/delivery-report.json +0 -17
  103. package/.ultra-template/reports/templates/test-report.json +0 -18
  104. package/.ultra-template/tasks/tasks.json +0 -15
  105. package/.ultra-template/templates/task-context.md +0 -19
  106. package/adapters/_shared/codex-assets.cjs +0 -725
  107. package/adapters/_shared/codex-ultra-tools-entry.cjs +0 -11
  108. package/adapters/_shared/interaction-contract.cjs +0 -129
  109. package/adapters/_shared/path-rewrite.cjs +0 -79
  110. package/adapters/_shared/runtime-native.cjs +0 -132
  111. package/adapters/_shared/settings-merge.cjs +0 -76
  112. package/agents/code-reviewer.md +0 -42
  113. package/agents/debugger.md +0 -40
  114. package/agents/review-code.md +0 -49
  115. package/agents/review-comments.md +0 -38
  116. package/agents/review-coordinator.md +0 -56
  117. package/agents/review-design.md +0 -40
  118. package/agents/review-errors.md +0 -43
  119. package/agents/review-spec.md +0 -39
  120. package/agents/review-tests.md +0 -40
  121. package/agents/tdd-runner.md +0 -38
  122. package/bin/orchestrator.js +0 -383
  123. package/commands/ultra-change.md +0 -10
  124. package/commands/ultra-deliver.md +0 -10
  125. package/commands/ultra-dev.md +0 -10
  126. package/commands/ultra-doctor.md +0 -10
  127. package/commands/ultra-init.md +0 -10
  128. package/commands/ultra-plan.md +0 -10
  129. package/commands/ultra-research.md +0 -10
  130. package/commands/ultra-review.md +0 -10
  131. package/commands/ultra-status.md +0 -10
  132. package/commands/ultra-test.md +0 -10
  133. package/commands/ultra-think.md +0 -10
  134. package/docs/AGENT-CONTEXT.md +0 -191
  135. package/docs/COMMIT-HASH-BACKFILL.md +0 -51
  136. package/docs/LEGACY-CLI-CROSSWALK.md +0 -175
  137. package/docs/ROADMAP.md +0 -101
  138. package/docs/STATE-DB-ACCESS-POLICY.md +0 -161
  139. package/hooks/active_task_context.py +0 -152
  140. package/hooks/context_envelope.py +0 -100
  141. package/hooks/health_check.py +0 -196
  142. package/hooks/pre_stop_check.py +0 -66
  143. package/hooks/runtime_paths.py +0 -396
  144. package/hooks/subagent_tracker.py +0 -88
  145. package/hooks/workflow_checkpoint.py +0 -76
  146. package/hooks/workflow_context.py +0 -48
  147. package/hooks/workflow_resume.py +0 -84
  148. package/mcp-server/hook-context.cjs +0 -38
  149. package/mcp-server/hook-event.cjs +0 -34
  150. package/mcp-server/lib/archive-journal.cjs +0 -1119
  151. package/mcp-server/lib/archive-mutation-worker.py +0 -365
  152. package/mcp-server/lib/artifact-registry.cjs +0 -1232
  153. package/mcp-server/lib/baseline-workflow.cjs +0 -1193
  154. package/mcp-server/lib/canonical-json.cjs +0 -19
  155. package/mcp-server/lib/change-authority.cjs +0 -26
  156. package/mcp-server/lib/change-packet.cjs +0 -790
  157. package/mcp-server/lib/change-workflow.cjs +0 -65
  158. package/mcp-server/lib/context-envelope.cjs +0 -581
  159. package/mcp-server/lib/context-paths.cjs +0 -142
  160. package/mcp-server/lib/decision-records.cjs +0 -423
  161. package/mcp-server/lib/delivery-transaction.cjs +0 -471
  162. package/mcp-server/lib/doctor-backup-worker.cjs +0 -328
  163. package/mcp-server/lib/doctor.cjs +0 -627
  164. package/mcp-server/lib/git-bootstrap.cjs +0 -651
  165. package/mcp-server/lib/init-project.cjs +0 -1021
  166. package/mcp-server/lib/kernel-change-workflow.cjs +0 -2029
  167. package/mcp-server/lib/legacy-change-workflow.cjs +0 -3032
  168. package/mcp-server/lib/managed-file-write.cjs +0 -172
  169. package/mcp-server/lib/plan-checkpoint.cjs +0 -177
  170. package/mcp-server/lib/plan-publication-worker.cjs +0 -830
  171. package/mcp-server/lib/plan-store.cjs +0 -794
  172. package/mcp-server/lib/prd-parser.cjs +0 -83
  173. package/mcp-server/lib/pricing.cjs +0 -44
  174. package/mcp-server/lib/project-context.cjs +0 -178
  175. package/mcp-server/lib/projector.cjs +0 -386
  176. package/mcp-server/lib/provider-refs.cjs +0 -42
  177. package/mcp-server/lib/runtime-paths.cjs +0 -1687
  178. package/mcp-server/lib/runtime-state.cjs +0 -222
  179. package/mcp-server/lib/safe-project-file.cjs +0 -385
  180. package/mcp-server/lib/stage-checkpoints.cjs +0 -463
  181. package/mcp-server/lib/state-authority.cjs +0 -95
  182. package/mcp-server/lib/state-db.cjs +0 -1580
  183. package/mcp-server/lib/state-ops.cjs +0 -1428
  184. package/mcp-server/lib/task-contract.cjs +0 -404
  185. package/mcp-server/lib/task-expander.cjs +0 -105
  186. package/mcp-server/lib/task-ledger.cjs +0 -1964
  187. package/mcp-server/lib/telemetry.cjs +0 -85
  188. package/mcp-server/lib/topo.cjs +0 -131
  189. package/mcp-server/lib/ultra-facade.cjs +0 -2684
  190. package/mcp-server/lib/worker-packet.cjs +0 -358
  191. package/mcp-server/server.cjs +0 -437
  192. package/orchestrator/auto-merge.cjs +0 -128
  193. package/orchestrator/daemon.cjs +0 -264
  194. package/orchestrator/dispatch-rules.cjs +0 -78
  195. package/orchestrator/parallel-orchestrator.cjs +0 -903
  196. package/orchestrator/planner/plan-builder.cjs +0 -125
  197. package/orchestrator/recovery.cjs +0 -188
  198. package/orchestrator/session-close-journal-worker.cjs +0 -5
  199. package/orchestrator/session-close-journal.cjs +0 -1014
  200. package/orchestrator/session-runner.cjs +0 -1153
  201. package/orchestrator/worktree-manager.cjs +0 -282
  202. package/settings.json +0 -7
  203. package/skills/cc-collab/SKILL.md +0 -42
  204. package/skills/code-review-expert/SKILL.md +0 -43
  205. package/skills/code-review-expert/references/correctness-reliability.md +0 -30
  206. package/skills/code-review-expert/references/design-boundaries.md +0 -26
  207. package/skills/code-review-expert/references/removal-plan.md +0 -24
  208. package/skills/codex-collab/SKILL.md +0 -49
  209. package/skills/codex-collab/evals/evals.json +0 -35
  210. package/skills/integration-rules/SKILL.md +0 -32
  211. package/skills/security-rules/SKILL.md +0 -38
  212. package/skills/testing-rules/SKILL.md +0 -32
  213. package/skills/ultra-doctor/SKILL.md +0 -41
  214. package/skills/ultra-plan/references/semantic-preflight.md +0 -20
  215. package/skills/ultra-verify/SKILL.md +0 -46
  216. package/skills/ultra-verify/evals/evals.json +0 -46
  217. package/skills/ultra-verify/references/cross-verify-modes.md +0 -28
  218. package/skills/ultra-verify/references/evidence-status.md +0 -21
  219. package/skills/ultra-verify/scripts/verify_wait.py +0 -94
  220. package/spec/cli-protocol.md +0 -102
  221. package/spec/mcp-tools.yaml +0 -312
  222. package/spec/schemas/baseline-reconciliation.v1.schema.json +0 -73
  223. package/spec/schemas/change-delta.v1.schema.json +0 -153
  224. package/spec/schemas/command-manifest.schema.json +0 -29
  225. package/spec/schemas/context-file.v4.5.schema.json +0 -54
  226. package/spec/schemas/delivery-report.v1.schema.json +0 -66
  227. package/spec/schemas/documentation-reconciliation.v1.schema.json +0 -106
  228. package/spec/schemas/mcp-tools.schema.json +0 -136
  229. package/spec/schemas/skill-manifest.schema.json +0 -24
  230. package/spec/schemas/state-db.sql +0 -641
  231. package/spec/schemas/task-ledger.v2.schema.json +0 -296
  232. package/spec/schemas/tasks.v4.5.schema.json +0 -96
  233. package/spec/schemas/test-report.v1.schema.json +0 -225
  234. package/ultra-tools/cli.cjs +0 -129
  235. package/ultra-tools/commands/db.cjs +0 -226
  236. package/ultra-tools/commands/legacy-memory.cjs +0 -159
  237. package/ultra-tools/commands/migrate.cjs +0 -1386
  238. package/ultra-tools/commands/session.cjs +0 -213
  239. package/ultra-tools/commands/status.cjs +0 -559
  240. package/ultra-tools/commands/system.cjs +0 -498
  241. 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
- };