@manehorizons/cadence-core 1.6.0 → 1.7.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 (94) hide show
  1. package/dist/cli/commands/assumption.d.ts.map +1 -1
  2. package/dist/cli/commands/assumption.js +2 -1
  3. package/dist/cli/commands/assumption.js.map +1 -1
  4. package/dist/cli/commands/decision.d.ts.map +1 -1
  5. package/dist/cli/commands/decision.js +2 -1
  6. package/dist/cli/commands/decision.js.map +1 -1
  7. package/dist/cli/commands/doctor.d.ts +3 -0
  8. package/dist/cli/commands/doctor.d.ts.map +1 -0
  9. package/dist/cli/commands/doctor.js +44 -0
  10. package/dist/cli/commands/doctor.js.map +1 -0
  11. package/dist/cli/commands/draft-new.d.ts.map +1 -1
  12. package/dist/cli/commands/draft-new.js +2 -1
  13. package/dist/cli/commands/draft-new.js.map +1 -1
  14. package/dist/cli/commands/intelligence.d.ts.map +1 -1
  15. package/dist/cli/commands/intelligence.js +4 -1
  16. package/dist/cli/commands/intelligence.js.map +1 -1
  17. package/dist/cli/commands/milestone.js +1 -1
  18. package/dist/cli/commands/milestone.js.map +1 -1
  19. package/dist/cli/commands/recommendation.d.ts.map +1 -1
  20. package/dist/cli/commands/recommendation.js +53 -1
  21. package/dist/cli/commands/recommendation.js.map +1 -1
  22. package/dist/cli/commands/spec.d.ts.map +1 -1
  23. package/dist/cli/commands/spec.js +2 -1
  24. package/dist/cli/commands/spec.js.map +1 -1
  25. package/dist/cli/register.d.ts.map +1 -1
  26. package/dist/cli/register.js +2 -0
  27. package/dist/cli/register.js.map +1 -1
  28. package/dist/doctor/model.d.ts +27 -0
  29. package/dist/doctor/model.d.ts.map +1 -0
  30. package/dist/doctor/model.js +10 -0
  31. package/dist/doctor/model.js.map +1 -0
  32. package/dist/doctor/run.d.ts +3 -0
  33. package/dist/doctor/run.d.ts.map +1 -0
  34. package/dist/doctor/run.js +141 -0
  35. package/dist/doctor/run.js.map +1 -0
  36. package/dist/intelligence/context.js +2 -1
  37. package/dist/intelligence/context.js.map +1 -1
  38. package/dist/intelligence/inspect.js +2 -1
  39. package/dist/intelligence/inspect.js.map +1 -1
  40. package/dist/intelligence/milestone.d.ts.map +1 -1
  41. package/dist/intelligence/milestone.js +2 -1
  42. package/dist/intelligence/milestone.js.map +1 -1
  43. package/dist/intelligence/recommend.d.ts.map +1 -1
  44. package/dist/intelligence/recommend.js +2 -1
  45. package/dist/intelligence/recommend.js.map +1 -1
  46. package/dist/intelligence/render-intelligence-audit.d.ts +1 -1
  47. package/dist/intelligence/render-intelligence-audit.d.ts.map +1 -1
  48. package/dist/intelligence/render-intelligence-stats.d.ts +1 -1
  49. package/dist/intelligence/render-intelligence-stats.d.ts.map +1 -1
  50. package/dist/intelligence/store/assumptions.d.ts +17 -0
  51. package/dist/intelligence/store/assumptions.d.ts.map +1 -0
  52. package/dist/intelligence/store/assumptions.js +63 -0
  53. package/dist/intelligence/store/assumptions.js.map +1 -0
  54. package/dist/intelligence/store/audit.d.ts +42 -0
  55. package/dist/intelligence/store/audit.d.ts.map +1 -0
  56. package/dist/intelligence/store/audit.js +88 -0
  57. package/dist/intelligence/store/audit.js.map +1 -0
  58. package/dist/intelligence/store/decisions.d.ts +19 -0
  59. package/dist/intelligence/store/decisions.d.ts.map +1 -0
  60. package/dist/intelligence/store/decisions.js +147 -0
  61. package/dist/intelligence/store/decisions.js.map +1 -0
  62. package/dist/intelligence/store/ids.d.ts +6 -0
  63. package/dist/intelligence/store/ids.d.ts.map +1 -0
  64. package/dist/intelligence/store/ids.js +44 -0
  65. package/dist/intelligence/store/ids.js.map +1 -0
  66. package/dist/intelligence/store/io.d.ts +10 -0
  67. package/dist/intelligence/store/io.d.ts.map +1 -0
  68. package/dist/intelligence/store/io.js +70 -0
  69. package/dist/intelligence/store/io.js.map +1 -0
  70. package/dist/intelligence/store/milestones.d.ts +4 -0
  71. package/dist/intelligence/store/milestones.d.ts.map +1 -0
  72. package/dist/intelligence/store/milestones.js +20 -0
  73. package/dist/intelligence/store/milestones.js.map +1 -0
  74. package/dist/intelligence/store/paths.d.ts +11 -0
  75. package/dist/intelligence/store/paths.d.ts.map +1 -0
  76. package/dist/intelligence/store/paths.js +42 -0
  77. package/dist/intelligence/store/paths.js.map +1 -0
  78. package/dist/intelligence/store/recommendations.d.ts +29 -0
  79. package/dist/intelligence/store/recommendations.d.ts.map +1 -0
  80. package/dist/intelligence/store/recommendations.js +168 -0
  81. package/dist/intelligence/store/recommendations.js.map +1 -0
  82. package/dist/intelligence/store/reconcile.d.ts +8 -0
  83. package/dist/intelligence/store/reconcile.d.ts.map +1 -0
  84. package/dist/intelligence/store/reconcile.js +42 -0
  85. package/dist/intelligence/store/reconcile.js.map +1 -0
  86. package/dist/intelligence/store/stats.d.ts +36 -0
  87. package/dist/intelligence/store/stats.d.ts.map +1 -0
  88. package/dist/intelligence/store/stats.js +106 -0
  89. package/dist/intelligence/store/stats.js.map +1 -0
  90. package/package.json +2 -2
  91. package/dist/intelligence/store.d.ts +0 -143
  92. package/dist/intelligence/store.d.ts.map +0 -1
  93. package/dist/intelligence/store.js +0 -701
  94. package/dist/intelligence/store.js.map +0 -1
