ultra-builder-pro-cli 0.25.1 → 0.26.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (243) hide show
  1. package/.ultra-template/changes/abandoned/.gitkeep +0 -0
  2. package/.ultra-template/contexts/TEMPLATE.md +94 -0
  3. package/.ultra-template/decisions/.gitkeep +1 -0
  4. package/.ultra-template/evidence/.gitkeep +1 -0
  5. package/.ultra-template/north-star.md +46 -0
  6. package/.ultra-template/research/.gitkeep +1 -0
  7. package/.ultra-template/specs/discovery.md +2 -2
  8. package/.ultra-template/tasks.json +3 -0
  9. package/.ultra-template/test-report.json +27 -0
  10. package/CHANGELOG.md +107 -1
  11. package/README.md +217 -579
  12. package/adapters/_shared/delegate-profiles/kimi-read-only.md +15 -0
  13. package/adapters/_shared/delegate-profiles/kimi-write.md +17 -0
  14. package/adapters/_shared/file-ops.cjs +76 -2
  15. package/adapters/_shared/host-profile.cjs +97 -0
  16. package/adapters/_shared/plugin-core.cjs +204 -0
  17. package/adapters/_shared/runtime-assets.cjs +31 -96
  18. package/adapters/claude.js +92 -291
  19. package/adapters/codex.js +309 -842
  20. package/adapters/grok.js +143 -1469
  21. package/adapters/kimi.js +124 -1053
  22. package/adapters/opencode.js +230 -742
  23. package/bin/delegate-worker.cjs +328 -0
  24. package/bin/delegate.cjs +463 -0
  25. package/bin/install.js +38 -11
  26. package/docs/ARCHITECTURE.md +125 -216
  27. package/docs/ARTIFACT-AUTHORITY.md +90 -268
  28. package/docs/DECISIONS.md +77 -87
  29. package/docs/PHILOSOPHY.md +208 -0
  30. package/docs/PLUGIN-ISOLATION-CONTRACT.md +65 -39
  31. package/docs/RUNTIME-COMPAT-MATRIX.md +140 -155
  32. package/docs/SKILL-AUTHORING.md +94 -73
  33. package/docs/WORKFLOW-LIFECYCLE.md +97 -189
  34. package/hooks/README.md +19 -37
  35. package/hooks/_common.py +113 -0
  36. package/hooks/adapters/codex.py +9 -16
  37. package/hooks/adapters/grok.py +5 -5
  38. package/hooks/adapters/kimi.py +8 -14
  39. package/hooks/block_dangerous_commands.py +201 -0
  40. package/hooks/compact_context.py +65 -0
  41. package/hooks/mid_workflow_recall.py +25 -0
  42. package/hooks/post_edit_guard.py +105 -0
  43. package/hooks/session_context.py +30 -0
  44. package/package.json +17 -52
  45. package/skills/ultra-change/SKILL.md +115 -50
  46. package/skills/ultra-change/references/change-contract.md +68 -3
  47. package/skills/ultra-delegate/SKILL.md +89 -0
  48. package/skills/ultra-delegate/references/delegation-contract.md +59 -0
  49. package/skills/ultra-delegate/scripts/delegate_wait.py +86 -0
  50. package/skills/ultra-deliver/SKILL.md +98 -42
  51. package/skills/ultra-deliver/references/baseline-reconciliation.md +39 -20
  52. package/skills/ultra-dev/SKILL.md +115 -62
  53. package/skills/ultra-dev/references/debugging.md +22 -0
  54. package/skills/ultra-domain-modeling/SKILL.md +79 -0
  55. package/skills/ultra-grilling/SKILL.md +118 -0
  56. package/skills/ultra-grilling/references/reframing.md +72 -0
  57. package/skills/ultra-init/SKILL.md +102 -37
  58. package/skills/ultra-init/references/brownfield-adoption.md +33 -83
  59. package/skills/ultra-init/scripts/init_project.cjs +84 -0
  60. package/skills/ultra-plan/SKILL.md +101 -68
  61. package/skills/ultra-research/SKILL.md +68 -48
  62. package/skills/ultra-research/references/00-problem-validation.md +5 -5
  63. package/skills/ultra-research/references/01-opportunity-discovery.md +3 -3
  64. package/skills/ultra-research/references/02-market-assessment.md +5 -4
  65. package/skills/ultra-research/references/03-competitive-landscape.md +3 -3
  66. package/skills/ultra-research/references/04-product-strategy.md +3 -3
  67. package/skills/ultra-research/references/05-assumptions-validation.md +5 -5
  68. package/skills/ultra-research/references/10-user-personas.md +3 -2
  69. package/skills/ultra-research/references/11-user-scenarios.md +3 -3
  70. package/skills/ultra-research/references/20-user-stories.md +4 -3
  71. package/skills/ultra-research/references/21-features-scope.md +3 -2
  72. package/skills/ultra-research/references/22-success-metrics.md +3 -3
  73. package/skills/ultra-research/references/30-architecture-context.md +3 -3
  74. package/skills/ultra-research/references/31-solution-strategy.md +3 -3
  75. package/skills/ultra-research/references/32-building-blocks.md +3 -3
  76. package/skills/ultra-research/references/40-deployment.md +3 -3
  77. package/skills/ultra-research/references/41-quality-risks.md +3 -3
  78. package/skills/ultra-research/references/99-synthesis.md +7 -7
  79. package/skills/ultra-review/SKILL.md +84 -32
  80. package/skills/ultra-review/references/code.md +17 -0
  81. package/skills/ultra-review/references/comments.md +12 -0
  82. package/skills/ultra-review/references/design.md +12 -0
  83. package/skills/ultra-review/references/errors.md +13 -0
  84. package/skills/ultra-review/references/spec.md +13 -0
  85. package/skills/ultra-review/references/tests.md +12 -0
  86. package/skills/ultra-review/references/worker-packet.md +40 -0
  87. package/skills/ultra-review/scripts/review_wait.py +26 -15
  88. package/skills/ultra-status/SKILL.md +61 -36
  89. package/skills/ultra-tdd/SKILL.md +81 -0
  90. package/skills/ultra-tdd/references/templates/README.md +20 -0
  91. package/skills/ultra-tdd/references/templates/feature-flag-default-audit.sh +79 -0
  92. package/skills/ultra-tdd/references/templates/persistence-real.ts +78 -0
  93. package/skills/ultra-tdd/references/templates/testcontainer-postgres.py +78 -0
  94. package/skills/ultra-tdd/references/templates/testcontainer-postgres.ts +73 -0
  95. package/skills/ultra-tdd/references/templates/vertical-slice.ts +66 -0
  96. package/skills/ultra-tdd/references/test-execution.md +18 -0
  97. package/skills/ultra-test/SKILL.md +75 -30
  98. package/skills/ultra-test/references/export-syntax.md +18 -0
  99. package/skills/ultra-test/scripts/worktree_digest.cjs +54 -0
  100. package/skills/ultra-think/SKILL.md +49 -27
  101. package/skills/ultra-think/references/autonomy-boundary.md +48 -0
  102. package/skills/ultra-think/references/interaction-boundary.md +18 -33
  103. package/.ultra-template/docs/research/README.md +0 -55
  104. package/.ultra-template/reports/templates/delivery-report.json +0 -17
  105. package/.ultra-template/reports/templates/test-report.json +0 -18
  106. package/.ultra-template/tasks/tasks.json +0 -15
  107. package/.ultra-template/templates/task-context.md +0 -19
  108. package/adapters/_shared/codex-assets.cjs +0 -725
  109. package/adapters/_shared/codex-ultra-tools-entry.cjs +0 -11
  110. package/adapters/_shared/interaction-contract.cjs +0 -129
  111. package/adapters/_shared/path-rewrite.cjs +0 -79
  112. package/adapters/_shared/runtime-native.cjs +0 -132
  113. package/adapters/_shared/settings-merge.cjs +0 -76
  114. package/agents/code-reviewer.md +0 -42
  115. package/agents/debugger.md +0 -40
  116. package/agents/review-code.md +0 -49
  117. package/agents/review-comments.md +0 -38
  118. package/agents/review-coordinator.md +0 -56
  119. package/agents/review-design.md +0 -40
  120. package/agents/review-errors.md +0 -43
  121. package/agents/review-spec.md +0 -39
  122. package/agents/review-tests.md +0 -40
  123. package/agents/tdd-runner.md +0 -38
  124. package/bin/orchestrator.js +0 -383
  125. package/commands/ultra-change.md +0 -10
  126. package/commands/ultra-deliver.md +0 -10
  127. package/commands/ultra-dev.md +0 -10
  128. package/commands/ultra-doctor.md +0 -10
  129. package/commands/ultra-init.md +0 -10
  130. package/commands/ultra-plan.md +0 -10
  131. package/commands/ultra-research.md +0 -10
  132. package/commands/ultra-review.md +0 -10
  133. package/commands/ultra-status.md +0 -10
  134. package/commands/ultra-test.md +0 -10
  135. package/commands/ultra-think.md +0 -10
  136. package/docs/AGENT-CONTEXT.md +0 -191
  137. package/docs/COMMIT-HASH-BACKFILL.md +0 -51
  138. package/docs/LEGACY-CLI-CROSSWALK.md +0 -175
  139. package/docs/ROADMAP.md +0 -101
  140. package/docs/STATE-DB-ACCESS-POLICY.md +0 -161
  141. package/hooks/active_task_context.py +0 -152
  142. package/hooks/context_envelope.py +0 -100
  143. package/hooks/health_check.py +0 -196
  144. package/hooks/pre_stop_check.py +0 -66
  145. package/hooks/runtime_paths.py +0 -396
  146. package/hooks/subagent_tracker.py +0 -88
  147. package/hooks/workflow_checkpoint.py +0 -76
  148. package/hooks/workflow_context.py +0 -48
  149. package/hooks/workflow_resume.py +0 -84
  150. package/mcp-server/hook-context.cjs +0 -38
  151. package/mcp-server/hook-event.cjs +0 -34
  152. package/mcp-server/lib/archive-journal.cjs +0 -1119
  153. package/mcp-server/lib/archive-mutation-worker.py +0 -365
  154. package/mcp-server/lib/artifact-registry.cjs +0 -1232
  155. package/mcp-server/lib/baseline-workflow.cjs +0 -1193
  156. package/mcp-server/lib/canonical-json.cjs +0 -19
  157. package/mcp-server/lib/change-authority.cjs +0 -26
  158. package/mcp-server/lib/change-packet.cjs +0 -790
  159. package/mcp-server/lib/change-workflow.cjs +0 -65
  160. package/mcp-server/lib/context-envelope.cjs +0 -581
  161. package/mcp-server/lib/context-paths.cjs +0 -142
  162. package/mcp-server/lib/decision-records.cjs +0 -423
  163. package/mcp-server/lib/delivery-transaction.cjs +0 -471
  164. package/mcp-server/lib/doctor-backup-worker.cjs +0 -328
  165. package/mcp-server/lib/doctor.cjs +0 -627
  166. package/mcp-server/lib/git-bootstrap.cjs +0 -651
  167. package/mcp-server/lib/init-project.cjs +0 -1021
  168. package/mcp-server/lib/kernel-change-workflow.cjs +0 -2029
  169. package/mcp-server/lib/legacy-change-workflow.cjs +0 -3032
  170. package/mcp-server/lib/managed-file-write.cjs +0 -172
  171. package/mcp-server/lib/plan-checkpoint.cjs +0 -177
  172. package/mcp-server/lib/plan-publication-worker.cjs +0 -830
  173. package/mcp-server/lib/plan-store.cjs +0 -794
  174. package/mcp-server/lib/prd-parser.cjs +0 -83
  175. package/mcp-server/lib/pricing.cjs +0 -44
  176. package/mcp-server/lib/project-context.cjs +0 -178
  177. package/mcp-server/lib/projector.cjs +0 -386
  178. package/mcp-server/lib/provider-refs.cjs +0 -42
  179. package/mcp-server/lib/runtime-paths.cjs +0 -1714
  180. package/mcp-server/lib/runtime-state.cjs +0 -222
  181. package/mcp-server/lib/safe-project-file.cjs +0 -385
  182. package/mcp-server/lib/stage-checkpoints.cjs +0 -463
  183. package/mcp-server/lib/state-authority.cjs +0 -95
  184. package/mcp-server/lib/state-db.cjs +0 -1580
  185. package/mcp-server/lib/state-ops.cjs +0 -1428
  186. package/mcp-server/lib/task-contract.cjs +0 -404
  187. package/mcp-server/lib/task-expander.cjs +0 -105
  188. package/mcp-server/lib/task-ledger.cjs +0 -1964
  189. package/mcp-server/lib/telemetry.cjs +0 -85
  190. package/mcp-server/lib/topo.cjs +0 -131
  191. package/mcp-server/lib/ultra-facade.cjs +0 -2684
  192. package/mcp-server/lib/worker-packet.cjs +0 -358
  193. package/mcp-server/server.cjs +0 -437
  194. package/orchestrator/auto-merge.cjs +0 -128
  195. package/orchestrator/daemon.cjs +0 -264
  196. package/orchestrator/dispatch-rules.cjs +0 -78
  197. package/orchestrator/parallel-orchestrator.cjs +0 -903
  198. package/orchestrator/planner/plan-builder.cjs +0 -125
  199. package/orchestrator/recovery.cjs +0 -188
  200. package/orchestrator/session-close-journal-worker.cjs +0 -5
  201. package/orchestrator/session-close-journal.cjs +0 -1014
  202. package/orchestrator/session-runner.cjs +0 -1153
  203. package/orchestrator/worktree-manager.cjs +0 -282
  204. package/settings.json +0 -7
  205. package/skills/cc-collab/SKILL.md +0 -42
  206. package/skills/code-review-expert/SKILL.md +0 -43
  207. package/skills/code-review-expert/references/correctness-reliability.md +0 -30
  208. package/skills/code-review-expert/references/design-boundaries.md +0 -26
  209. package/skills/code-review-expert/references/removal-plan.md +0 -24
  210. package/skills/codex-collab/SKILL.md +0 -49
  211. package/skills/codex-collab/evals/evals.json +0 -35
  212. package/skills/integration-rules/SKILL.md +0 -32
  213. package/skills/security-rules/SKILL.md +0 -38
  214. package/skills/testing-rules/SKILL.md +0 -32
  215. package/skills/ultra-doctor/SKILL.md +0 -41
  216. package/skills/ultra-plan/references/semantic-preflight.md +0 -20
  217. package/skills/ultra-verify/SKILL.md +0 -46
  218. package/skills/ultra-verify/evals/evals.json +0 -46
  219. package/skills/ultra-verify/references/cross-verify-modes.md +0 -28
  220. package/skills/ultra-verify/references/evidence-status.md +0 -21
  221. package/skills/ultra-verify/scripts/verify_wait.py +0 -94
  222. package/spec/cli-protocol.md +0 -102
  223. package/spec/mcp-tools.yaml +0 -312
  224. package/spec/schemas/baseline-reconciliation.v1.schema.json +0 -73
  225. package/spec/schemas/change-delta.v1.schema.json +0 -153
  226. package/spec/schemas/command-manifest.schema.json +0 -29
  227. package/spec/schemas/context-file.v4.5.schema.json +0 -54
  228. package/spec/schemas/delivery-report.v1.schema.json +0 -66
  229. package/spec/schemas/documentation-reconciliation.v1.schema.json +0 -106
  230. package/spec/schemas/mcp-tools.schema.json +0 -136
  231. package/spec/schemas/skill-manifest.schema.json +0 -24
  232. package/spec/schemas/state-db.sql +0 -641
  233. package/spec/schemas/task-ledger.v2.schema.json +0 -296
  234. package/spec/schemas/tasks.v4.5.schema.json +0 -96
  235. package/spec/schemas/test-report.v1.schema.json +0 -225
  236. package/ultra-tools/cli.cjs +0 -129
  237. package/ultra-tools/commands/db.cjs +0 -226
  238. package/ultra-tools/commands/legacy-memory.cjs +0 -159
  239. package/ultra-tools/commands/migrate.cjs +0 -1386
  240. package/ultra-tools/commands/session.cjs +0 -213
  241. package/ultra-tools/commands/status.cjs +0 -559
  242. package/ultra-tools/commands/system.cjs +0 -498
  243. package/ultra-tools/commands/task.cjs +0 -105
