ultra-builder-pro-cli 0.25.1 → 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 +40 -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 -1714
  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,790 +0,0 @@
1
- 'use strict';
2
-
3
- const crypto = require('node:crypto');
4
- const fs = require('node:fs');
5
- const path = require('node:path');
6
-
7
- const Ajv = require('ajv/dist/2020');
8
-
9
- const ops = require('./state-ops.cjs');
10
- const artifacts = require('./artifact-registry.cjs');
11
- const baselines = require('./baseline-workflow.cjs');
12
- const deltaSchema = require('../../spec/schemas/change-delta.v1.schema.json');
13
- const documentationSchema = require(
14
- '../../spec/schemas/documentation-reconciliation.v1.schema.json',
15
- );
16
-
17
- const ajv = new Ajv({ allErrors: true, strict: false });
18
- const validateDeltaSchema = ajv.compile(deltaSchema);
19
- const validateDocumentationSchema = ajv.compile(documentationSchema);
20
- const ID = /^[a-zA-Z0-9_-]+$/;
21
-
22
- class ChangePacketError extends Error {
23
- constructor(code, message, details = undefined) {
24
- super(message);
25
- this.name = 'ChangePacketError';
26
- this.code = code;
27
- if (details !== undefined) this.details = details;
28
- }
29
- }
30
-
31
- function nowIso() {
32
- return new Date().toISOString();
33
- }
34
-
35
- function digestBytes(bytes) {
36
- return crypto.createHash('sha256').update(bytes).digest('hex');
37
- }
38
-
39
- function digestFile(file) {
40
- return digestBytes(fs.readFileSync(file));
41
- }
42
-
43
- function canonical(value) {
44
- if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;
45
- if (value && typeof value === 'object') {
46
- return `{${Object.keys(value).sort().map(
47
- (key) => `${JSON.stringify(key)}:${canonical(value[key])}`,
48
- ).join(',')}}`;
49
- }
50
- return JSON.stringify(value);
51
- }
52
-
53
- function sameValue(left, right) {
54
- return canonical(left) === canonical(right);
55
- }
56
-
57
- function safeFile(rootDir, relative) {
58
- const normalized = artifacts.normalizeRelativePath(relative);
59
- const root = path.resolve(rootDir);
60
- const file = path.resolve(root, normalized);
61
- if (file !== root && !file.startsWith(`${root}${path.sep}`)) {
62
- throw new ChangePacketError('VALIDATION_ERROR', `path escapes project root: ${relative}`);
63
- }
64
- return { relative: normalized, file };
65
- }
66
-
67
- function requireInside(rootDir, relative, prefix, field) {
68
- const resolved = safeFile(rootDir, relative);
69
- const normalizedPrefix = artifacts.normalizeRelativePath(prefix);
70
- if (resolved.relative !== normalizedPrefix
71
- && !resolved.relative.startsWith(`${normalizedPrefix}/`)) {
72
- throw new ChangePacketError(
73
- 'CHANGE_OVERLAY_PATH_INVALID',
74
- `${field} must stay inside ${normalizedPrefix}`,
75
- );
76
- }
77
- return resolved;
78
- }
79
-
80
- function deliveryConsumer() {
81
- return { type: 'external', id: 'ultra-deliver', relation: 'consumed_by' };
82
- }
83
-
84
- function readCurrentArtifact(db, changeId, kind) {
85
- const rows = db.prepare(
86
- `SELECT id FROM artifacts
87
- WHERE owner_type = 'change' AND owner_id = ? AND kind = ?
88
- AND status <> 'archived'
89
- ORDER BY updated_at DESC, rowid DESC`,
90
- ).all(changeId, kind);
91
- if (rows.length !== 1) return null;
92
- const artifact = artifacts.getArtifact(db, { id: rows[0].id });
93
- return artifact.status === 'current' ? artifact : null;
94
- }
95
-
96
- function exactBaselineAnchor(db, change, input) {
97
- const baseline = baselines.readBaseline(db);
98
- if (!baseline || baseline.status !== 'ready') {
99
- throw new ChangePacketError(
100
- 'BASELINE_NOT_READY',
101
- 'typed Change delta requires one approved ready baseline',
102
- );
103
- }
104
- const anchor = input.baseline_anchor;
105
- if (!anchor || anchor.baseline_id !== baseline.id
106
- || (anchor.repository_revision || null) !== (baseline.repository_revision || null)) {
107
- throw new ChangePacketError(
108
- 'CHANGE_DELTA_BASELINE_CONFLICT',
109
- 'delta baseline id or repository revision is stale',
110
- {
111
- expected_baseline_id: baseline.id,
112
- expected_repository_revision: baseline.repository_revision || null,
113
- },
114
- );
115
- }
116
- const expected = [...baseline.spec_refs]
117
- .map((item) => ({ path: artifacts.normalizeRelativePath(item.path), digest: item.digest }))
118
- .sort((left, right) => left.path.localeCompare(right.path));
119
- const actual = [...(anchor.specs || [])]
120
- .map((item) => ({ path: artifacts.normalizeRelativePath(item.path), digest: item.digest }))
121
- .sort((left, right) => left.path.localeCompare(right.path));
122
- if (!sameValue(expected, actual)) {
123
- throw new ChangePacketError(
124
- 'CHANGE_DELTA_BASELINE_CONFLICT',
125
- 'delta specification anchors do not match the current baseline',
126
- { expected, actual },
127
- );
128
- }
129
- for (const spec of expected) {
130
- const resolved = safeFile(input.root_dir, spec.path);
131
- if (!fs.existsSync(resolved.file) || digestFile(resolved.file) !== spec.digest) {
132
- throw new ChangePacketError(
133
- 'CHANGE_DELTA_BASELINE_CONFLICT',
134
- `baseline specification bytes are stale: ${spec.path}`,
135
- );
136
- }
137
- }
138
- return baseline;
139
- }
140
-
141
- function normalizeDelta(db, change, input, rootDir) {
142
- const candidate = {
143
- $schema: 'ultra-change-delta-v1',
144
- change_id: change.id,
145
- baseline_anchor: input.baseline_anchor,
146
- decisions: input.decisions || [],
147
- non_goals: input.non_goals || [],
148
- acceptance: input.acceptance || [],
149
- documentation_impact: input.documentation_impact,
150
- unknowns: input.unknowns || [],
151
- ...(input.no_semantic_change_reason === undefined
152
- ? {}
153
- : { no_semantic_change_reason: input.no_semantic_change_reason }),
154
- mutations: input.mutations || [],
155
- };
156
- if (!validateDeltaSchema(candidate)) {
157
- throw new ChangePacketError(
158
- 'CHANGE_DELTA_INVALID',
159
- `change delta does not satisfy its schema: ${ajv.errorsText(validateDeltaSchema.errors)}`,
160
- validateDeltaSchema.errors,
161
- );
162
- }
163
- const baseline = exactBaselineAnchor(db, change, {
164
- ...candidate,
165
- root_dir: rootDir,
166
- });
167
- if (!sameValue(candidate.non_goals, change.contract.non_goals)
168
- || !sameValue(candidate.acceptance, change.contract.acceptance)
169
- || !sameValue(candidate.documentation_impact, change.docs_impact)) {
170
- throw new ChangePacketError(
171
- 'CHANGE_DELTA_CONTRACT_MISMATCH',
172
- 'delta non-goals, acceptance, and documentation impact must match accepted Change authority',
173
- );
174
- }
175
- const acceptanceIds = new Set(change.contract.acceptance.map((item) => item.id));
176
- const documentationPaths = new Set(change.docs_impact.files || []);
177
- const mutationIds = new Set();
178
- const targetPaths = new Set();
179
- const overlayRoot = `${change.artifact_root}/delta`;
180
- const normalizedMutations = candidate.mutations.map((mutation) => {
181
- if (!ID.test(mutation.id) || mutationIds.has(mutation.id)) {
182
- throw new ChangePacketError(
183
- 'CHANGE_DELTA_INVALID', `duplicate or invalid mutation id: ${mutation.id}`,
184
- );
185
- }
186
- mutationIds.add(mutation.id);
187
- const target = safeFile(rootDir, mutation.target_path);
188
- if (targetPaths.has(target.relative)) {
189
- throw new ChangePacketError(
190
- 'CHANGE_DELTA_INVALID', `duplicate mutation target: ${target.relative}`,
191
- );
192
- }
193
- targetPaths.add(target.relative);
194
- const overlay = requireInside(
195
- rootDir, mutation.overlay_path, overlayRoot, 'mutations.overlay_path',
196
- );
197
- if (!fs.existsSync(overlay.file) || !fs.statSync(overlay.file).isFile()) {
198
- throw new ChangePacketError(
199
- 'CHANGE_DELTA_PAYLOAD_MISSING', `delta overlay file is missing: ${overlay.relative}`,
200
- );
201
- }
202
- const actualAfter = digestFile(overlay.file);
203
- if (actualAfter !== mutation.after_digest) {
204
- throw new ChangePacketError(
205
- 'CHANGE_DELTA_PAYLOAD_STALE',
206
- `delta overlay digest is stale: ${overlay.relative}`,
207
- );
208
- }
209
- const targetExists = fs.existsSync(target.file);
210
- const actualBefore = targetExists ? digestFile(target.file) : null;
211
- if ((mutation.action === 'add' && (targetExists || mutation.before_digest !== null))
212
- || (mutation.action === 'update'
213
- && (!targetExists || mutation.before_digest !== actualBefore))) {
214
- throw new ChangePacketError(
215
- 'CHANGE_DELTA_BASELINE_CONFLICT',
216
- `delta before-state is stale: ${target.relative}`,
217
- { expected: mutation.before_digest, actual: actualBefore },
218
- );
219
- }
220
- const baselineRef = baseline.spec_refs.find((item) => (
221
- artifacts.normalizeRelativePath(item.path) === target.relative
222
- ));
223
- if (mutation.action === 'update'
224
- && (!baselineRef || baselineRef.digest !== mutation.before_digest)) {
225
- throw new ChangePacketError(
226
- 'CHANGE_DELTA_BASELINE_CONFLICT',
227
- `delta target is not anchored by the current baseline: ${target.relative}`,
228
- );
229
- }
230
- if (mutation.acceptance_refs.some((id) => !acceptanceIds.has(id))) {
231
- throw new ChangePacketError(
232
- 'CHANGE_DELTA_ACCEPTANCE_INVALID',
233
- `delta mutation ${mutation.id} references unknown acceptance`,
234
- );
235
- }
236
- if (mutation.documentation_refs.some((item) => !documentationPaths.has(item))) {
237
- throw new ChangePacketError(
238
- 'CHANGE_DELTA_DOCUMENTATION_INVALID',
239
- `delta mutation ${mutation.id} references undeclared documentation`,
240
- );
241
- }
242
- return {
243
- ...mutation,
244
- target_path: target.relative,
245
- overlay_path: overlay.relative,
246
- };
247
- });
248
- const documented = new Set(normalizedMutations.flatMap((item) => item.documentation_refs));
249
- if ([...documentationPaths].some((item) => !documented.has(item))) {
250
- throw new ChangePacketError(
251
- 'CHANGE_DELTA_DOCUMENTATION_INVALID',
252
- 'every required documentation path must be traced from at least one mutation',
253
- );
254
- }
255
- return {
256
- ...candidate,
257
- baseline_anchor: {
258
- ...candidate.baseline_anchor,
259
- specs: [...candidate.baseline_anchor.specs]
260
- .sort((left, right) => left.path.localeCompare(right.path)),
261
- },
262
- decisions: [...candidate.decisions].sort((left, right) => left.id.localeCompare(right.id)),
263
- non_goals: [...candidate.non_goals].sort(),
264
- acceptance: [...candidate.acceptance].sort((left, right) => left.id.localeCompare(right.id)),
265
- unknowns: [...candidate.unknowns].sort((left, right) => left.id.localeCompare(right.id)),
266
- mutations: normalizedMutations.sort((left, right) => left.id.localeCompare(right.id)),
267
- };
268
- }
269
-
270
- function atomicWrite(file, bytes) {
271
- fs.mkdirSync(path.dirname(file), { recursive: true });
272
- const previous = fs.existsSync(file) ? fs.readFileSync(file) : null;
273
- const temporary = `${file}.tmp-${process.pid}-${crypto.randomUUID()}`;
274
- fs.writeFileSync(temporary, bytes);
275
- fs.renameSync(temporary, file);
276
- return () => {
277
- if (previous === null) fs.rmSync(file, { force: true });
278
- else fs.writeFileSync(file, previous);
279
- };
280
- }
281
-
282
- function renderProgress(change, { delta = null, reconciliation = null } = {}) {
283
- return [
284
- `# Progress: ${change.title}`,
285
- '',
286
- `Change: \`${change.id}\``,
287
- `Status: \`${change.status}\``,
288
- `Delta: ${delta ? `\`${delta.id}\` @ \`${delta.digest}\`` : 'not recorded'}`,
289
- `Documentation reconciliation: ${reconciliation
290
- ? `\`${reconciliation.id}\` @ \`${reconciliation.digest}\``
291
- : 'not recorded'}`,
292
- '',
293
- 'This file is a deterministic projection of registered Change authorities.',
294
- '',
295
- ].join('\n');
296
- }
297
-
298
- function writeProgressInTx(db, change, rootDir, { delta = null, reconciliation = null } = {}) {
299
- const relative = `${change.artifact_root}/progress.md`;
300
- const resolved = safeFile(rootDir, relative);
301
- const rollback = atomicWrite(
302
- resolved.file,
303
- renderProgress(change, { delta, reconciliation }),
304
- );
305
- try {
306
- const sources = [
307
- { type: 'change', id: change.id, relation: 'projected_from' },
308
- ...(delta ? [{ type: 'artifact', id: delta.id, relation: 'projected_from' }] : []),
309
- ...(reconciliation
310
- ? [{ type: 'artifact', id: reconciliation.id, relation: 'projected_from' }]
311
- : []),
312
- ];
313
- const result = artifacts.recordArtifactInTx(db, {
314
- id: `change-${change.id}-progress`,
315
- owner_type: 'change',
316
- owner_id: change.id,
317
- kind: 'progress_projection',
318
- path: relative,
319
- source_refs: sources,
320
- consumer_refs: [],
321
- provenance: { writer: 'change-packet', generated: true },
322
- metadata: { projection: true, terminal_role: true },
323
- }, { rootDir });
324
- return { result, rollback };
325
- } catch (error) {
326
- rollback();
327
- throw error;
328
- }
329
- }
330
-
331
- function recordDelta(db, change, input, { rootDir = process.cwd() } = {}) {
332
- if (!change || !['active', 'blocked'].includes(change.status)) {
333
- throw new ChangePacketError(
334
- 'CHANGE_NOT_MUTABLE',
335
- `change ${change?.id || '(missing)'} cannot accept delta authority`,
336
- );
337
- }
338
- const delta = normalizeDelta(db, change, input, rootDir);
339
- const relative = `${change.artifact_root}/delta/change-delta.json`;
340
- const resolved = safeFile(rootDir, relative);
341
- const serialized = `${JSON.stringify(delta, null, 2)}\n`;
342
- const rollbackDelta = atomicWrite(resolved.file, serialized);
343
- let rollbackProgress = null;
344
- try {
345
- const result = ops.tx(db, () => {
346
- const consumer = deliveryConsumer();
347
- const payloads = delta.mutations.map((mutation) => (
348
- artifacts.recordArtifactInTx(db, {
349
- id: `change-${change.id}-delta-${mutation.id}`,
350
- owner_type: 'change',
351
- owner_id: change.id,
352
- kind: 'delta_payload',
353
- path: mutation.overlay_path,
354
- content_digest: mutation.after_digest,
355
- source_refs: [{ type: 'change', id: change.id, relation: 'proposed_by' }],
356
- consumer_refs: [consumer],
357
- provenance: { writer: 'change.delta', mutation_id: mutation.id },
358
- metadata: {
359
- target_path: mutation.target_path,
360
- action: mutation.action,
361
- before_digest: mutation.before_digest,
362
- after_digest: mutation.after_digest,
363
- },
364
- }, { rootDir }).artifact
365
- ));
366
- const recorded = artifacts.recordArtifactInTx(db, {
367
- id: `change-${change.id}-delta`,
368
- owner_type: 'change',
369
- owner_id: change.id,
370
- kind: 'change_delta',
371
- path: relative,
372
- content_digest: digestBytes(serialized),
373
- source_refs: [
374
- { type: 'change', id: change.id, relation: 'normalized_from' },
375
- {
376
- type: 'baseline',
377
- id: delta.baseline_anchor.baseline_id,
378
- relation: 'deltas_from',
379
- },
380
- ...payloads.map((artifact) => ({
381
- type: 'artifact', id: artifact.id, relation: 'contains_payload',
382
- })),
383
- ],
384
- consumer_refs: [consumer],
385
- provenance: {
386
- writer: 'change.delta',
387
- recorded_at: nowIso(),
388
- baseline_revision: delta.baseline_anchor.repository_revision,
389
- },
390
- metadata: {
391
- mutation_ids: delta.mutations.map((item) => item.id),
392
- acceptance_ids: delta.acceptance.map((item) => item.id),
393
- },
394
- }, { rootDir });
395
- const progress = writeProgressInTx(
396
- db,
397
- change,
398
- rootDir,
399
- { delta: recorded.artifact },
400
- );
401
- rollbackProgress = progress.rollback;
402
- ops.appendEventInTx(db, {
403
- type: 'change_delta_recorded',
404
- change_id: change.id,
405
- payload: {
406
- artifact_id: recorded.artifact.id,
407
- digest: recorded.artifact.digest,
408
- mutation_ids: delta.mutations.map((item) => item.id),
409
- },
410
- });
411
- return {
412
- delta,
413
- artifact: recorded.artifact,
414
- payload_artifacts: payloads,
415
- progress_artifact: progress.result.artifact,
416
- };
417
- });
418
- return result;
419
- } catch (error) {
420
- if (rollbackProgress) rollbackProgress();
421
- rollbackDelta();
422
- throw error;
423
- }
424
- }
425
-
426
- function normalizeDocumentation(db, change, input, rootDir) {
427
- const candidate = {
428
- $schema: 'ultra-documentation-reconciliation-v1',
429
- change_id: change.id,
430
- delta_artifact_id: input.delta_artifact_id,
431
- delta_digest: input.delta_digest,
432
- ...(input.no_change_reason === undefined
433
- ? {}
434
- : { no_change_reason: input.no_change_reason }),
435
- documents: input.documents || [],
436
- };
437
- if (!validateDocumentationSchema(candidate)) {
438
- throw new ChangePacketError(
439
- 'DOCUMENTATION_RECONCILIATION_INVALID',
440
- `documentation reconciliation does not satisfy its schema: ${ajv.errorsText(validateDocumentationSchema.errors)}`,
441
- validateDocumentationSchema.errors,
442
- );
443
- }
444
- const deltaArtifact = artifacts.getArtifact(db, { id: candidate.delta_artifact_id });
445
- if (deltaArtifact.owner_type !== 'change' || deltaArtifact.owner_id !== change.id
446
- || deltaArtifact.kind !== 'change_delta' || deltaArtifact.status !== 'current'
447
- || deltaArtifact.digest !== candidate.delta_digest) {
448
- throw new ChangePacketError(
449
- 'DOCUMENTATION_DELTA_AUTHORITY_INVALID',
450
- 'documentation reconciliation must bind the current Change delta digest',
451
- );
452
- }
453
- const deltaFile = safeFile(rootDir, deltaArtifact.path);
454
- if (!fs.existsSync(deltaFile.file) || digestFile(deltaFile.file) !== deltaArtifact.digest) {
455
- throw new ChangePacketError(
456
- 'DOCUMENTATION_DELTA_AUTHORITY_INVALID',
457
- 'documentation reconciliation delta bytes are stale',
458
- );
459
- }
460
- const delta = JSON.parse(fs.readFileSync(deltaFile.file, 'utf8'));
461
- const deltaIds = new Set(delta.mutations.map((item) => item.id));
462
- const acceptanceIds = new Set(delta.acceptance.map((item) => item.id));
463
- const requiredPaths = new Set(change.docs_impact.status === 'required'
464
- ? change.docs_impact.files
465
- : []);
466
- const actualPaths = new Set();
467
- const documentationRoot = `${change.artifact_root}/documentation`;
468
- const documents = candidate.documents.map((document) => {
469
- if (actualPaths.has(document.path)) {
470
- throw new ChangePacketError(
471
- 'DOCUMENTATION_RECONCILIATION_INVALID',
472
- `duplicate documentation path: ${document.path}`,
473
- );
474
- }
475
- const target = safeFile(rootDir, document.path);
476
- actualPaths.add(target.relative);
477
- const overlay = requireInside(
478
- rootDir, document.overlay_path, documentationRoot, 'documents.overlay_path',
479
- );
480
- if (!fs.existsSync(overlay.file) || !fs.statSync(overlay.file).isFile()
481
- || digestFile(overlay.file) !== document.after_digest) {
482
- throw new ChangePacketError(
483
- 'DOCUMENTATION_EVIDENCE_STALE',
484
- `documentation overlay is missing or stale: ${overlay.relative}`,
485
- );
486
- }
487
- const targetExists = fs.existsSync(target.file);
488
- const actualBefore = targetExists ? digestFile(target.file) : null;
489
- if ((document.action === 'add' && (targetExists || document.before_digest !== null))
490
- || (document.action === 'update'
491
- && (!targetExists || actualBefore !== document.before_digest))) {
492
- throw new ChangePacketError(
493
- 'DOCUMENTATION_BASELINE_CONFLICT',
494
- `documentation before-state is stale: ${target.relative}`,
495
- );
496
- }
497
- if (document.delta_refs.some((id) => !deltaIds.has(id))
498
- || document.acceptance_refs.some((id) => !acceptanceIds.has(id))) {
499
- throw new ChangePacketError(
500
- 'DOCUMENTATION_TRACE_INVALID',
501
- `documentation trace references are invalid: ${target.relative}`,
502
- );
503
- }
504
- if (document.consumers.length === 0
505
- && String(document.no_consumer_reason || '').trim().length < 3) {
506
- throw new ChangePacketError(
507
- 'DOCUMENTATION_ORPHAN_UNRESOLVED',
508
- `documentation path has no verified consumer: ${target.relative}`,
509
- );
510
- }
511
- return {
512
- ...document,
513
- path: target.relative,
514
- overlay_path: overlay.relative,
515
- };
516
- });
517
- if (!sameValue([...requiredPaths].sort(), [...actualPaths].sort())) {
518
- throw new ChangePacketError(
519
- 'DOCUMENTATION_RECONCILIATION_INCOMPLETE',
520
- 'documentation reconciliation must cover the exact declared documentation impact',
521
- { required: [...requiredPaths].sort(), actual: [...actualPaths].sort() },
522
- );
523
- }
524
- return {
525
- reconciliation: {
526
- ...candidate,
527
- documents: documents.sort((left, right) => left.path.localeCompare(right.path)),
528
- },
529
- deltaArtifact,
530
- };
531
- }
532
-
533
- function recordDocumentationReconciliation(
534
- db,
535
- change,
536
- input,
537
- { rootDir = process.cwd() } = {},
538
- ) {
539
- if (!change || !['active', 'blocked'].includes(change.status)) {
540
- throw new ChangePacketError(
541
- 'CHANGE_NOT_MUTABLE',
542
- `change ${change?.id || '(missing)'} cannot accept documentation reconciliation`,
543
- );
544
- }
545
- const normalized = normalizeDocumentation(db, change, input, rootDir);
546
- const relative = `${change.artifact_root}/documentation/reconciliation.json`;
547
- const resolved = safeFile(rootDir, relative);
548
- const serialized = `${JSON.stringify(normalized.reconciliation, null, 2)}\n`;
549
- const rollbackReconciliation = atomicWrite(resolved.file, serialized);
550
- let rollbackProgress = null;
551
- try {
552
- return ops.tx(db, () => {
553
- const fallbackConsumer = deliveryConsumer();
554
- const payloads = normalized.reconciliation.documents.map((document, index) => (
555
- artifacts.recordArtifactInTx(db, {
556
- id: `change-${change.id}-documentation-${index + 1}`,
557
- owner_type: 'change',
558
- owner_id: change.id,
559
- kind: 'documentation_payload',
560
- path: document.overlay_path,
561
- content_digest: document.after_digest,
562
- source_refs: [{
563
- type: 'artifact',
564
- id: normalized.deltaArtifact.id,
565
- relation: 'documents',
566
- }],
567
- consumer_refs: document.consumers.length > 0
568
- ? document.consumers
569
- : [fallbackConsumer],
570
- provenance: { writer: 'change.documentation_reconcile', target_path: document.path },
571
- metadata: {
572
- target_path: document.path,
573
- action: document.action,
574
- before_digest: document.before_digest,
575
- after_digest: document.after_digest,
576
- orphan_disposition: document.consumers.length > 0
577
- ? 'consumers_verified'
578
- : 'no_consumer_reason',
579
- },
580
- }, { rootDir }).artifact
581
- ));
582
- const recorded = artifacts.recordArtifactInTx(db, {
583
- id: `change-${change.id}-documentation-reconciliation`,
584
- owner_type: 'change',
585
- owner_id: change.id,
586
- kind: 'documentation_reconciliation',
587
- path: relative,
588
- content_digest: digestBytes(serialized),
589
- source_refs: [
590
- {
591
- type: 'artifact',
592
- id: normalized.deltaArtifact.id,
593
- relation: 'reconciles',
594
- },
595
- ...payloads.map((artifact) => ({
596
- type: 'artifact', id: artifact.id, relation: 'contains_document',
597
- })),
598
- ],
599
- consumer_refs: [fallbackConsumer],
600
- provenance: { writer: 'change.documentation_reconcile', recorded_at: nowIso() },
601
- metadata: {
602
- document_paths: normalized.reconciliation.documents.map((item) => item.path),
603
- orphan_check: 'pass',
604
- },
605
- }, { rootDir });
606
- const progress = writeProgressInTx(db, change, rootDir, {
607
- delta: normalized.deltaArtifact,
608
- reconciliation: recorded.artifact,
609
- });
610
- rollbackProgress = progress.rollback;
611
- ops.appendEventInTx(db, {
612
- type: 'documentation_reconciliation_recorded',
613
- change_id: change.id,
614
- payload: {
615
- artifact_id: recorded.artifact.id,
616
- digest: recorded.artifact.digest,
617
- document_paths: normalized.reconciliation.documents.map((item) => item.path),
618
- },
619
- });
620
- return {
621
- reconciliation: normalized.reconciliation,
622
- artifact: recorded.artifact,
623
- payload_artifacts: payloads,
624
- progress_artifact: progress.result.artifact,
625
- };
626
- });
627
- } catch (error) {
628
- if (rollbackProgress) rollbackProgress();
629
- rollbackReconciliation();
630
- throw error;
631
- }
632
- }
633
-
634
- function readDeltaAuthority(db, changeId) {
635
- return readCurrentArtifact(db, changeId, 'change_delta');
636
- }
637
-
638
- function readDocumentationAuthority(db, changeId) {
639
- return readCurrentArtifact(db, changeId, 'documentation_reconciliation');
640
- }
641
-
642
- function readAuthorityJson(db, changeId, kind, schema, rootDir) {
643
- const artifact = readCurrentArtifact(db, changeId, kind);
644
- if (!artifact) {
645
- throw new ChangePacketError(
646
- kind === 'change_delta'
647
- ? 'CHANGE_DELTA_AUTHORITY_MISSING'
648
- : 'DOCUMENTATION_RECONCILIATION_MISSING',
649
- `change ${changeId} requires one current ${kind} authority`,
650
- );
651
- }
652
- const resolved = safeFile(rootDir, artifact.path);
653
- if (!fs.existsSync(resolved.file) || !fs.statSync(resolved.file).isFile()
654
- || digestFile(resolved.file) !== artifact.digest) {
655
- throw new ChangePacketError(
656
- kind === 'change_delta'
657
- ? 'CHANGE_DELTA_AUTHORITY_STALE'
658
- : 'DOCUMENTATION_RECONCILIATION_STALE',
659
- `${kind} bytes do not match registered authority`,
660
- );
661
- }
662
- let value;
663
- try { value = JSON.parse(fs.readFileSync(resolved.file, 'utf8')); }
664
- catch (error) {
665
- throw new ChangePacketError(
666
- kind === 'change_delta'
667
- ? 'CHANGE_DELTA_INVALID'
668
- : 'DOCUMENTATION_RECONCILIATION_INVALID',
669
- `${kind} is not valid JSON: ${error.message}`,
670
- );
671
- }
672
- const validator = schema === 'delta' ? validateDeltaSchema : validateDocumentationSchema;
673
- if (!validator(value)) {
674
- throw new ChangePacketError(
675
- schema === 'delta'
676
- ? 'CHANGE_DELTA_INVALID'
677
- : 'DOCUMENTATION_RECONCILIATION_INVALID',
678
- `${kind} no longer satisfies its schema: ${ajv.errorsText(validator.errors)}`,
679
- validator.errors,
680
- );
681
- }
682
- if (value.change_id !== changeId) {
683
- throw new ChangePacketError(
684
- 'CHANGE_PACKET_AUTHORITY_MISMATCH',
685
- `${kind} belongs to ${value.change_id}, not ${changeId}`,
686
- );
687
- }
688
- return { artifact, value };
689
- }
690
-
691
- function loadDelta(db, changeId, { rootDir = process.cwd() } = {}) {
692
- return readAuthorityJson(db, changeId, 'change_delta', 'delta', rootDir);
693
- }
694
-
695
- function loadDocumentationReconciliation(
696
- db,
697
- changeId,
698
- { rootDir = process.cwd() } = {},
699
- ) {
700
- return readAuthorityJson(
701
- db,
702
- changeId,
703
- 'documentation_reconciliation',
704
- 'documentation',
705
- rootDir,
706
- );
707
- }
708
-
709
- function deliveryEntries(db, change, { rootDir = process.cwd() } = {}) {
710
- const delta = loadDelta(db, change.id, { rootDir });
711
- const documentation = loadDocumentationReconciliation(db, change.id, { rootDir });
712
- if (
713
- documentation.value.delta_artifact_id !== delta.artifact.id
714
- || documentation.value.delta_digest !== delta.artifact.digest
715
- ) {
716
- throw new ChangePacketError(
717
- 'DOCUMENTATION_DELTA_AUTHORITY_INVALID',
718
- 'documentation reconciliation is not bound to the current Change delta',
719
- );
720
- }
721
- const entries = [
722
- ...delta.value.mutations.map((item) => ({
723
- kind: 'baseline_specification',
724
- id: item.id,
725
- target_path: item.target_path,
726
- overlay_path: item.overlay_path,
727
- action: item.action,
728
- before_digest: item.before_digest,
729
- after_digest: item.after_digest,
730
- })),
731
- ...documentation.value.documents.map((item, index) => ({
732
- kind: 'documentation',
733
- id: `documentation-${index + 1}`,
734
- target_path: item.path,
735
- overlay_path: item.overlay_path,
736
- action: item.action,
737
- before_digest: item.before_digest,
738
- after_digest: item.after_digest,
739
- })),
740
- ];
741
- const targets = new Set();
742
- for (const entry of entries) {
743
- if (targets.has(entry.target_path)) {
744
- throw new ChangePacketError(
745
- 'CHANGE_PACKET_TARGET_CONFLICT',
746
- `multiple delivery entries target ${entry.target_path}`,
747
- );
748
- }
749
- targets.add(entry.target_path);
750
- const target = safeFile(rootDir, entry.target_path);
751
- const overlay = safeFile(rootDir, entry.overlay_path);
752
- const actualBefore = fs.existsSync(target.file) ? digestFile(target.file) : null;
753
- const actualAfter = fs.existsSync(overlay.file) ? digestFile(overlay.file) : null;
754
- if (actualBefore !== entry.before_digest) {
755
- throw new ChangePacketError(
756
- entry.kind === 'documentation'
757
- ? 'DOCUMENTATION_BASELINE_CONFLICT'
758
- : 'CHANGE_DELTA_BASELINE_CONFLICT',
759
- `delivery before-state changed: ${entry.target_path}`,
760
- { expected: entry.before_digest, actual: actualBefore },
761
- );
762
- }
763
- if (actualAfter !== entry.after_digest) {
764
- throw new ChangePacketError(
765
- entry.kind === 'documentation'
766
- ? 'DOCUMENTATION_EVIDENCE_STALE'
767
- : 'CHANGE_DELTA_PAYLOAD_STALE',
768
- `delivery overlay changed: ${entry.overlay_path}`,
769
- { expected: entry.after_digest, actual: actualAfter },
770
- );
771
- }
772
- }
773
- return {
774
- delta,
775
- documentation,
776
- entries,
777
- baseline_updates: delta.value.mutations.map((item) => item.target_path),
778
- };
779
- }
780
-
781
- module.exports = {
782
- ChangePacketError,
783
- recordDelta,
784
- recordDocumentationReconciliation,
785
- readDeltaAuthority,
786
- readDocumentationAuthority,
787
- loadDelta,
788
- loadDocumentationReconciliation,
789
- deliveryEntries,
790
- };