@@ -1,701 +0,0 @@
1
- import { mkdir, readFile, stat } from 'node:fs/promises';
2
- import { existsSync } from 'node:fs';
3
- import { join } from 'node:path';
4
- import { AssumptionLedgerZ, EvidenceLedgerZ, IntelligenceDecisionLedgerZ, MilestoneLedgerZ, RecommendationLedgerZ, emptyAssumptionLedger, emptyEvidenceLedger, emptyIntelligenceDecisionLedger, emptyMilestoneLedger, emptyRecommendationLedger, } from '@manehorizons/cadence-types';
5
- import { atomicWriteJSON, atomicWriteText } from '../state/atomic-write.js';
6
- import { renderRecommendationsMd } from './render.js';
7
- import { renderMilestonesMd } from './render-milestone.js';
8
- import { renderAssumptionsMd } from './render-assumption.js';
9
- import { renderDecisionsMd } from './render-decision.js';
10
- const INTELLIGENCE_DIR = '.cadence/intelligence';
11
- const RECOMMENDATIONS_JSON = 'recommendations.json';
12
- const EVIDENCE_JSON = 'evidence.json';
13
- const RECOMMENDATIONS_MD = 'RECOMMENDATIONS.md';
14
- const ASSUMPTIONS_JSON = 'assumptions.json';
15
- const DECISIONS_JSON = 'decisions.json';
16
- const ASSUMPTIONS_MD = 'ASSUMPTIONS.md';
17
- const DECISIONS_MD = 'DECISIONS.md';
18
- export function intelligenceDir(root) {
19
- return join(root, INTELLIGENCE_DIR);
20
- }
21
- function recommendationsPath(root) {
22
- return join(intelligenceDir(root), RECOMMENDATIONS_JSON);
23
- }
24
- function evidencePath(root) {
25
- return join(intelligenceDir(root), EVIDENCE_JSON);
26
- }
27
- function assumptionsPath(root) {
28
- return join(intelligenceDir(root), ASSUMPTIONS_JSON);
29
- }
30
- function decisionsPath(root) {
31
- return join(intelligenceDir(root), DECISIONS_JSON);
32
- }
33
- function recommendationsMdPath(root) {
34
- return join(intelligenceDir(root), RECOMMENDATIONS_MD);
35
- }
36
- function assumptionsMdPath(root) {
37
- return join(intelligenceDir(root), ASSUMPTIONS_MD);
38
- }
39
- function decisionsMdPath(root) {
40
- return join(intelligenceDir(root), DECISIONS_MD);
41
- }
42
- export async function readRecommendationLedger(root) {
43
- const path = recommendationsPath(root);
44
- if (!existsSync(path))
45
- return emptyRecommendationLedger();
46
- const raw = await readFile(path, 'utf8');
47
- return RecommendationLedgerZ.parse(JSON.parse(raw));
48
- }
49
- export async function readEvidenceLedger(root) {
50
- const path = evidencePath(root);
51
- if (!existsSync(path))
52
- return emptyEvidenceLedger();
53
- const raw = await readFile(path, 'utf8');
54
- return EvidenceLedgerZ.parse(JSON.parse(raw));
55
- }
56
- export async function readAssumptionLedger(root) {
57
- const path = assumptionsPath(root);
58
- if (!existsSync(path))
59
- return emptyAssumptionLedger();
60
- const raw = await readFile(path, 'utf8');
61
- return AssumptionLedgerZ.parse(JSON.parse(raw));
62
- }
63
- export async function readIntelligenceDecisionLedger(root) {
64
- const path = decisionsPath(root);
65
- if (!existsSync(path))
66
- return emptyIntelligenceDecisionLedger();
67
- const raw = await readFile(path, 'utf8');
68
- return IntelligenceDecisionLedgerZ.parse(JSON.parse(raw));
69
- }
70
- async function writeIntelligenceLedgers(root, ledger, evidenceLedger) {
71
- const dir = intelligenceDir(root);
72
- await mkdir(dir, { recursive: true });
73
- RecommendationLedgerZ.parse(ledger);
74
- EvidenceLedgerZ.parse(evidenceLedger);
75
- await atomicWriteJSON(recommendationsPath(root), ledger);
76
- await atomicWriteJSON(evidencePath(root), evidenceLedger);
77
- // Read sibling ledgers so the rec MD renders status-annotated link bullets (Slice 15).
78
- const asLedger = await readAssumptionLedger(root);
79
- const decLedger = await readIntelligenceDecisionLedger(root);
80
- await atomicWriteText(recommendationsMdPath(root), renderRecommendationsMd(ledger, evidenceLedger, asLedger, decLedger));
81
- }
82
- function slugDate(now) {
83
- return now.toISOString().slice(0, 10).replaceAll('-', '');
84
- }
85
- function nextRecommendationId(ledger, now) {
86
- const prefix = `rec-${slugDate(now)}-`;
87
- const max = ledger.recommendations
88
- .map((r) => r.id)
89
- .filter((id) => id.startsWith(prefix))
90
- .map((id) => Number.parseInt(id.slice(prefix.length), 10))
91
- .filter((n) => Number.isFinite(n))
92
- .reduce((a, b) => Math.max(a, b), 0);
93
- return `${prefix}${String(max + 1).padStart(3, '0')}`;
94
- }
95
- function nextEvidenceId(ledger, now) {
96
- const prefix = `ev-${slugDate(now)}-`;
97
- const max = ledger.evidence
98
- .map((ev) => ev.id)
99
- .filter((id) => id.startsWith(prefix))
100
- .map((id) => Number.parseInt(id.slice(prefix.length), 10))
101
- .filter((n) => Number.isFinite(n))
102
- .reduce((a, b) => Math.max(a, b), 0);
103
- return `${prefix}${String(max + 1).padStart(3, '0')}`;
104
- }
105
- function nextAssumptionId(ledger, now) {
106
- const prefix = `as-${slugDate(now)}-`;
107
- const max = ledger.assumptions
108
- .map((a) => a.id)
109
- .filter((id) => id.startsWith(prefix))
110
- .map((id) => Number.parseInt(id.slice(prefix.length), 10))
111
- .filter((n) => Number.isFinite(n))
112
- .reduce((a, b) => Math.max(a, b), 0);
113
- return `${prefix}${String(max + 1).padStart(3, '0')}`;
114
- }
115
- function nextIntelligenceDecisionId(ledger, now) {
116
- const prefix = `dec-${slugDate(now)}-`;
117
- const max = ledger.decisions
118
- .map((d) => d.id)
119
- .filter((id) => id.startsWith(prefix))
120
- .map((id) => Number.parseInt(id.slice(prefix.length), 10))
121
- .filter((n) => Number.isFinite(n))
122
- .reduce((a, b) => Math.max(a, b), 0);
123
- return `${prefix}${String(max + 1).padStart(3, '0')}`;
124
- }
125
- export async function addRecommendation(root, input) {
126
- const ledger = await readRecommendationLedger(root);
127
- const evidenceLedger = await readEvidenceLedger(root);
128
- const now = new Date();
129
- const ts = now.toISOString();
130
- const recommendationId = nextRecommendationId(ledger, now);
131
- const evidence = input.evidenceSummary
132
- ? {
133
- id: nextEvidenceId(evidenceLedger, now),
134
- recommendationId,
135
- kind: 'note',
136
- summary: input.evidenceSummary,
137
- createdAt: ts,
138
- }
139
- : null;
140
- const rec = {
141
- id: recommendationId,
142
- title: input.title,
143
- summary: input.summary,
144
- source: 'manual',
145
- status: 'candidate',
146
- readiness: input.readiness,
147
- priority: input.priority,
148
- leverageScore: 5,
149
- riskScore: 5,
150
- confidence: input.evidenceSummary ? 0.7 : 0.4,
151
- decayState: 'fresh',
152
- affectedAreas: input.affectedAreas,
153
- affectedFiles: input.affectedFiles,
154
- suggestedBackendAction: 'cadence milestone propose',
155
- evidenceIds: evidence ? [evidence.id] : [],
156
- assumptionIds: [],
157
- decisionIds: [],
158
- createdAt: ts,
159
- updatedAt: ts,
160
- };
161
- if (evidence)
162
- evidenceLedger.evidence.push(evidence);
163
- ledger.recommendations.push(rec);
164
- await writeIntelligenceLedgers(root, ledger, evidenceLedger);
165
- return rec;
166
- }
167
- async function writeAssumptionLedger(root, ledger) {
168
- AssumptionLedgerZ.parse(ledger);
169
- await mkdir(intelligenceDir(root), { recursive: true });
170
- await atomicWriteJSON(assumptionsPath(root), ledger);
171
- await atomicWriteText(assumptionsMdPath(root), renderAssumptionsMd(ledger));
172
- }
173
- export async function addAssumption(root, input) {
174
- const recLedger = await readRecommendationLedger(root);
175
- if (!recLedger.recommendations.some((r) => r.id === input.recommendationId)) {
176
- throw new Error(`unknown recommendation "${input.recommendationId}"`);
177
- }
178
- const asLedger = await readAssumptionLedger(root);
179
- const now = new Date();
180
- const a = {
181
- id: nextAssumptionId(asLedger, now),
182
- recommendationId: input.recommendationId,
183
- text: input.text,
184
- status: 'open',
185
- createdAt: now.toISOString(),
186
- };
187
- asLedger.assumptions.push(a);
188
- await writeAssumptionLedger(root, asLedger);
189
- const decLedger = await readIntelligenceDecisionLedger(root);
190
- const evLedger = await readEvidenceLedger(root);
191
- const derivedRec = deriveRecommendationLinks(recLedger, asLedger, decLedger);
192
- await writeIntelligenceLedgers(root, derivedRec, evLedger);
193
- return a;
194
- }
195
- const ASSUMPTION_TRANSITION_ALLOWED = {
196
- validate: ['open'],
197
- reject: ['open'],
198
- reopen: ['validated', 'rejected'],
199
- };
200
- const ASSUMPTION_TRANSITION_NEXT = {
201
- validate: 'validated',
202
- reject: 'rejected',
203
- reopen: 'open',
204
- };
205
- export function applyAssumptionTransition(ledger, id, action, _now) {
206
- const target = ledger.assumptions.find((a) => a.id === id);
207
- if (!target)
208
- return { ok: false, error: `assumption ${id} not found` };
209
- if (!ASSUMPTION_TRANSITION_ALLOWED[action].includes(target.status)) {
210
- return {
211
- ok: false,
212
- error: `cannot ${action} assumption in status ${target.status}`,
213
- };
214
- }
215
- const nextStatus = ASSUMPTION_TRANSITION_NEXT[action];
216
- const ledgerOut = {
217
- schemaVersion: 1,
218
- assumptions: ledger.assumptions.map((a) => a.id === id ? { ...a, status: nextStatus } : a),
219
- };
220
- return { ok: true, ledger: ledgerOut };
221
- }
222
- export async function runAssumptionTransition(root, id, action) {
223
- const ledger = await readAssumptionLedger(root);
224
- const res = applyAssumptionTransition(ledger, id, action, new Date());
225
- if (!res.ok)
226
- return res;
227
- await writeAssumptionLedger(root, res.ledger);
228
- // Slice 15: propagate status change into RECOMMENDATIONS.md annotated bullets.
229
- await rerenderRecommendationsMdIfPresent(root);
230
- return res;
231
- }
232
- async function rerenderRecommendationsMdIfPresent(root) {
233
- if (!existsSync(recommendationsPath(root)))
234
- return;
235
- const recLedger = await readRecommendationLedger(root);
236
- const evLedger = await readEvidenceLedger(root);
237
- const asLedger = await readAssumptionLedger(root);
238
- const decLedger = await readIntelligenceDecisionLedger(root);
239
- await atomicWriteText(recommendationsMdPath(root), renderRecommendationsMd(recLedger, evLedger, asLedger, decLedger));
240
- }
241
- export function deriveRecommendationLinks(recLedger, asLedger, decLedger) {
242
- return {
243
- schemaVersion: 1,
244
- recommendations: recLedger.recommendations.map((r) => ({
245
- ...r,
246
- assumptionIds: asLedger.assumptions
247
- .filter((a) => a.recommendationId === r.id)
248
- .map((a) => a.id),
249
- decisionIds: decLedger.decisions
250
- .filter((d) => d.recommendationId === r.id)
251
- .map((d) => d.id),
252
- })),
253
- };
254
- }
255
- async function writeIntelligenceDecisionLedger(root, ledger) {
256
- IntelligenceDecisionLedgerZ.parse(ledger);
257
- await mkdir(intelligenceDir(root), { recursive: true });
258
- await atomicWriteJSON(decisionsPath(root), ledger);
259
- await atomicWriteText(decisionsMdPath(root), renderDecisionsMd(ledger));
260
- }
261
- export async function addIntelligenceDecision(root, input) {
262
- let recLedger = null;
263
- if (input.recommendationId !== undefined) {
264
- recLedger = await readRecommendationLedger(root);
265
- if (!recLedger.recommendations.some((r) => r.id === input.recommendationId)) {
266
- throw new Error(`unknown recommendation "${input.recommendationId}"`);
267
- }
268
- }
269
- const decLedger = await readIntelligenceDecisionLedger(root);
270
- const now = new Date();
271
- // Construct in schema-declaration order so the persisted JSON property
272
- // order matches what `IntelligenceDecisionLedgerZ.parse(...)` would produce
273
- // on reconcile. Keeps byte-equality between addIntelligenceDecision writes
274
- // and reconcile writes (relied on by Slice-17 AC-6).
275
- const out = {
276
- id: nextIntelligenceDecisionId(decLedger, now),
277
- ...(input.recommendationId !== undefined ? { recommendationId: input.recommendationId } : {}),
278
- title: input.title,
279
- rationale: input.rationale,
280
- status: 'active',
281
- decidedAt: now.toISOString(),
282
- supersedes: [],
283
- };
284
- decLedger.decisions.push(out);
285
- // Slice 31: re-derive supersedes arrays across the whole ledger. New
286
- // decisions can't be referenced by older ones yet, but pre-Slice-31
287
- // ledgers being loaded for the first time benefit from the field
288
- // being populated explicitly on write.
289
- const derivedDec = deriveDecisionInverseLinks(decLedger);
290
- await writeIntelligenceDecisionLedger(root, derivedDec);
291
- if (input.recommendationId !== undefined && recLedger !== null) {
292
- const asLedger = await readAssumptionLedger(root);
293
- const evLedger = await readEvidenceLedger(root);
294
- const derivedRec = deriveRecommendationLinks(recLedger, asLedger, decLedger);
295
- await writeIntelligenceLedgers(root, derivedRec, evLedger);
296
- }
297
- return out;
298
- }
299
- const DECISION_TRANSITION_ALLOWED = {
300
- supersede: ['active'],
301
- rescind: ['active'],
302
- reactivate: ['superseded', 'rescinded'],
303
- };
304
- const DECISION_TRANSITION_NEXT = {
305
- supersede: 'superseded',
306
- rescind: 'rescinded',
307
- reactivate: 'active',
308
- };
309
- function walkSupersededByChain(ledger, startId, forbid) {
310
- const chain = [];
311
- const seen = new Set();
312
- let cursor = startId;
313
- while (cursor) {
314
- if (cursor === forbid)
315
- return { ok: false, chain };
316
- if (seen.has(cursor))
317
- return { ok: true, chain };
318
- seen.add(cursor);
319
- chain.push(cursor);
320
- const node = ledger.decisions.find((d) => d.id === cursor);
321
- cursor = node?.supersededBy;
322
- }
323
- return { ok: true, chain };
324
- }
325
- // Slice 31: pure helper. For every decision D in the ledger, recompute
326
- // D.supersedes from current supersededBy values — the inverse-link of
327
- // Slice 28's edge. Mirrors Slice 11's deriveRecommendationLinks shape
328
- // but operates within one ledger. Idempotent.
329
- export function deriveDecisionInverseLinks(ledger) {
330
- return {
331
- schemaVersion: 1,
332
- decisions: ledger.decisions.map((d) => ({
333
- ...d,
334
- supersedes: ledger.decisions
335
- .filter((other) => other.supersededBy === d.id)
336
- .map((other) => other.id),
337
- })),
338
- };
339
- }
340
- export function applyDecisionTransition(ledger, id, action, by, _now) {
341
- const target = ledger.decisions.find((d) => d.id === id);
342
- if (!target)
343
- return { ok: false, error: `decision ${id} not found` };
344
- if (!DECISION_TRANSITION_ALLOWED[action].includes(target.status)) {
345
- return {
346
- ok: false,
347
- error: `cannot ${action} decision in status ${target.status}`,
348
- };
349
- }
350
- if (action === 'supersede' && by !== undefined) {
351
- if (by === id) {
352
- return {
353
- ok: false,
354
- error: 'cannot supersede: decision cannot supersede itself',
355
- };
356
- }
357
- const replacement = ledger.decisions.find((d) => d.id === by);
358
- if (!replacement) {
359
- return {
360
- ok: false,
361
- error: `cannot supersede: decision ${by} not found`,
362
- };
363
- }
364
- const walk = walkSupersededByChain(ledger, by, id);
365
- if (!walk.ok) {
366
- return {
367
- ok: false,
368
- error: `cannot supersede: would create cycle (${[...walk.chain, id].join(' → ')})`,
369
- };
370
- }
371
- }
372
- const nextStatus = DECISION_TRANSITION_NEXT[action];
373
- const ledgerOut = {
374
- schemaVersion: 1,
375
- decisions: ledger.decisions.map((d) => {
376
- if (d.id !== id)
377
- return d;
378
- const updated = { ...d, status: nextStatus };
379
- if (action === 'supersede') {
380
- if (by !== undefined)
381
- updated.supersededBy = by;
382
- }
383
- else if (action === 'reactivate') {
384
- delete updated.supersededBy;
385
- }
386
- return updated;
387
- }),
388
- };
389
- // Slice 31: re-derive supersedes arrays so the returned ledger is fully
390
- // consistent (target's supersededBy update propagates to the replacement's
391
- // supersedes array, and reactivate-cleared targets drop out).
392
- return { ok: true, ledger: deriveDecisionInverseLinks(ledgerOut) };
393
- }
394
- export async function runDecisionTransition(root, id, action, by) {
395
- const ledger = await readIntelligenceDecisionLedger(root);
396
- const res = applyDecisionTransition(ledger, id, action, by, new Date());
397
- if (!res.ok)
398
- return res;
399
- await writeIntelligenceDecisionLedger(root, res.ledger);
400
- // Slice 15: propagate status change into RECOMMENDATIONS.md annotated bullets.
401
- await rerenderRecommendationsMdIfPresent(root);
402
- return res;
403
- }
404
- const RECOMMENDATION_TRANSITION_ALLOWED = {
405
- convert: ['candidate', 'accepted'],
406
- };
407
- const RECOMMENDATION_TRANSITION_NEXT = {
408
- convert: 'converted',
409
- };
410
- export function applyRecommendationTransition(ledger, id, action, toPhase, now) {
411
- const target = ledger.recommendations.find((r) => r.id === id);
412
- if (!target)
413
- return { ok: false, error: `recommendation ${id} not found` };
414
- if (!RECOMMENDATION_TRANSITION_ALLOWED[action].includes(target.status)) {
415
- return {
416
- ok: false,
417
- error: `cannot ${action} recommendation in status ${target.status}`,
418
- };
419
- }
420
- const nextStatus = RECOMMENDATION_TRANSITION_NEXT[action];
421
- const updatedAt = now.toISOString();
422
- const ledgerOut = {
423
- schemaVersion: 1,
424
- recommendations: ledger.recommendations.map((r) => r.id === id
425
- ? { ...r, status: nextStatus, convertedToPhaseId: toPhase, updatedAt }
426
- : r),
427
- };
428
- return { ok: true, ledger: ledgerOut };
429
- }
430
- // Slice 34.1: existence check for `.cadence/phases/<phaseId>/` lives in the
431
- // I/O wrapper deliberately — keeps the pure helper disk-free (per Slice 34
432
- // Decision Log §10 + §11 architectural principle).
433
- async function phaseDirectoryExists(root, phaseId) {
434
- try {
435
- const s = await stat(join(root, '.cadence/phases', phaseId));
436
- return s.isDirectory();
437
- }
438
- catch {
439
- return false;
440
- }
441
- }
442
- export async function runRecommendationTransition(root, id, action, toPhase) {
443
- // FK check FIRST so a missing phase is caught before any ledger read.
444
- // Mirrors Slice 28's --by precedence: validate FK before applying transition.
445
- if (!(await phaseDirectoryExists(root, toPhase))) {
446
- return { ok: false, error: `cannot convert: phase ${toPhase} not found` };
447
- }
448
- const ledger = await readRecommendationLedger(root);
449
- const res = applyRecommendationTransition(ledger, id, action, toPhase, new Date());
450
- if (!res.ok)
451
- return res;
452
- // writeIntelligenceLedgers handles atomic JSON + RECOMMENDATIONS.md re-render
453
- // (Slice 15 annotated form), so we don't need a separate rerender call.
454
- const evidenceLedger = await readEvidenceLedger(root);
455
- await writeIntelligenceLedgers(root, res.ledger, evidenceLedger);
456
- return res;
457
- }
458
- const MILESTONES_JSON = 'milestones.json';
459
- const MILESTONES_MD = 'MILESTONES.md';
460
- const REC_STATUSES = [
461
- 'candidate',
462
- 'accepted',
463
- 'deferred',
464
- 'rejected',
465
- 'converted',
466
- ];
467
- const REC_READINESSES = [
468
- 'raw-idea',
469
- 'needs-evidence',
470
- 'needs-decision',
471
- 'ready-for-milestone',
472
- 'ready-for-cadence-spec',
473
- 'blocked',
474
- ];
475
- const EV_KINDS = ['file', 'command', 'cadence-artifact', 'note'];
476
- const AS_STATUSES = ['open', 'validated', 'rejected'];
477
- const DEC_STATUSES = [
478
- 'active',
479
- 'superseded',
480
- 'rescinded',
481
- ];
482
- export function computeIntelligenceStats(recLedger, evLedger, asLedger, decLedger) {
483
- const recByStatus = Object.fromEntries(REC_STATUSES.map((s) => [s, 0]));
484
- const recByReadiness = Object.fromEntries(REC_READINESSES.map((r) => [r, 0]));
485
- for (const r of recLedger.recommendations) {
486
- recByStatus[r.status]++;
487
- recByReadiness[r.readiness]++;
488
- }
489
- const evByKind = Object.fromEntries(EV_KINDS.map((k) => [k, 0]));
490
- for (const ev of evLedger.evidence)
491
- evByKind[ev.kind]++;
492
- const asByStatus = Object.fromEntries(AS_STATUSES.map((s) => [s, 0]));
493
- for (const a of asLedger.assumptions)
494
- asByStatus[a.status]++;
495
- const decByStatus = Object.fromEntries(DEC_STATUSES.map((s) => [s, 0]));
496
- let decUntied = 0;
497
- for (const d of decLedger.decisions) {
498
- decByStatus[d.status]++;
499
- if (d.recommendationId === undefined)
500
- decUntied++;
501
- }
502
- const asById = new Map(asLedger.assumptions.map((a) => [a.id, a]));
503
- const decById = new Map(decLedger.decisions.map((d) => [d.id, d]));
504
- const evById = new Map(evLedger.evidence.map((e) => [e.id, e]));
505
- let brokenAssumptionLinks = 0;
506
- let brokenDecisionLinks = 0;
507
- let brokenEvidenceLinks = 0;
508
- for (const r of recLedger.recommendations) {
509
- for (const id of r.assumptionIds)
510
- if (!asById.has(id))
511
- brokenAssumptionLinks++;
512
- for (const id of r.decisionIds)
513
- if (!decById.has(id))
514
- brokenDecisionLinks++;
515
- for (const id of r.evidenceIds)
516
- if (!evById.has(id))
517
- brokenEvidenceLinks++;
518
- }
519
- const perRec = recLedger.recommendations.map((r) => {
520
- const ascount = Object.fromEntries(AS_STATUSES.map((s) => [s, 0]));
521
- const dccount = Object.fromEntries(DEC_STATUSES.map((s) => [s, 0]));
522
- for (const id of r.assumptionIds) {
523
- const a = asById.get(id);
524
- if (a)
525
- ascount[a.status]++;
526
- }
527
- for (const id of r.decisionIds) {
528
- const d = decById.get(id);
529
- if (d)
530
- dccount[d.status]++;
531
- }
532
- let evCount = 0;
533
- for (const id of r.evidenceIds)
534
- if (evById.has(id))
535
- evCount++;
536
- return {
537
- id: r.id,
538
- title: r.title,
539
- status: r.status,
540
- assumptionsByStatus: ascount,
541
- decisionsByStatus: dccount,
542
- evidenceCount: evCount,
543
- };
544
- });
545
- return {
546
- recommendations: {
547
- total: recLedger.recommendations.length,
548
- byStatus: recByStatus,
549
- byReadiness: recByReadiness,
550
- },
551
- evidence: { total: evLedger.evidence.length, byKind: evByKind },
552
- assumptions: {
553
- total: asLedger.assumptions.length,
554
- byStatus: asByStatus,
555
- },
556
- decisions: {
557
- total: decLedger.decisions.length,
558
- byStatus: decByStatus,
559
- untied: decUntied,
560
- },
561
- links: { brokenAssumptionLinks, brokenDecisionLinks, brokenEvidenceLinks },
562
- perRec,
563
- };
564
- }
565
- export const AUDIT_KINDS = [
566
- 'broken-assumption-link',
567
- 'broken-decision-link',
568
- 'broken-evidence-link',
569
- 'orphan-assumption',
570
- 'orphan-decision',
571
- 'orphan-evidence',
572
- 'stale-supersededby',
573
- 'stale-converted-phase',
574
- ];
575
- export function computeIntelligenceAudit(recLedger, evLedger, asLedger, decLedger, existingPhaseIds = new Set()) {
576
- const findings = [];
577
- const recIds = new Set(recLedger.recommendations.map((r) => r.id));
578
- const evIds = new Set(evLedger.evidence.map((e) => e.id));
579
- const asIds = new Set(asLedger.assumptions.map((a) => a.id));
580
- const decIds = new Set(decLedger.decisions.map((d) => d.id));
581
- for (const r of recLedger.recommendations) {
582
- for (const id of r.assumptionIds) {
583
- if (!asIds.has(id)) {
584
- findings.push({ kind: 'broken-assumption-link', recId: r.id, assumptionId: id });
585
- }
586
- }
587
- for (const id of r.decisionIds) {
588
- if (!decIds.has(id)) {
589
- findings.push({ kind: 'broken-decision-link', recId: r.id, decisionId: id });
590
- }
591
- }
592
- for (const id of r.evidenceIds) {
593
- if (!evIds.has(id)) {
594
- findings.push({ kind: 'broken-evidence-link', recId: r.id, evidenceId: id });
595
- }
596
- }
597
- }
598
- for (const a of asLedger.assumptions) {
599
- if (!recIds.has(a.recommendationId)) {
600
- findings.push({
601
- kind: 'orphan-assumption',
602
- assumptionId: a.id,
603
- missingRecId: a.recommendationId,
604
- });
605
- }
606
- }
607
- for (const d of decLedger.decisions) {
608
- if (d.recommendationId !== undefined && !recIds.has(d.recommendationId)) {
609
- findings.push({
610
- kind: 'orphan-decision',
611
- decisionId: d.id,
612
- missingRecId: d.recommendationId,
613
- });
614
- }
615
- }
616
- for (const ev of evLedger.evidence) {
617
- if (!recIds.has(ev.recommendationId)) {
618
- findings.push({
619
- kind: 'orphan-evidence',
620
- evidenceId: ev.id,
621
- missingRecId: ev.recommendationId,
622
- });
623
- }
624
- }
625
- // Slice 30: stale supersededBy refs (decision.supersededBy points to a missing decision id).
626
- for (const d of decLedger.decisions) {
627
- if (d.supersededBy !== undefined && !decIds.has(d.supersededBy)) {
628
- findings.push({
629
- kind: 'stale-supersededby',
630
- decisionId: d.id,
631
- missingTargetId: d.supersededBy,
632
- });
633
- }
634
- }
635
- // Slice 34.2: stale convertedToPhaseId refs (rec converted to a phase that
636
- // no longer exists on disk). Phase existence is a filesystem fact; the
637
- // caller pre-computes the set so `computeIntelligenceAudit` stays pure-sync.
638
- for (const r of recLedger.recommendations) {
639
- if (r.convertedToPhaseId !== undefined && !existingPhaseIds.has(r.convertedToPhaseId)) {
640
- findings.push({
641
- kind: 'stale-converted-phase',
642
- recommendationId: r.id,
643
- missingPhaseId: r.convertedToPhaseId,
644
- });
645
- }
646
- }
647
- const byKind = Object.fromEntries(AUDIT_KINDS.map((k) => [k, []]));
648
- for (const f of findings)
649
- byKind[f.kind].push(f);
650
- return { findings, byKind };
651
- }
652
- export async function runIntelligenceReconcile(root) {
653
- const recExists = existsSync(recommendationsPath(root));
654
- const evExists = existsSync(evidencePath(root));
655
- const asExists = existsSync(assumptionsPath(root));
656
- const decExists = existsSync(decisionsPath(root));
657
- if (!recExists && !evExists && !asExists && !decExists) {
658
- return { present: false, recommendations: 0, assumptions: 0, decisions: 0 };
659
- }
660
- const recLedger = await readRecommendationLedger(root);
661
- const evLedger = await readEvidenceLedger(root);
662
- const asLedger = await readAssumptionLedger(root);
663
- const decLedger = await readIntelligenceDecisionLedger(root);
664
- // Re-derive rec link arrays from current subject ledgers (idempotent if already correct).
665
- const derivedRec = deriveRecommendationLinks(recLedger, asLedger, decLedger);
666
- // Slice 31: re-derive decision inverse-links (supersedes arrays) from
667
- // current supersededBy values. Idempotent. Operator's "force re-derive"
668
- // command fixes any drift introduced by manual JSON edits.
669
- const derivedDec = deriveDecisionInverseLinks(decLedger);
670
- // writeIntelligenceLedgers handles atomic JSON + RECOMMENDATIONS.md re-render (Slice 15 annotated form).
671
- await writeIntelligenceLedgers(root, derivedRec, evLedger);
672
- await writeIntelligenceDecisionLedger(root, derivedDec);
673
- // Re-render subject MDs from current ledgers (source-of-truth JSON untouched).
674
- await mkdir(intelligenceDir(root), { recursive: true });
675
- await atomicWriteText(assumptionsMdPath(root), renderAssumptionsMd(asLedger));
676
- await atomicWriteText(decisionsMdPath(root), renderDecisionsMd(derivedDec));
677
- return {
678
- present: true,
679
- recommendations: derivedRec.recommendations.length,
680
- assumptions: asLedger.assumptions.length,
681
- decisions: derivedDec.decisions.length,
682
- };
683
- }
684
- function milestonesPath(root) {
685
- return join(intelligenceDir(root), MILESTONES_JSON);
686
- }
687
- export async function readMilestoneLedger(root) {
688
- const path = milestonesPath(root);
689
- if (!existsSync(path))
690
- return emptyMilestoneLedger();
691
- const raw = await readFile(path, 'utf8');
692
- return MilestoneLedgerZ.parse(JSON.parse(raw));
693
- }
694
- export async function writeMilestoneLedger(root, ledger) {
695
- const dir = intelligenceDir(root);
696
- await mkdir(dir, { recursive: true });
697
- MilestoneLedgerZ.parse(ledger);
698
- await atomicWriteJSON(join(dir, MILESTONES_JSON), ledger);
699
- await atomicWriteText(join(dir, MILESTONES_MD), renderMilestonesMd(ledger));
700
- }
701
- //# sourceMappingURL=store.js.map