@@ -1,1193 +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
- const { spawnSync } = require('node:child_process');
7
-
8
- const ops = require('./state-ops.cjs');
9
- const providerRefs = require('./provider-refs.cjs');
10
- const checkpoints = require('./stage-checkpoints.cjs');
11
-
12
- const BASELINE_ID = /^[a-zA-Z0-9_-]+$/;
13
- const MODES = new Set(['greenfield', 'brownfield']);
14
- const SPEC_KINDS = new Set(['discovery', 'product', 'architecture', 'quality', 'delivery', 'other']);
15
- const EVIDENCE_KINDS = new Set(['source', 'docs', 'runtime', 'test', 'deploy', 'external']);
16
- const VERIFICATION_STATUSES = new Set(['pass', 'known_red', 'not_run']);
17
- const GAP_CATEGORIES = new Set([
18
- 'baseline_blocker', 'documentation_drift', 'known_defect',
19
- 'technical_debt', 'unknown', 'future_change',
20
- ]);
21
- const GAP_STATUSES = new Set(['open', 'accepted', 'resolved', 'deferred']);
22
- const BASELINE_START_FIELDS = new Set([
23
- 'id', 'project_name', 'project_type', 'stack', 'mode', 'repository_revision',
24
- 'replace_migrated', 'replace_ready', 'replacement_authorization', 'scope',
25
- 'provider_refs', 'classification',
26
- ]);
27
- const BASELINE_REFRESH_FIELDS = new Set([
28
- 'id', 'project_name', 'project_type', 'stack', 'classification',
29
- ]);
30
- const BASELINE_RECORD_FIELDS = new Set([
31
- 'id', 'repository_revision', 'scope', 'spec_refs', 'evidence', 'verification',
32
- 'unknowns', 'gaps', 'classification', 'provider_refs',
33
- ]);
34
- const BASELINE_CONVERGE_FIELDS = new Set([
35
- 'id', 'expected_revision', 'approved_by', 'approval_note',
36
- 'accept_dirty_worktree', 'accept_known_red',
37
- ]);
38
-
39
- class BaselineWorkflowError extends Error {
40
- constructor(code, message, details) {
41
- super(message);
42
- this.name = 'BaselineWorkflowError';
43
- this.code = code;
44
- if (details !== undefined) this.details = details;
45
- }
46
- }
47
-
48
- function nowIso() {
49
- return new Date().toISOString();
50
- }
51
-
52
- function gitHead(rootDir) {
53
- const result = spawnSync('git', ['rev-parse', 'HEAD'], {
54
- cwd: rootDir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
55
- });
56
- return result.status === 0 ? result.stdout.trim() : null;
57
- }
58
-
59
- function gitRepositoryRoot(rootDir) {
60
- const result = spawnSync('git', ['rev-parse', '--show-toplevel'], {
61
- cwd: rootDir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
62
- });
63
- return result.status === 0 ? result.stdout.trim() : null;
64
- }
65
-
66
- function gitBranch(rootDir) {
67
- let result = spawnSync('git', ['symbolic-ref', '--quiet', '--short', 'HEAD'], {
68
- cwd: rootDir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
69
- });
70
- if (result.status !== 0) {
71
- result = spawnSync('git', ['branch', '--show-current'], {
72
- cwd: rootDir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
73
- });
74
- }
75
- return result.status === 0 ? (result.stdout.trim() || null) : null;
76
- }
77
-
78
- function gitCommitIsAncestor(rootDir, ancestor, descendant) {
79
- if (!ancestor || !descendant) return false;
80
- const result = spawnSync(
81
- 'git', ['merge-base', '--is-ancestor', ancestor, descendant],
82
- { cwd: rootDir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] },
83
- );
84
- return result.status === 0;
85
- }
86
-
87
- function hashScopedWorkingTree(rootDir, relativePaths) {
88
- const root = path.resolve(rootDir);
89
- const digest = crypto.createHash('sha256');
90
- for (const relative of [...new Set(relativePaths)].sort()) {
91
- const file = path.resolve(root, relative);
92
- if (file !== root && !file.startsWith(`${root}${path.sep}`)) {
93
- throw new BaselineWorkflowError(
94
- 'BASELINE_SCOPE_INVALID',
95
- `Git returned a path outside the repository: ${relative}`,
96
- );
97
- }
98
- digest.update(relative).update('\0');
99
- const stat = fs.lstatSync(file, { throwIfNoEntry: false });
100
- if (!stat) {
101
- digest.update('missing\0');
102
- } else if (stat.isSymbolicLink()) {
103
- digest.update('symlink\0').update(fs.readlinkSync(file)).update('\0');
104
- } else if (stat.isFile()) {
105
- digest.update(stat.mode & 0o111 ? 'file+x\0' : 'file\0');
106
- digest.update(fs.readFileSync(file)).update('\0');
107
- } else if (stat.isDirectory()) {
108
- const submoduleHead = spawnSync('git', ['-C', file, 'rev-parse', 'HEAD'], {
109
- cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
110
- });
111
- digest.update('directory\0');
112
- if (submoduleHead.status === 0) digest.update(submoduleHead.stdout.trim());
113
- digest.update('\0');
114
- } else {
115
- digest.update(`special:${stat.mode}\0`);
116
- }
117
- }
118
- return digest.digest('hex');
119
- }
120
-
121
- function gitWorktreeSnapshot(rootDir, scope = ['.']) {
122
- const repositoryRoot = gitRepositoryRoot(rootDir);
123
- if (!repositoryRoot) {
124
- return {
125
- head: null, branch: null, state: 'unavailable', digest: null, files: [],
126
- };
127
- }
128
- const head = gitHead(rootDir);
129
- const selectedScope = Array.isArray(scope) && scope.length > 0 ? scope : ['.'];
130
- const pathspecs = selectedScope.map((item) => (
131
- item === '.' ? '.' : `:(literal)${item}`
132
- ));
133
- const args = ['--', ...pathspecs, ':(exclude).ultra', ':(exclude).ultra/**'];
134
- const status = spawnSync(
135
- 'git', ['status', '--porcelain=v1', '-z', '--untracked-files=all', ...args],
136
- { cwd: rootDir, encoding: 'buffer', stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 32 * 1024 * 1024 },
137
- );
138
- const inventory = spawnSync(
139
- 'git', ['ls-files', '--cached', '--others', '--exclude-standard', '-z',
140
- '--', ...pathspecs, ':(exclude).ultra', ':(exclude).ultra/**'],
141
- { cwd: rootDir, encoding: 'buffer', stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 32 * 1024 * 1024 },
142
- );
143
- if (status.status !== 0 || inventory.status !== 0) {
144
- return {
145
- head, branch: gitBranch(rootDir), state: 'unavailable', digest: null, files: [],
146
- };
147
- }
148
- const statusText = status.stdout.toString('utf8');
149
- const files = statusText.split('\0').filter(Boolean);
150
- const scopedFiles = inventory.stdout.toString('utf8').split('\0').filter(Boolean);
151
- return {
152
- head,
153
- branch: gitBranch(rootDir),
154
- state: head ? (files.length > 0 ? 'dirty' : 'clean') : 'unborn',
155
- digest: hashScopedWorkingTree(rootDir, scopedFiles),
156
- files,
157
- };
158
- }
159
-
160
- function gitWorktreeChanges(rootDir, scope = ['.']) {
161
- const snapshot = gitWorktreeSnapshot(rootDir, scope);
162
- return snapshot.state === 'unavailable' ? null : snapshot.files;
163
- }
164
-
165
- function parseJson(value, field, fallback) {
166
- if (value == null || value === '') return fallback;
167
- try { return JSON.parse(value); }
168
- catch (error) {
169
- throw new BaselineWorkflowError('STATE_CORRUPT', `invalid ${field}: ${error.message}`);
170
- }
171
- }
172
-
173
- function rowToBaseline(row) {
174
- if (!row) return null;
175
- const baseline = {
176
- ...row,
177
- scope: parseJson(row.scope_json, 'scope_json', []),
178
- spec_refs: parseJson(row.spec_refs_json, 'spec_refs_json', []),
179
- evidence: parseJson(row.evidence_json, 'evidence_json', []),
180
- verification: parseJson(row.verification_json, 'verification_json', []),
181
- unknowns: parseJson(row.unknowns_json, 'unknowns_json', []),
182
- gaps: parseJson(row.gaps_json, 'gaps_json', []),
183
- classification: parseJson(row.classification_json, 'classification_json', {}),
184
- worktree_files: parseJson(row.worktree_files_json, 'worktree_files_json', []),
185
- worktree_accepted: Boolean(row.worktree_accepted),
186
- known_red_accepted: Boolean(row.known_red_accepted),
187
- provider_refs: parseJson(row.provider_refs_json, 'provider_refs_json', {}),
188
- };
189
- for (const key of [
190
- 'scope_json', 'spec_refs_json', 'evidence_json', 'verification_json', 'unknowns_json',
191
- 'gaps_json', 'classification_json', 'worktree_files_json', 'provider_refs_json',
192
- ]) delete baseline[key];
193
- return baseline;
194
- }
195
-
196
- function hasTable(db, table) {
197
- return Boolean(db.prepare(
198
- "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?",
199
- ).get(table));
200
- }
201
-
202
- function readBaseline(db, id) {
203
- if (!hasTable(db, 'baselines')) return null;
204
- const row = id
205
- ? db.prepare('SELECT * FROM baselines WHERE id = ?').get(id)
206
- : db.prepare(
207
- "SELECT * FROM baselines WHERE status != 'superseded' ORDER BY updated_at DESC, rowid DESC LIMIT 1",
208
- ).get();
209
- return rowToBaseline(row);
210
- }
211
-
212
- function nonEmpty(value, field) {
213
- const text = typeof value === 'string' ? value.trim() : '';
214
- if (!text) throw new BaselineWorkflowError('VALIDATION_ERROR', `${field} must be a non-empty string`);
215
- return text;
216
- }
217
-
218
- function optionalText(value, field, { nullable = false, emptyAsNull = false } = {}) {
219
- if (value === undefined) return undefined;
220
- if (value === null && nullable) return null;
221
- if (typeof value !== 'string') {
222
- throw new BaselineWorkflowError(
223
- 'VALIDATION_ERROR',
224
- `${field} must be a string${nullable ? ' or null' : ''}`,
225
- );
226
- }
227
- const text = value.trim();
228
- return emptyAsNull && !text ? null : text;
229
- }
230
-
231
- function optionalBoolean(input, field) {
232
- if (Object.hasOwn(input, field) && typeof input[field] !== 'boolean') {
233
- throw new BaselineWorkflowError('VALIDATION_ERROR', `${field} must be a boolean`);
234
- }
235
- }
236
-
237
- function optionalBaselineId(value) {
238
- if (value === undefined) return undefined;
239
- const id = nonEmpty(value, 'id');
240
- if (!BASELINE_ID.test(id)) {
241
- throw new BaselineWorkflowError('VALIDATION_ERROR', 'baseline id is invalid');
242
- }
243
- return id;
244
- }
245
-
246
- function assertExactFields(value, allowed, field) {
247
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
248
- throw new BaselineWorkflowError('VALIDATION_ERROR', `${field} must be an object`);
249
- }
250
- const unknown = Object.keys(value).filter((key) => !allowed.has(key));
251
- if (unknown.length > 0) {
252
- throw new BaselineWorkflowError(
253
- 'VALIDATION_ERROR', `${field}.${unknown[0]} is not allowed`,
254
- );
255
- }
256
- }
257
-
258
- function normalizeReplacementAuthorization(value) {
259
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
260
- throw new BaselineWorkflowError(
261
- 'BASELINE_REPLACEMENT_AUTHORIZATION_REQUIRED',
262
- 'replacing a ready baseline requires approved_by and reason',
263
- );
264
- }
265
- const unknown = Object.keys(value).filter((key) => !['approved_by', 'reason'].includes(key));
266
- if (unknown.length > 0) {
267
- throw new BaselineWorkflowError(
268
- 'VALIDATION_ERROR',
269
- `replacement_authorization.${unknown[0]} is not allowed`,
270
- );
271
- }
272
- let approvedBy;
273
- let reason;
274
- try {
275
- approvedBy = nonEmpty(value.approved_by, 'replacement_authorization.approved_by');
276
- reason = nonEmpty(value.reason, 'replacement_authorization.reason');
277
- } catch {
278
- throw new BaselineWorkflowError(
279
- 'BASELINE_REPLACEMENT_AUTHORIZATION_REQUIRED',
280
- 'replacing a ready baseline requires approved_by and reason',
281
- );
282
- }
283
- if (reason.length < 3) {
284
- throw new BaselineWorkflowError(
285
- 'BASELINE_REPLACEMENT_AUTHORIZATION_REQUIRED',
286
- 'replacement_authorization.reason must explain the owner-approved replacement',
287
- );
288
- }
289
- return { approved_by: approvedBy, reason };
290
- }
291
-
292
- function safeRelativePath(rootDir, candidate, field = 'path') {
293
- const value = nonEmpty(candidate, field);
294
- if (path.isAbsolute(value)) {
295
- throw new BaselineWorkflowError('VALIDATION_ERROR', `${field} must be project-relative: ${value}`);
296
- }
297
- const root = path.resolve(rootDir);
298
- const resolved = path.resolve(root, value);
299
- if (resolved !== root && !resolved.startsWith(`${root}${path.sep}`)) {
300
- throw new BaselineWorkflowError('VALIDATION_ERROR', `${field} escapes project root: ${value}`);
301
- }
302
- return resolved;
303
- }
304
-
305
- function normalizeScope(value, rootDir) {
306
- const scope = value === undefined ? ['.'] : value;
307
- if (!Array.isArray(scope) || scope.length === 0) {
308
- throw new BaselineWorkflowError('VALIDATION_ERROR', 'scope must be a non-empty array');
309
- }
310
- return [...new Set(scope.map((item, index) => {
311
- const relative = nonEmpty(item, `scope[${index}]`);
312
- const resolved = safeRelativePath(rootDir, item, `scope[${index}]`);
313
- if (!fs.existsSync(resolved)) {
314
- throw new BaselineWorkflowError(
315
- 'BASELINE_SCOPE_MISSING', `baseline scope does not exist: ${relative}`,
316
- );
317
- }
318
- return relative;
319
- }))];
320
- }
321
-
322
- function storedScopeBlockers(baseline, rootDir) {
323
- const blockers = [];
324
- for (const item of baseline.scope) {
325
- let target;
326
- try { target = safeRelativePath(rootDir, item, 'scope path'); }
327
- catch { blockers.push(`BASELINE_SCOPE_INVALID:${item}`); continue; }
328
- if (!fs.existsSync(target)) blockers.push(`BASELINE_SCOPE_MISSING:${item}`);
329
- }
330
- return blockers;
331
- }
332
-
333
- function digestFile(file) {
334
- return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
335
- }
336
-
337
- function workspaceRevision({ scope, specs, evidence }, rootDir) {
338
- const evidenceRefs = evidence.map((item) => {
339
- try {
340
- const file = safeRelativePath(rootDir, item.ref, 'evidence ref');
341
- return fs.existsSync(file) && fs.statSync(file).isFile()
342
- ? { kind: item.kind, ref: item.ref, digest: digestFile(file) }
343
- : { kind: item.kind, ref: item.ref };
344
- } catch {
345
- return { kind: item.kind, ref: item.ref };
346
- }
347
- });
348
- const payload = JSON.stringify({ scope, specs, evidence: evidenceRefs });
349
- return `workspace:${crypto.createHash('sha256').update(payload).digest('hex')}`;
350
- }
351
-
352
- function normalizeSpecRefs(value, rootDir) {
353
- if (!Array.isArray(value)) throw new BaselineWorkflowError('VALIDATION_ERROR', 'spec_refs must be an array');
354
- return value.map((item, index) => {
355
- if (!item || typeof item !== 'object' || Array.isArray(item) || !SPEC_KINDS.has(item.kind)) {
356
- throw new BaselineWorkflowError('VALIDATION_ERROR', `invalid spec_refs[${index}]`);
357
- }
358
- assertExactFields(
359
- item,
360
- new Set(['kind', 'path', 'digest']),
361
- `spec_refs[${index}]`,
362
- );
363
- const specPath = nonEmpty(item.path, `spec_refs[${index}].path`);
364
- const file = safeRelativePath(rootDir, specPath, `spec_refs[${index}].path`);
365
- if (!fs.existsSync(file) || !fs.statSync(file).isFile()) {
366
- throw new BaselineWorkflowError('BASELINE_SPEC_MISSING', `baseline specification missing: ${specPath}`);
367
- }
368
- return { kind: item.kind, path: specPath, digest: digestFile(file) };
369
- });
370
- }
371
-
372
- function normalizeEvidence(value, rootDir) {
373
- if (!Array.isArray(value)) throw new BaselineWorkflowError('VALIDATION_ERROR', 'evidence must be an array');
374
- return value.map((item, index) => {
375
- if (!item || typeof item !== 'object' || Array.isArray(item) || !EVIDENCE_KINDS.has(item.kind)) {
376
- throw new BaselineWorkflowError('VALIDATION_ERROR', `invalid evidence[${index}]`);
377
- }
378
- assertExactFields(
379
- item,
380
- new Set(['kind', 'ref', 'summary', 'digest']),
381
- `evidence[${index}]`,
382
- );
383
- const normalized = {
384
- kind: item.kind,
385
- ref: nonEmpty(item.ref, `evidence[${index}].ref`),
386
- summary: nonEmpty(item.summary, `evidence[${index}].summary`),
387
- };
388
- if (item.kind === 'source') {
389
- const file = safeRelativePath(rootDir, normalized.ref, `evidence[${index}].ref`);
390
- if (!fs.existsSync(file) || !fs.statSync(file).isFile()) {
391
- throw new BaselineWorkflowError(
392
- 'BASELINE_EVIDENCE_MISSING', `baseline source evidence missing: ${normalized.ref}`,
393
- );
394
- }
395
- normalized.digest = digestFile(file);
396
- }
397
- return normalized;
398
- });
399
- }
400
-
401
- function normalizeVerification(value) {
402
- if (!Array.isArray(value)) throw new BaselineWorkflowError('VALIDATION_ERROR', 'verification must be an array');
403
- return value.map((item, index) => {
404
- if (!item || typeof item !== 'object' || Array.isArray(item) || !VERIFICATION_STATUSES.has(item.status)) {
405
- throw new BaselineWorkflowError('VALIDATION_ERROR', `invalid verification[${index}]`);
406
- }
407
- assertExactFields(
408
- item,
409
- new Set(['name', 'command', 'status', 'evidence', 'rationale']),
410
- `verification[${index}]`,
411
- );
412
- const normalized = {
413
- name: nonEmpty(item.name, `verification[${index}].name`),
414
- command: nonEmpty(item.command, `verification[${index}].command`),
415
- status: item.status,
416
- evidence: nonEmpty(item.evidence, `verification[${index}].evidence`),
417
- };
418
- if (item.rationale !== undefined) normalized.rationale = nonEmpty(item.rationale, `verification[${index}].rationale`);
419
- return normalized;
420
- });
421
- }
422
-
423
- function normalizeUnknowns(value) {
424
- if (!Array.isArray(value)) throw new BaselineWorkflowError('VALIDATION_ERROR', 'unknowns must be an array');
425
- return value.map((item, index) => {
426
- if (!item || typeof item !== 'object' || Array.isArray(item)) {
427
- throw new BaselineWorkflowError('VALIDATION_ERROR', `invalid unknowns[${index}]`);
428
- }
429
- assertExactFields(
430
- item,
431
- new Set(['summary', 'blocking', 'owner']),
432
- `unknowns[${index}]`,
433
- );
434
- if (item.blocking !== undefined && typeof item.blocking !== 'boolean') {
435
- throw new BaselineWorkflowError(
436
- 'VALIDATION_ERROR', `unknowns[${index}].blocking must be a boolean`,
437
- );
438
- }
439
- const normalized = {
440
- summary: nonEmpty(item.summary, `unknowns[${index}].summary`),
441
- blocking: item.blocking === true,
442
- };
443
- if (item.owner !== undefined) normalized.owner = nonEmpty(item.owner, `unknowns[${index}].owner`);
444
- return normalized;
445
- });
446
- }
447
-
448
- function normalizeGaps(value) {
449
- if (!Array.isArray(value)) throw new BaselineWorkflowError('VALIDATION_ERROR', 'gaps must be an array');
450
- const ids = new Set();
451
- return value.map((item, index) => {
452
- if (!item || typeof item !== 'object' || Array.isArray(item)) {
453
- throw new BaselineWorkflowError('VALIDATION_ERROR', `invalid gaps[${index}]`);
454
- }
455
- assertExactFields(
456
- item,
457
- new Set([
458
- 'id', 'category', 'status', 'blocking', 'summary',
459
- 'evidence_refs', 'owner', 'resolution',
460
- ]),
461
- `gaps[${index}]`,
462
- );
463
- const id = nonEmpty(item.id, `gaps[${index}].id`);
464
- if (!BASELINE_ID.test(id) || ids.has(id)) {
465
- throw new BaselineWorkflowError('VALIDATION_ERROR', `invalid or duplicate gap id: ${id}`);
466
- }
467
- ids.add(id);
468
- if (!GAP_CATEGORIES.has(item.category)) {
469
- throw new BaselineWorkflowError('VALIDATION_ERROR', `invalid gap category: ${item.category}`);
470
- }
471
- if (item.blocking !== undefined && typeof item.blocking !== 'boolean') {
472
- throw new BaselineWorkflowError(
473
- 'VALIDATION_ERROR', `gaps[${index}].blocking must be a boolean`,
474
- );
475
- }
476
- const status = item.status === undefined ? 'open' : item.status;
477
- if (!GAP_STATUSES.has(status)) {
478
- throw new BaselineWorkflowError('VALIDATION_ERROR', `invalid gap status: ${status}`);
479
- }
480
- const evidenceRefs = item.evidence_refs === undefined ? [] : item.evidence_refs;
481
- if (!Array.isArray(evidenceRefs) || evidenceRefs.some((ref) => typeof ref !== 'string' || !ref.trim())) {
482
- throw new BaselineWorkflowError('VALIDATION_ERROR', `gaps[${index}].evidence_refs must be strings`);
483
- }
484
- const normalized = {
485
- id,
486
- category: item.category,
487
- status,
488
- blocking: item.category === 'baseline_blocker' ? true : item.blocking === true,
489
- summary: nonEmpty(item.summary, `gaps[${index}].summary`),
490
- evidence_refs: [...new Set(evidenceRefs.map((ref) => ref.trim()))],
491
- owner: item.owner == null ? null : nonEmpty(item.owner, `gaps[${index}].owner`),
492
- };
493
- if (item.resolution != null) {
494
- normalized.resolution = nonEmpty(item.resolution, `gaps[${index}].resolution`);
495
- }
496
- return normalized;
497
- });
498
- }
499
-
500
- function normalizeClassification(value) {
501
- if (value === undefined || value === null) return {};
502
- if (typeof value !== 'object' || Array.isArray(value)) {
503
- throw new BaselineWorkflowError('VALIDATION_ERROR', 'classification must be an object');
504
- }
505
- const serialized = JSON.stringify(value);
506
- if (Buffer.byteLength(serialized) > 32 * 1024) {
507
- throw new BaselineWorkflowError('VALIDATION_ERROR', 'classification exceeds 32 KiB');
508
- }
509
- return JSON.parse(serialized);
510
- }
511
-
512
- function summarizeGaps(gaps = []) {
513
- const summary = { total: gaps.length, open: 0, blocking: 0, by_category: {} };
514
- for (const gap of gaps) {
515
- summary.by_category[gap.category] = (summary.by_category[gap.category] || 0) + 1;
516
- if (gap.status === 'open') summary.open += 1;
517
- if (gap.status === 'open' && gap.blocking) summary.blocking += 1;
518
- }
519
- return summary;
520
- }
521
-
522
- function startBaseline(db, input = {}, { rootDir = process.cwd(), emitEvent = true } = {}) {
523
- assertExactFields(input, BASELINE_START_FIELDS, 'baseline.start');
524
- const id = optionalBaselineId(input.id) || 'project-baseline';
525
- const projectName = nonEmpty(input.project_name, 'project_name');
526
- const projectType = optionalText(
527
- input.project_type, 'project_type', { nullable: true, emptyAsNull: true },
528
- ) ?? null;
529
- const stack = optionalText(input.stack, 'stack', { nullable: true, emptyAsNull: true }) ?? null;
530
- const repositoryRevision = optionalText(
531
- input.repository_revision,
532
- 'repository_revision',
533
- { nullable: true, emptyAsNull: true },
534
- ) ?? null;
535
- optionalBoolean(input, 'replace_migrated');
536
- optionalBoolean(input, 'replace_ready');
537
- if (!MODES.has(input.mode)) throw new BaselineWorkflowError('VALIDATION_ERROR', `invalid baseline mode: ${input.mode}`);
538
- if (readBaseline(db, id)) throw new BaselineWorkflowError('DUPLICATE_BASELINE_ID', `baseline ${id} exists`);
539
- const current = readBaseline(db);
540
- if (input.replace_migrated === true
541
- && (!current || current.mode !== 'migrated' || input.mode !== 'brownfield')) {
542
- throw new BaselineWorkflowError(
543
- 'BASELINE_REPLACEMENT_INVALID',
544
- 'replace_migrated requires a current migrated baseline and brownfield mode',
545
- );
546
- }
547
- const replacingMigrated = Boolean(
548
- current && current.mode === 'migrated' && current.status !== 'superseded'
549
- && input.replace_migrated === true,
550
- );
551
- let replacementAuthorization = null;
552
- if (input.replace_ready === true) {
553
- if (!current || current.status !== 'ready' || replacingMigrated) {
554
- throw new BaselineWorkflowError(
555
- 'BASELINE_REPLACEMENT_INVALID',
556
- 'replace_ready requires a current ready baseline',
557
- );
558
- }
559
- replacementAuthorization = normalizeReplacementAuthorization(input.replacement_authorization);
560
- } else if (input.replacement_authorization !== undefined) {
561
- throw new BaselineWorkflowError(
562
- 'BASELINE_REPLACEMENT_INVALID',
563
- 'replacement_authorization is valid only with replace_ready=true',
564
- );
565
- }
566
- if (current && current.status !== 'superseded') {
567
- if (replacingMigrated) {
568
- // Compatibility state is never an accepted product baseline. Explicit
569
- // replacement preserves its audit row while starting real adoption.
570
- } else if (current.status === 'ready' && input.replace_ready !== true) {
571
- throw new BaselineWorkflowError('BASELINE_EXISTS', `ready baseline ${current.id} requires replace_ready=true`);
572
- } else if (current.status !== 'ready') {
573
- throw new BaselineWorkflowError('BASELINE_IN_PROGRESS', `baseline ${current.id} is ${current.status}`);
574
- }
575
- }
576
- const scope = normalizeScope(input.scope, rootDir);
577
- const providers = providerRefs.normalizeProviderRefs(input.provider_refs, BaselineWorkflowError);
578
- const classification = normalizeClassification(input.classification);
579
- const snapshot = gitWorktreeSnapshot(rootDir, scope);
580
- if (snapshot.head && repositoryRevision && repositoryRevision !== snapshot.head) {
581
- throw new BaselineWorkflowError(
582
- 'BASELINE_REVISION_MISMATCH', 'repository_revision does not match current Git HEAD',
583
- );
584
- }
585
- const status = input.mode === 'brownfield' ? 'adopting' : 'draft';
586
- const ts = nowIso();
587
- return ops.tx(db, () => {
588
- if (current && (current.status === 'ready' || replacingMigrated)) {
589
- db.prepare("UPDATE baselines SET status = 'superseded', updated_at = ? WHERE id = ?")
590
- .run(ts, current.id);
591
- }
592
- db.prepare(
593
- `INSERT INTO baselines
594
- (id, project_name, project_type, stack, mode, status, repository_root, scope_json,
595
- repository_revision, repository_branch, worktree_state, worktree_digest,
596
- worktree_files_json, classification_json, provider_refs_json, started_at, updated_at)
597
- VALUES (?, ?, ?, ?, ?, ?, '.', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
598
- ).run(
599
- id, projectName, projectType, stack, input.mode, status,
600
- JSON.stringify(scope), snapshot.head || repositoryRevision, snapshot.branch,
601
- snapshot.state, snapshot.digest, JSON.stringify(snapshot.files), JSON.stringify(classification),
602
- JSON.stringify(providers), ts, ts,
603
- );
604
- if (emitEvent) {
605
- const payload = {
606
- baseline_id: id,
607
- mode: input.mode,
608
- replaced_baseline_id: current?.id || null,
609
- };
610
- if (replacementAuthorization) {
611
- payload.replacement_authorization = { ...replacementAuthorization, recorded_at: ts };
612
- }
613
- ops.appendEventInTx(db, {
614
- type: 'baseline_started',
615
- payload,
616
- });
617
- }
618
- return readBaseline(db, id);
619
- });
620
- }
621
-
622
- function refreshInProgressBaseline(db, input = {}, {
623
- rootDir = process.cwd(), emitEvent = true,
624
- } = {}) {
625
- assertExactFields(input, BASELINE_REFRESH_FIELDS, 'baseline.refresh');
626
- const id = optionalBaselineId(input.id);
627
- const current = readBaseline(db, id);
628
- if (!current) {
629
- throw new BaselineWorkflowError('BASELINE_NOT_FOUND', `baseline ${input.id || '(current)'} not found`);
630
- }
631
- if (!['draft', 'adopting', 'blocked'].includes(current.status)) return current;
632
- const projectName = input.project_name === undefined
633
- ? current.project_name
634
- : nonEmpty(input.project_name, 'project_name');
635
- const projectType = input.project_type === undefined
636
- ? current.project_type
637
- : optionalText(input.project_type, 'project_type', { nullable: true, emptyAsNull: true });
638
- const stack = input.stack === undefined
639
- ? current.stack
640
- : optionalText(input.stack, 'stack', { nullable: true, emptyAsNull: true });
641
- const classification = input.classification === undefined
642
- ? current.classification
643
- : normalizeClassification(input.classification);
644
- const snapshot = gitWorktreeSnapshot(rootDir, current.scope);
645
- const repositoryRevision = snapshot.head
646
- || (snapshot.state === 'unborn' ? null : current.repository_revision);
647
- const ts = nowIso();
648
- return ops.tx(db, () => {
649
- db.prepare(
650
- `UPDATE baselines SET project_name = ?, project_type = ?, stack = ?,
651
- repository_revision = ?, repository_branch = ?, worktree_state = ?,
652
- worktree_digest = ?, worktree_files_json = ?, classification_json = ?,
653
- updated_at = ? WHERE id = ?`,
654
- ).run(
655
- projectName, projectType, stack,
656
- repositoryRevision, snapshot.branch, snapshot.state, snapshot.digest,
657
- JSON.stringify(snapshot.files), JSON.stringify(classification), ts, current.id,
658
- );
659
- if (emitEvent) {
660
- ops.appendEventInTx(db, {
661
- type: 'baseline_metadata_refreshed',
662
- payload: {
663
- baseline_id: current.id,
664
- project_name: projectName,
665
- project_type: projectType,
666
- stack,
667
- repository_revision: repositoryRevision,
668
- repository_branch: snapshot.branch,
669
- worktree_state: snapshot.state,
670
- },
671
- });
672
- }
673
- return readBaseline(db, current.id);
674
- });
675
- }
676
-
677
- function recordBaseline(db, input = {}, { rootDir = process.cwd(), emitEvent = true } = {}) {
678
- assertExactFields(input, BASELINE_RECORD_FIELDS, 'baseline.record');
679
- const id = optionalBaselineId(input.id);
680
- const suppliedRevision = optionalText(
681
- input.repository_revision,
682
- 'repository_revision',
683
- { nullable: true, emptyAsNull: true },
684
- );
685
- const current = readBaseline(db, id);
686
- if (!current) throw new BaselineWorkflowError('BASELINE_NOT_FOUND', `baseline ${input.id} not found`);
687
- if (['ready', 'superseded'].includes(current.status)) {
688
- throw new BaselineWorkflowError('BASELINE_NOT_MUTABLE', `baseline ${current.id} is ${current.status}`);
689
- }
690
- const scope = normalizeScope(input.scope === undefined ? current.scope : input.scope, rootDir);
691
- const specs = input.spec_refs === undefined ? current.spec_refs : normalizeSpecRefs(input.spec_refs, rootDir);
692
- const evidence = input.evidence === undefined
693
- ? current.evidence : normalizeEvidence(input.evidence, rootDir);
694
- const verification = input.verification === undefined
695
- ? current.verification : normalizeVerification(input.verification);
696
- const unknowns = input.unknowns === undefined ? current.unknowns : normalizeUnknowns(input.unknowns);
697
- const gaps = input.gaps === undefined ? current.gaps : normalizeGaps(input.gaps);
698
- const classification = input.classification === undefined
699
- ? current.classification : normalizeClassification(input.classification);
700
- const providers = input.provider_refs === undefined
701
- ? current.provider_refs
702
- : providerRefs.normalizeProviderRefs(input.provider_refs, BaselineWorkflowError);
703
- const snapshot = gitWorktreeSnapshot(rootDir, scope);
704
- if (snapshot.state === 'unborn') {
705
- throw new BaselineWorkflowError(
706
- 'BASELINE_GIT_HEAD_REQUIRED',
707
- 'Git is initialized but has no commit; create an owner-authorized local checkpoint commit before recording the baseline',
708
- );
709
- }
710
- if (snapshot.head && suppliedRevision && suppliedRevision !== snapshot.head) {
711
- throw new BaselineWorkflowError(
712
- 'BASELINE_REVISION_MISMATCH', 'repository_revision does not match current Git HEAD',
713
- );
714
- }
715
- const revision = snapshot.head || (suppliedRevision === undefined
716
- ? (current.repository_revision || workspaceRevision({ scope, specs, evidence }, rootDir))
717
- : (suppliedRevision || workspaceRevision({ scope, specs, evidence }, rootDir)));
718
- const status = current.mode === 'brownfield' ? 'adopting' : 'draft';
719
- const ts = nowIso();
720
- return ops.tx(db, () => {
721
- db.prepare(
722
- `UPDATE baselines SET status = ?, scope_json = ?, repository_revision = ?,
723
- repository_branch = ?, worktree_state = ?, worktree_digest = ?, worktree_files_json = ?,
724
- worktree_accepted = 0, known_red_accepted = 0,
725
- spec_refs_json = ?, evidence_json = ?, verification_json = ?,
726
- unknowns_json = ?, gaps_json = ?, classification_json = ?, provider_refs_json = ?,
727
- approved_by = NULL, approval_note = NULL, converged_at = NULL, updated_at = ? WHERE id = ?`,
728
- ).run(
729
- status, JSON.stringify(scope), revision, snapshot.branch, snapshot.state, snapshot.digest,
730
- JSON.stringify(snapshot.files), JSON.stringify(specs), JSON.stringify(evidence),
731
- JSON.stringify(verification), JSON.stringify(unknowns), JSON.stringify(gaps),
732
- JSON.stringify(classification), JSON.stringify(providers), ts, current.id,
733
- );
734
- if (emitEvent) {
735
- ops.appendEventInTx(db, {
736
- type: 'baseline_recorded',
737
- payload: {
738
- baseline_id: current.id, spec_count: specs.length,
739
- gap_count: gaps.length, worktree_state: snapshot.state,
740
- },
741
- });
742
- }
743
- return readBaseline(db, current.id);
744
- });
745
- }
746
-
747
- function storedSpecBlockers(baseline, rootDir) {
748
- const blockers = [];
749
- for (const spec of baseline.spec_refs) {
750
- let file;
751
- try { file = safeRelativePath(rootDir, spec.path, 'spec path'); }
752
- catch { blockers.push(`BASELINE_SPEC_INVALID:${spec.path}`); continue; }
753
- if (!fs.existsSync(file) || !fs.statSync(file).isFile()) {
754
- blockers.push(`BASELINE_SPEC_MISSING:${spec.path}`);
755
- } else if (!spec.digest || digestFile(file) !== spec.digest) {
756
- blockers.push(`BASELINE_SPEC_STALE:${spec.path}`);
757
- }
758
- }
759
- return blockers;
760
- }
761
-
762
- function storedEvidenceBlockers(baseline, rootDir) {
763
- const blockers = [];
764
- for (const item of baseline.evidence) {
765
- if (item.kind !== 'source' && !item.digest) continue;
766
- let file;
767
- try { file = safeRelativePath(rootDir, item.ref, 'evidence ref'); }
768
- catch { blockers.push(`BASELINE_EVIDENCE_INVALID:${item.ref}`); continue; }
769
- if (!fs.existsSync(file) || !fs.statSync(file).isFile()) {
770
- blockers.push(`BASELINE_EVIDENCE_MISSING:${item.ref}`);
771
- } else if (!item.digest) {
772
- blockers.push(`BASELINE_EVIDENCE_UNVERIFIED:${item.ref}`);
773
- } else if (digestFile(file) !== item.digest) {
774
- blockers.push(`BASELINE_EVIDENCE_STALE:${item.ref}`);
775
- }
776
- }
777
- return blockers;
778
- }
779
-
780
- function storedReadyInvariantBlockers(baseline) {
781
- const blockers = [];
782
- const arrays = ['scope', 'spec_refs', 'evidence', 'verification', 'unknowns', 'gaps'];
783
- for (const field of arrays) {
784
- if (!Array.isArray(baseline[field])) blockers.push(`BASELINE_STATE_INVALID:${field}`);
785
- }
786
- if (!Array.isArray(baseline.scope) || baseline.scope.length === 0) {
787
- blockers.push('BASELINE_SCOPE_MISSING');
788
- }
789
- if (Array.isArray(baseline.verification)) {
790
- for (const item of baseline.verification) {
791
- if (!item || typeof item !== 'object') {
792
- blockers.push('BASELINE_STATE_INVALID:verification');
793
- } else if (item.status === 'known_red') {
794
- if (!String(item.rationale || '').trim()) {
795
- blockers.push(`BASELINE_KNOWN_RED_RATIONALE_MISSING:${item.name || 'unknown'}`);
796
- }
797
- if (!baseline.known_red_accepted) {
798
- blockers.push(`BASELINE_KNOWN_RED_NOT_ACCEPTED:${item.name || 'unknown'}`);
799
- }
800
- } else if (!['pass', 'not_run'].includes(item.status)) {
801
- blockers.push(`BASELINE_VERIFICATION_STATUS_INVALID:${item.name || 'unknown'}`);
802
- }
803
- }
804
- }
805
- if (!String(baseline.repository_revision || '').trim()) {
806
- blockers.push('BASELINE_REVISION_MISSING');
807
- }
808
- if (!String(baseline.approved_by || '').trim()) blockers.push('BASELINE_APPROVER_MISSING');
809
- if (!String(baseline.approval_note || '').trim()) {
810
- blockers.push('BASELINE_APPROVAL_NOTE_MISSING');
811
- }
812
- if (!baseline.converged_at || Number.isNaN(Date.parse(baseline.converged_at))) {
813
- blockers.push('BASELINE_CONVERGENCE_TIMESTAMP_MISSING');
814
- }
815
- return [...new Set(blockers)];
816
- }
817
-
818
- function storedReadySemanticWarnings(baseline) {
819
- const warnings = [];
820
- if (Array.isArray(baseline.spec_refs)) {
821
- for (const kind of ['discovery', 'product', 'architecture']) {
822
- if (!baseline.spec_refs.some((item) => item?.kind === kind)) {
823
- warnings.push(`BASELINE_SPEC_MISSING:${kind}`);
824
- }
825
- }
826
- }
827
- if (Array.isArray(baseline.evidence) && baseline.evidence.length === 0) {
828
- warnings.push('BASELINE_EVIDENCE_MISSING');
829
- }
830
- if (baseline.mode === 'brownfield'
831
- && Array.isArray(baseline.evidence)
832
- && !baseline.evidence.some((item) => item?.kind === 'source')) {
833
- warnings.push('BASELINE_SOURCE_EVIDENCE_MISSING');
834
- }
835
- if (Array.isArray(baseline.verification) && baseline.verification.length === 0) {
836
- warnings.push('BASELINE_VERIFICATION_MISSING');
837
- } else if (Array.isArray(baseline.verification)) {
838
- for (const item of baseline.verification) {
839
- if (item?.status === 'not_run') {
840
- warnings.push(`BASELINE_VERIFICATION_NOT_RUN:${item.name || 'unknown'}`);
841
- }
842
- }
843
- }
844
- if (Array.isArray(baseline.unknowns)) {
845
- for (const item of baseline.unknowns.filter((unknown) => unknown?.blocking === true)) {
846
- warnings.push(`BASELINE_UNKNOWN_RECORDED:${item.summary || 'unknown'}`);
847
- }
848
- }
849
- if (Array.isArray(baseline.gaps)) {
850
- for (const gap of baseline.gaps.filter((item) => (
851
- item?.status === 'open' && item?.blocking === true
852
- ))) {
853
- warnings.push(`BASELINE_GAP_RECORDED:${gap.id || 'unknown'}`);
854
- }
855
- }
856
- return [...new Set(warnings)];
857
- }
858
-
859
- function acceptedBaselineResearch(db, baseline) {
860
- return checkpoints.currentCheckpoint(
861
- db,
862
- 'research',
863
- { baseline_id: baseline.id },
864
- { includeDraft: false },
865
- );
866
- }
867
-
868
- function convergenceResearchBlockers(db, baseline) {
869
- const checkpoint = acceptedBaselineResearch(db, baseline);
870
- if (!checkpoint) return ['BASELINE_RESEARCH_INCOMPLETE'];
871
- return [];
872
- }
873
-
874
- function storedResearchHealth(db, baseline) {
875
- const checkpointId = baseline.research_checkpoint_id || baseline.research_run_id;
876
- if (!checkpointId) {
877
- return { blockers: [], warnings: ['BASELINE_RESEARCH_PROVENANCE_MISSING'] };
878
- }
879
- let checkpoint;
880
- try {
881
- checkpoint = checkpoints.readCheckpoint(db, checkpointId);
882
- } catch (error) {
883
- if (error?.code === 'CHECKPOINT_DIGEST_MISMATCH') {
884
- return { blockers: ['BASELINE_RESEARCH_RECORD_INVALID'], warnings: [] };
885
- }
886
- throw error;
887
- }
888
- if (!checkpoint
889
- || checkpoint.stage !== 'research'
890
- || checkpoint.scope_type !== 'baseline'
891
- || checkpoint.scope_id !== baseline.id
892
- || checkpoints.checkpointDigest(checkpoint) !== checkpoint.digest) {
893
- return { blockers: ['BASELINE_RESEARCH_RECORD_INVALID'], warnings: [] };
894
- }
895
- if (checkpoint.status !== 'accepted') {
896
- return {
897
- blockers: [],
898
- warnings: [
899
- checkpoint.status === 'superseded'
900
- ? 'BASELINE_RESEARCH_SUPERSEDED'
901
- : `BASELINE_RESEARCH_NOT_ACCEPTED:${checkpoint.status}`,
902
- ],
903
- };
904
- }
905
- return { blockers: [], warnings: checkpoint.diagnostics || [] };
906
- }
907
-
908
- function convergenceBlockers(db, baseline, input, rootDir) {
909
- const blockers = new Set();
910
- const expected = typeof input.expected_revision === 'string' ? input.expected_revision.trim() : '';
911
- if (!expected) blockers.add('BASELINE_REVISION_REQUIRED');
912
- else if (baseline.repository_revision !== expected) blockers.add('BASELINE_REVISION_MISMATCH');
913
- const snapshot = gitWorktreeSnapshot(rootDir, baseline.scope);
914
- if (snapshot.state === 'unborn') blockers.add('BASELINE_GIT_HEAD_REQUIRED');
915
- if (snapshot.head && expected && snapshot.head !== expected
916
- && (!gitCommitIsAncestor(rootDir, expected, snapshot.head)
917
- || (baseline.worktree_digest && snapshot.digest
918
- && baseline.worktree_digest !== snapshot.digest))) {
919
- blockers.add('BASELINE_HEAD_STALE');
920
- }
921
- if (baseline.repository_branch && snapshot.branch
922
- && baseline.repository_branch !== snapshot.branch) blockers.add('BASELINE_BRANCH_STALE');
923
- if (!Array.isArray(baseline.scope) || baseline.scope.length === 0) blockers.add('BASELINE_SCOPE_MISSING');
924
- for (const blocker of storedSpecBlockers(baseline, rootDir)) blockers.add(blocker);
925
- for (const blocker of storedEvidenceBlockers(baseline, rootDir)) blockers.add(blocker);
926
- for (const blocker of storedScopeBlockers(baseline, rootDir)) blockers.add(blocker);
927
- if (baseline.worktree_digest && snapshot.digest && baseline.worktree_digest !== snapshot.digest) {
928
- blockers.add('BASELINE_WORKTREE_STALE');
929
- if (snapshot.state === 'dirty') blockers.add('BASELINE_WORKTREE_DIRTY');
930
- }
931
- if (baseline.worktree_state === 'dirty' && input.accept_dirty_worktree !== true) {
932
- blockers.add('BASELINE_DIRTY_WORKTREE_NOT_ACCEPTED');
933
- }
934
- for (const item of baseline.verification) {
935
- if (item.status === 'known_red') {
936
- if (input.accept_known_red !== true) blockers.add(`BASELINE_KNOWN_RED_NOT_ACCEPTED:${item.name}`);
937
- if (!item.rationale) blockers.add(`BASELINE_KNOWN_RED_RATIONALE_MISSING:${item.name}`);
938
- }
939
- }
940
- if (!String(input.approved_by || '').trim()) blockers.add('BASELINE_APPROVER_REQUIRED');
941
- if (!String(input.approval_note || '').trim()) blockers.add('BASELINE_APPROVAL_NOTE_REQUIRED');
942
- return [...blockers].sort();
943
- }
944
-
945
- function convergenceWarnings(db, baseline) {
946
- const warnings = new Set(storedReadySemanticWarnings(baseline));
947
- for (const warning of convergenceResearchBlockers(db, baseline)) warnings.add(warning);
948
- return [...warnings].sort();
949
- }
950
-
951
- function convergeBaseline(db, input = {}, { rootDir = process.cwd(), emitEvent = true } = {}) {
952
- assertExactFields(input, BASELINE_CONVERGE_FIELDS, 'baseline.converge');
953
- const normalizedInput = {
954
- ...input,
955
- id: optionalBaselineId(input.id),
956
- expected_revision: optionalText(input.expected_revision, 'expected_revision'),
957
- approved_by: optionalText(input.approved_by, 'approved_by'),
958
- approval_note: optionalText(input.approval_note, 'approval_note'),
959
- };
960
- optionalBoolean(input, 'accept_dirty_worktree');
961
- optionalBoolean(input, 'accept_known_red');
962
- const current = readBaseline(db, normalizedInput.id);
963
- if (!current) throw new BaselineWorkflowError('BASELINE_NOT_FOUND', `baseline ${input.id} not found`);
964
- if (!['draft', 'adopting', 'blocked'].includes(current.status)) {
965
- throw new BaselineWorkflowError('BASELINE_NOT_CONVERGEABLE', `baseline ${current.id} is ${current.status}`);
966
- }
967
- const blockers = convergenceBlockers(db, current, normalizedInput, rootDir);
968
- const warnings = convergenceWarnings(db, current);
969
- const ts = nowIso();
970
- if (blockers.length > 0) {
971
- ops.tx(db, () => {
972
- db.prepare("UPDATE baselines SET status = 'blocked', updated_at = ? WHERE id = ?")
973
- .run(ts, current.id);
974
- if (emitEvent) {
975
- ops.appendEventInTx(db, {
976
- type: 'baseline_blocked', payload: { baseline_id: current.id, blockers },
977
- });
978
- }
979
- });
980
- return {
981
- ready: false,
982
- status: 'blocked',
983
- blockers,
984
- warnings,
985
- baseline: readBaseline(db, current.id),
986
- };
987
- }
988
- const research = acceptedBaselineResearch(db, current);
989
- ops.tx(db, () => {
990
- db.prepare(
991
- `UPDATE baselines SET status = 'ready', approved_by = ?, approval_note = ?,
992
- worktree_accepted = ?, known_red_accepted = ?, research_checkpoint_id = ?,
993
- research_run_id = NULL,
994
- converged_at = ?, updated_at = ? WHERE id = ?`,
995
- ).run(
996
- normalizedInput.approved_by, normalizedInput.approval_note,
997
- normalizedInput.accept_dirty_worktree === true ? 1 : 0,
998
- normalizedInput.accept_known_red === true ? 1 : 0,
999
- research?.id || null, ts, ts, current.id,
1000
- );
1001
- if (emitEvent) {
1002
- ops.appendEventInTx(db, {
1003
- type: 'baseline_converged', payload: { baseline_id: current.id, revision: current.repository_revision },
1004
- });
1005
- }
1006
- });
1007
- return {
1008
- ready: true,
1009
- status: 'ready',
1010
- blockers: [],
1011
- warnings,
1012
- baseline: readBaseline(db, current.id),
1013
- };
1014
- }
1015
-
1016
- function inspectBaseline(db, { rootDir = process.cwd(), id } = {}) {
1017
- if (!hasTable(db, 'baselines')) {
1018
- return {
1019
- status: 'fail', blockers: ['BASELINE_SCHEMA_MIGRATION_REQUIRED'], warnings: [], baseline: null,
1020
- };
1021
- }
1022
- const baseline = readBaseline(db, id);
1023
- if (!baseline) {
1024
- return { status: 'fail', blockers: ['BASELINE_MISSING'], warnings: [], baseline: null };
1025
- }
1026
- if (baseline.mode === 'migrated') {
1027
- return {
1028
- status: 'fail', blockers: ['BASELINE_MIGRATION_REVIEW_REQUIRED'], warnings: [], baseline,
1029
- };
1030
- }
1031
- if (baseline.status !== 'ready') {
1032
- return {
1033
- status: 'fail', blockers: [`BASELINE_NOT_READY:${baseline.status}`], warnings: [], baseline,
1034
- };
1035
- }
1036
- const research = storedResearchHealth(db, baseline);
1037
- const blockers = [
1038
- ...storedReadyInvariantBlockers(baseline),
1039
- ...(Array.isArray(baseline.scope) ? storedScopeBlockers(baseline, rootDir) : []),
1040
- ...(Array.isArray(baseline.spec_refs) ? storedSpecBlockers(baseline, rootDir) : []),
1041
- ...(Array.isArray(baseline.evidence) ? storedEvidenceBlockers(baseline, rootDir) : []),
1042
- ...research.blockers,
1043
- ];
1044
- const warnings = [
1045
- ...storedReadySemanticWarnings(baseline),
1046
- ...research.warnings,
1047
- ];
1048
- const snapshot = gitWorktreeSnapshot(
1049
- rootDir, Array.isArray(baseline.scope) && baseline.scope.length > 0 ? baseline.scope : ['.'],
1050
- );
1051
- if (snapshot.state === 'unborn') {
1052
- blockers.push('BASELINE_GIT_HEAD_REQUIRED');
1053
- }
1054
- if (snapshot.head && baseline.repository_revision && snapshot.head !== baseline.repository_revision) {
1055
- if (!gitCommitIsAncestor(rootDir, baseline.repository_revision, snapshot.head)
1056
- || (baseline.worktree_digest && snapshot.digest
1057
- && baseline.worktree_digest !== snapshot.digest)) {
1058
- blockers.push('BASELINE_HEAD_STALE');
1059
- }
1060
- } else if (!snapshot.head && baseline.repository_revision?.startsWith('workspace:')) {
1061
- const revision = workspaceRevision({
1062
- scope: baseline.scope, specs: baseline.spec_refs, evidence: baseline.evidence,
1063
- }, rootDir);
1064
- if (revision !== baseline.repository_revision) blockers.push('BASELINE_WORKSPACE_STALE');
1065
- }
1066
- if (baseline.repository_branch && snapshot.branch
1067
- && baseline.repository_branch !== snapshot.branch) blockers.push('BASELINE_BRANCH_STALE');
1068
- if (baseline.worktree_digest && snapshot.digest && baseline.worktree_digest !== snapshot.digest) {
1069
- blockers.push('BASELINE_WORKTREE_STALE');
1070
- if (snapshot.state === 'dirty') blockers.push('BASELINE_WORKTREE_DIRTY');
1071
- }
1072
- if (baseline.worktree_state === 'dirty' && !baseline.worktree_accepted) {
1073
- blockers.push('BASELINE_DIRTY_WORKTREE_NOT_ACCEPTED');
1074
- }
1075
- return {
1076
- status: blockers.length === 0 ? 'pass' : 'fail', blockers: [...new Set(blockers)].sort(),
1077
- warnings: [...new Set(warnings)], baseline,
1078
- };
1079
- }
1080
-
1081
- function inferSpecKind(file) {
1082
- const name = path.basename(file, path.extname(file)).toLowerCase();
1083
- return SPEC_KINDS.has(name) ? name : 'other';
1084
- }
1085
-
1086
- function reconcileBaseline(db, {
1087
- baseline_updates = [], change_id = null, reconciliation = null,
1088
- accept_delivery_worktree = false,
1089
- } = {}, {
1090
- rootDir = process.cwd(), emitEvent = true,
1091
- } = {}) {
1092
- const current = readBaseline(db);
1093
- if (!current || current.status !== 'ready') {
1094
- throw new BaselineWorkflowError('BASELINE_NOT_READY', 'a ready baseline is required for reconciliation');
1095
- }
1096
- if (current.mode === 'migrated' && current.spec_refs.length === 0) return current;
1097
- const refs = new Map(current.spec_refs.map((ref) => [ref.path, ref]));
1098
- const evidence = current.evidence.map((item) => ({ ...item }));
1099
- for (const update of baseline_updates) {
1100
- const file = safeRelativePath(rootDir, update, 'baseline update');
1101
- if (!fs.existsSync(file) || !fs.statSync(file).isFile()) {
1102
- throw new BaselineWorkflowError('BASELINE_FILE_MISSING', `baseline update missing: ${update}`);
1103
- }
1104
- const digest = digestFile(file);
1105
- const existingRef = refs.get(update);
1106
- if (existingRef) refs.set(update, { ...existingRef, digest });
1107
- let evidenceMatched = false;
1108
- for (const item of evidence) {
1109
- if (item.ref !== update || (item.kind !== 'source' && !item.digest)) continue;
1110
- item.digest = digest;
1111
- evidenceMatched = true;
1112
- }
1113
- if (!existingRef && !evidenceMatched) {
1114
- refs.set(update, { kind: inferSpecKind(update), path: update, digest });
1115
- }
1116
- }
1117
- const specs = [...refs.values()];
1118
- const resolvedGapIds = new Set(reconciliation?.resolved_gap_ids || []);
1119
- const resolvedUnknowns = new Set(reconciliation?.resolved_unknowns || []);
1120
- const gaps = current.gaps.map((gap) => (
1121
- resolvedGapIds.has(gap.id) ? { ...gap, status: 'resolved' } : gap
1122
- ));
1123
- const unknowns = current.unknowns.filter((unknown) => !resolvedUnknowns.has(unknown.summary));
1124
- const snapshot = gitWorktreeSnapshot(rootDir, current.scope);
1125
- const revision = snapshot.head || workspaceRevision({
1126
- scope: current.scope, specs, evidence,
1127
- }, rootDir);
1128
- const ts = nowIso();
1129
- ops.tx(db, () => {
1130
- db.prepare(
1131
- `UPDATE baselines SET repository_revision = ?, repository_branch = ?, worktree_state = ?,
1132
- worktree_digest = ?, worktree_files_json = ?, worktree_accepted = ?,
1133
- spec_refs_json = ?, evidence_json = ?, gaps_json = ?, unknowns_json = ?,
1134
- updated_at = ? WHERE id = ?`,
1135
- ).run(
1136
- revision, snapshot.branch, snapshot.state, snapshot.digest, JSON.stringify(snapshot.files),
1137
- accept_delivery_worktree && snapshot.state === 'dirty' ? 1 : 0,
1138
- JSON.stringify(specs), JSON.stringify(evidence), JSON.stringify(gaps),
1139
- JSON.stringify(unknowns), ts, current.id,
1140
- );
1141
- if (emitEvent) {
1142
- ops.appendEventInTx(db, {
1143
- type: 'baseline_reconciled',
1144
- change_id,
1145
- payload: {
1146
- baseline_id: current.id, baseline_updates, repository_revision: revision,
1147
- semantic_change_ids: (reconciliation?.semantic_changes || []).map((item) => item.id),
1148
- resolved_gap_ids: [...resolvedGapIds], resolved_unknowns: [...resolvedUnknowns],
1149
- },
1150
- });
1151
- }
1152
- });
1153
- return readBaseline(db, current.id);
1154
- }
1155
-
1156
- function appendGapInTx(db, input = {}, { emitEvent = true } = {}) {
1157
- const current = readBaseline(db, input.baseline_id);
1158
- if (!current) throw new BaselineWorkflowError('BASELINE_NOT_FOUND', 'current baseline not found');
1159
- const next = normalizeGaps([...current.gaps.filter((gap) => gap.id !== input.gap?.id), input.gap]);
1160
- const ts = nowIso();
1161
- db.prepare('UPDATE baselines SET gaps_json = ?, updated_at = ? WHERE id = ?')
1162
- .run(JSON.stringify(next), ts, current.id);
1163
- if (emitEvent) {
1164
- ops.appendEventInTx(db, {
1165
- type: 'baseline_gap_recorded',
1166
- payload: { baseline_id: current.id, gap_id: input.gap.id, category: input.gap.category },
1167
- });
1168
- }
1169
- return readBaseline(db, current.id);
1170
- }
1171
-
1172
- function appendGap(db, input = {}, options = {}) {
1173
- return ops.tx(db, () => appendGapInTx(db, input, options));
1174
- }
1175
-
1176
- module.exports = {
1177
- BaselineWorkflowError,
1178
- startBaseline,
1179
- refreshInProgressBaseline,
1180
- recordBaseline,
1181
- convergeBaseline,
1182
- readBaseline,
1183
- inspectBaseline,
1184
- reconcileBaseline,
1185
- gitHead,
1186
- gitBranch,
1187
- gitWorktreeSnapshot,
1188
- gitWorktreeChanges,
1189
- appendGap,
1190
- appendGapInTx,
1191
- summarizeGaps,
1192
- normalizeGaps,
1193
- };