@nforma.ai/nforma 0.2.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 (215) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +1024 -0
  3. package/agents/qgsd-codebase-mapper.md +764 -0
  4. package/agents/qgsd-debugger.md +1201 -0
  5. package/agents/qgsd-executor.md +472 -0
  6. package/agents/qgsd-integration-checker.md +443 -0
  7. package/agents/qgsd-phase-researcher.md +502 -0
  8. package/agents/qgsd-plan-checker.md +643 -0
  9. package/agents/qgsd-planner.md +1182 -0
  10. package/agents/qgsd-project-researcher.md +621 -0
  11. package/agents/qgsd-quorum-orchestrator.md +628 -0
  12. package/agents/qgsd-quorum-slot-worker.md +41 -0
  13. package/agents/qgsd-quorum-synthesizer.md +133 -0
  14. package/agents/qgsd-quorum-test-worker.md +37 -0
  15. package/agents/qgsd-quorum-worker.md +161 -0
  16. package/agents/qgsd-research-synthesizer.md +239 -0
  17. package/agents/qgsd-roadmapper.md +660 -0
  18. package/agents/qgsd-verifier.md +628 -0
  19. package/bin/accept-debug-invariant.cjs +165 -0
  20. package/bin/account-manager.cjs +719 -0
  21. package/bin/aggregate-requirements.cjs +466 -0
  22. package/bin/analyze-assumptions.cjs +757 -0
  23. package/bin/analyze-state-space.cjs +921 -0
  24. package/bin/attribute-trace-divergence.cjs +150 -0
  25. package/bin/auth-drivers/gh-cli.cjs +93 -0
  26. package/bin/auth-drivers/index.cjs +46 -0
  27. package/bin/auth-drivers/pool.cjs +67 -0
  28. package/bin/auth-drivers/simple.cjs +95 -0
  29. package/bin/autoClosePtoF.cjs +110 -0
  30. package/bin/blessed-terminal.cjs +350 -0
  31. package/bin/build-phase-index.cjs +472 -0
  32. package/bin/call-quorum-slot.cjs +541 -0
  33. package/bin/ccr-secure-config.cjs +99 -0
  34. package/bin/ccr-secure-start.cjs +83 -0
  35. package/bin/check-bundled-sdks.cjs +177 -0
  36. package/bin/check-coverage-guard.cjs +112 -0
  37. package/bin/check-liveness-fairness.cjs +95 -0
  38. package/bin/check-mcp-health.cjs +123 -0
  39. package/bin/check-provider-health.cjs +395 -0
  40. package/bin/check-results-exit.cjs +24 -0
  41. package/bin/check-spec-sync.cjs +360 -0
  42. package/bin/check-trace-redaction.cjs +271 -0
  43. package/bin/check-trace-schema-drift.cjs +99 -0
  44. package/bin/compareDrift.cjs +21 -0
  45. package/bin/conformance-schema.cjs +12 -0
  46. package/bin/count-scenarios.cjs +420 -0
  47. package/bin/debt-dedup.cjs +144 -0
  48. package/bin/debt-ledger.cjs +61 -0
  49. package/bin/debt-retention.cjs +76 -0
  50. package/bin/debt-state-machine.cjs +80 -0
  51. package/bin/detect-coverage-gaps.cjs +204 -0
  52. package/bin/detect-project-intent.cjs +362 -0
  53. package/bin/export-prism-constants.cjs +164 -0
  54. package/bin/extract-annotations.cjs +633 -0
  55. package/bin/extractFormalExpected.cjs +104 -0
  56. package/bin/fingerprint-drift.cjs +24 -0
  57. package/bin/fingerprint-issue.cjs +46 -0
  58. package/bin/formal-core.cjs +519 -0
  59. package/bin/formal-ref-linker.cjs +141 -0
  60. package/bin/formal-test-sync.cjs +788 -0
  61. package/bin/generate-formal-specs.cjs +588 -0
  62. package/bin/generate-petri-net.cjs +397 -0
  63. package/bin/generate-phase-spec.cjs +249 -0
  64. package/bin/generate-proposed-changes.cjs +194 -0
  65. package/bin/generate-tla-cfg.cjs +122 -0
  66. package/bin/generate-traceability-matrix.cjs +701 -0
  67. package/bin/generate-triage-bundle.cjs +300 -0
  68. package/bin/gh-account-rotate.cjs +34 -0
  69. package/bin/initialize-model-registry.cjs +105 -0
  70. package/bin/install-formal-tools.cjs +382 -0
  71. package/bin/install.js +2424 -0
  72. package/bin/isNumericThreshold.cjs +34 -0
  73. package/bin/issue-classifier.cjs +151 -0
  74. package/bin/levenshtein.cjs +74 -0
  75. package/bin/lint-formal-models.cjs +580 -0
  76. package/bin/load-baseline-requirements.cjs +275 -0
  77. package/bin/manage-agents-core.cjs +815 -0
  78. package/bin/migrate-formal-dir.cjs +172 -0
  79. package/bin/migrate-planning.cjs +206 -0
  80. package/bin/migrate-to-slots.cjs +255 -0
  81. package/bin/nForma.cjs +2726 -0
  82. package/bin/observe-config.cjs +353 -0
  83. package/bin/observe-debt-writer.cjs +140 -0
  84. package/bin/observe-handler-grafana.cjs +128 -0
  85. package/bin/observe-handler-internal.cjs +301 -0
  86. package/bin/observe-handler-logstash.cjs +153 -0
  87. package/bin/observe-handler-prometheus.cjs +185 -0
  88. package/bin/observe-handlers.cjs +436 -0
  89. package/bin/observe-registry.cjs +131 -0
  90. package/bin/observe-render.cjs +168 -0
  91. package/bin/planning-paths.cjs +167 -0
  92. package/bin/polyrepo.cjs +560 -0
  93. package/bin/prism-priority.cjs +153 -0
  94. package/bin/probe-quorum-slots.cjs +167 -0
  95. package/bin/promote-model.cjs +225 -0
  96. package/bin/propose-debug-invariants.cjs +165 -0
  97. package/bin/providers.json +392 -0
  98. package/bin/pty-proxy.py +129 -0
  99. package/bin/qgsd-solve.cjs +2477 -0
  100. package/bin/quorum-consensus-gate.cjs +238 -0
  101. package/bin/quorum-formal-context.cjs +183 -0
  102. package/bin/quorum-slot-dispatch.cjs +934 -0
  103. package/bin/read-policy.cjs +60 -0
  104. package/bin/requirement-map.cjs +63 -0
  105. package/bin/requirements-core.cjs +247 -0
  106. package/bin/resolve-cli.cjs +101 -0
  107. package/bin/review-mcp-logs.cjs +294 -0
  108. package/bin/run-account-manager-tlc.cjs +188 -0
  109. package/bin/run-account-pool-alloy.cjs +158 -0
  110. package/bin/run-alloy.cjs +153 -0
  111. package/bin/run-audit-alloy.cjs +187 -0
  112. package/bin/run-breaker-tlc.cjs +181 -0
  113. package/bin/run-formal-check.cjs +395 -0
  114. package/bin/run-formal-verify.cjs +701 -0
  115. package/bin/run-installer-alloy.cjs +188 -0
  116. package/bin/run-oauth-rotation-prism.cjs +132 -0
  117. package/bin/run-oscillation-tlc.cjs +202 -0
  118. package/bin/run-phase-tlc.cjs +228 -0
  119. package/bin/run-prism.cjs +446 -0
  120. package/bin/run-protocol-tlc.cjs +201 -0
  121. package/bin/run-quorum-composition-alloy.cjs +155 -0
  122. package/bin/run-sensitivity-sweep.cjs +231 -0
  123. package/bin/run-stop-hook-tlc.cjs +188 -0
  124. package/bin/run-tlc.cjs +467 -0
  125. package/bin/run-transcript-alloy.cjs +173 -0
  126. package/bin/run-uppaal.cjs +264 -0
  127. package/bin/secrets.cjs +134 -0
  128. package/bin/sensitivity-report.cjs +219 -0
  129. package/bin/sensitivity-sweep-feedback.cjs +194 -0
  130. package/bin/set-secret.cjs +29 -0
  131. package/bin/setup-telemetry-cron.sh +36 -0
  132. package/bin/sweepPtoF.cjs +63 -0
  133. package/bin/sync-baseline-requirements.cjs +290 -0
  134. package/bin/task-envelope.cjs +360 -0
  135. package/bin/telemetry-collector.cjs +229 -0
  136. package/bin/unified-mcp-server.mjs +735 -0
  137. package/bin/update-agents.cjs +369 -0
  138. package/bin/update-scoreboard.cjs +1134 -0
  139. package/bin/validate-debt-entry.cjs +207 -0
  140. package/bin/validate-invariant.cjs +419 -0
  141. package/bin/validate-memory.cjs +389 -0
  142. package/bin/validate-requirements-haiku.cjs +435 -0
  143. package/bin/validate-traces.cjs +438 -0
  144. package/bin/verify-formal-results.cjs +124 -0
  145. package/bin/verify-quorum-health.cjs +273 -0
  146. package/bin/write-check-result.cjs +106 -0
  147. package/bin/xstate-to-tla.cjs +483 -0
  148. package/bin/xstate-trace-walker.cjs +205 -0
  149. package/commands/qgsd/add-phase.md +43 -0
  150. package/commands/qgsd/add-requirement.md +24 -0
  151. package/commands/qgsd/add-todo.md +47 -0
  152. package/commands/qgsd/audit-milestone.md +37 -0
  153. package/commands/qgsd/check-todos.md +45 -0
  154. package/commands/qgsd/cleanup.md +18 -0
  155. package/commands/qgsd/close-formal-gaps.md +33 -0
  156. package/commands/qgsd/complete-milestone.md +136 -0
  157. package/commands/qgsd/debug.md +166 -0
  158. package/commands/qgsd/discuss-phase.md +83 -0
  159. package/commands/qgsd/execute-phase.md +117 -0
  160. package/commands/qgsd/fix-tests.md +27 -0
  161. package/commands/qgsd/formal-test-sync.md +32 -0
  162. package/commands/qgsd/health.md +22 -0
  163. package/commands/qgsd/help.md +22 -0
  164. package/commands/qgsd/insert-phase.md +32 -0
  165. package/commands/qgsd/join-discord.md +18 -0
  166. package/commands/qgsd/list-phase-assumptions.md +46 -0
  167. package/commands/qgsd/map-codebase.md +71 -0
  168. package/commands/qgsd/map-requirements.md +20 -0
  169. package/commands/qgsd/mcp-restart.md +176 -0
  170. package/commands/qgsd/mcp-set-model.md +134 -0
  171. package/commands/qgsd/mcp-setup.md +1371 -0
  172. package/commands/qgsd/mcp-status.md +274 -0
  173. package/commands/qgsd/mcp-update.md +238 -0
  174. package/commands/qgsd/new-milestone.md +44 -0
  175. package/commands/qgsd/new-project.md +42 -0
  176. package/commands/qgsd/observe.md +260 -0
  177. package/commands/qgsd/pause-work.md +38 -0
  178. package/commands/qgsd/plan-milestone-gaps.md +34 -0
  179. package/commands/qgsd/plan-phase.md +44 -0
  180. package/commands/qgsd/polyrepo.md +50 -0
  181. package/commands/qgsd/progress.md +24 -0
  182. package/commands/qgsd/queue.md +54 -0
  183. package/commands/qgsd/quick.md +133 -0
  184. package/commands/qgsd/quorum-test.md +275 -0
  185. package/commands/qgsd/quorum.md +707 -0
  186. package/commands/qgsd/reapply-patches.md +110 -0
  187. package/commands/qgsd/remove-phase.md +31 -0
  188. package/commands/qgsd/research-phase.md +189 -0
  189. package/commands/qgsd/resume-work.md +40 -0
  190. package/commands/qgsd/set-profile.md +34 -0
  191. package/commands/qgsd/settings.md +39 -0
  192. package/commands/qgsd/solve.md +565 -0
  193. package/commands/qgsd/sync-baselines.md +119 -0
  194. package/commands/qgsd/triage.md +233 -0
  195. package/commands/qgsd/update.md +37 -0
  196. package/commands/qgsd/verify-work.md +38 -0
  197. package/hooks/dist/config-loader.js +297 -0
  198. package/hooks/dist/conformance-schema.cjs +12 -0
  199. package/hooks/dist/gsd-context-monitor.js +64 -0
  200. package/hooks/dist/qgsd-check-update.js +62 -0
  201. package/hooks/dist/qgsd-circuit-breaker.js +682 -0
  202. package/hooks/dist/qgsd-precompact.js +156 -0
  203. package/hooks/dist/qgsd-prompt.js +653 -0
  204. package/hooks/dist/qgsd-session-start.js +122 -0
  205. package/hooks/dist/qgsd-slot-correlator.js +58 -0
  206. package/hooks/dist/qgsd-spec-regen.js +86 -0
  207. package/hooks/dist/qgsd-statusline.js +91 -0
  208. package/hooks/dist/qgsd-stop.js +553 -0
  209. package/hooks/dist/qgsd-token-collector.js +133 -0
  210. package/hooks/dist/unified-mcp-server.mjs +669 -0
  211. package/package.json +95 -0
  212. package/scripts/build-hooks.js +46 -0
  213. package/scripts/postinstall.js +48 -0
  214. package/scripts/secret-audit.sh +45 -0
  215. package/templates/qgsd.json +49 -0
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Dual-table renderer for /qgsd:observe
3
+ * Renders Issues table and Drifts table with error section
4
+ */
5
+
6
+ // Severity sort order (lower = higher priority)
7
+ const SEVERITY_ORDER = { error: 0, critical: 0, bug: 1, warning: 2, info: 3 };
8
+
9
+ /**
10
+ * Get sort order for a severity string
11
+ * @param {string} severity
12
+ * @returns {number}
13
+ */
14
+ function classifySeverity(severity) {
15
+ return SEVERITY_ORDER[severity] ?? 4;
16
+ }
17
+
18
+ /**
19
+ * Format age from ISO date to human-readable string
20
+ * @param {string} isoDate
21
+ * @returns {string}
22
+ */
23
+ function formatAge(isoDate) {
24
+ if (!isoDate) return '';
25
+ const diffMs = Date.now() - new Date(isoDate).getTime();
26
+ if (diffMs < 0) return 'future';
27
+ const minutes = Math.floor(diffMs / 60000);
28
+ if (minutes < 60) return `${minutes}m`;
29
+ const hours = Math.floor(minutes / 60);
30
+ if (hours < 24) return `${hours}h`;
31
+ const days = Math.floor(hours / 24);
32
+ return `${days}d`;
33
+ }
34
+
35
+ /**
36
+ * Truncate a string to maxLen, adding "..." if truncated
37
+ * @param {string} str
38
+ * @param {number} maxLen
39
+ * @returns {string}
40
+ */
41
+ function truncate(str, maxLen) {
42
+ if (!str) return '';
43
+ if (str.length <= maxLen) return str;
44
+ return str.slice(0, maxLen - 3) + '...';
45
+ }
46
+
47
+ /**
48
+ * Pad or truncate string to exact width
49
+ * @param {string} str
50
+ * @param {number} width
51
+ * @returns {string}
52
+ */
53
+ function pad(str, width) {
54
+ const s = String(str || '');
55
+ if (s.length >= width) return s.slice(0, width);
56
+ return s + ' '.repeat(width - s.length);
57
+ }
58
+
59
+ /**
60
+ * Render observe output with dual tables (Issues + Drifts) and error section
61
+ *
62
+ * @param {object[]} results - Array of handler results (standard schema, all resolved)
63
+ * @returns {string} Formatted output string
64
+ */
65
+ function renderObserveOutput(results) {
66
+ const lines = [];
67
+
68
+ // Separate successes and errors
69
+ const successes = results.filter(r => r.status === 'ok');
70
+ const errorResults = results.filter(r => r.status === 'error');
71
+ const sourceCount = results.length;
72
+
73
+ // Collect all issues
74
+ const allItems = [];
75
+ for (const r of successes) {
76
+ for (const issue of (r.issues || [])) {
77
+ allItems.push({ ...issue, source_label: issue.source_label || r.source_label });
78
+ }
79
+ }
80
+
81
+ // Split by issue_type
82
+ const issues = allItems.filter(item => item.issue_type !== 'drift');
83
+ const drifts = allItems.filter(item => item.issue_type === 'drift');
84
+
85
+ const totalIssues = issues.length;
86
+ const totalDrifts = drifts.length;
87
+
88
+ // Header
89
+ if (totalIssues === 0 && totalDrifts === 0 && errorResults.length === 0) {
90
+ lines.push('');
91
+ lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
92
+ lines.push(' QGSD > OBSERVE: All clear — no open issues found');
93
+ lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
94
+ lines.push(`Sources checked: ${sourceCount}`);
95
+ return lines.join('\n');
96
+ }
97
+
98
+ const failNote = errorResults.length > 0 ? `; ${errorResults.length} source(s) failed` : '';
99
+ lines.push('');
100
+ lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
101
+ lines.push(` QGSD > OBSERVE: ${totalIssues} issue(s), ${totalDrifts} drift(s) across ${sourceCount} source(s)${failNote}`);
102
+ lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
103
+
104
+ // Sort issues by severity then age (newest first)
105
+ issues.sort((a, b) => {
106
+ const sevCmp = classifySeverity(a.severity) - classifySeverity(b.severity);
107
+ if (sevCmp !== 0) return sevCmp;
108
+ return new Date(b.created_at || 0) - new Date(a.created_at || 0);
109
+ });
110
+
111
+ // Sort drifts by severity
112
+ drifts.sort((a, b) => {
113
+ return classifySeverity(a.severity) - classifySeverity(b.severity);
114
+ });
115
+
116
+ // Render Issues table
117
+ if (issues.length > 0) {
118
+ lines.push('');
119
+ lines.push('┌────────────────────────────── ISSUES ───────────────────────────────┐');
120
+ lines.push('│ # │ Title │ Source │ Sev │ Age │');
121
+ lines.push('├─────────────────────────────────────────────────────────────────────┤');
122
+
123
+ for (let i = 0; i < issues.length; i++) {
124
+ const issue = issues[i];
125
+ const num = String(i + 1).padStart(2, ' ');
126
+ const title = pad(truncate(issue.title, 40), 40);
127
+ const source = pad(truncate(issue.source_label || issue.source_type || '', 7), 7);
128
+ const sev = pad(truncate(issue.severity || 'info', 3), 3);
129
+ const age = pad(issue.age || formatAge(issue.created_at), 4);
130
+ lines.push(`│ ${num} │ ${title} │ ${source} │ ${sev} │ ${age} │`);
131
+ }
132
+
133
+ lines.push('└─────────────────────────────────────────────────────────────────────┘');
134
+ }
135
+
136
+ // Render Drifts table
137
+ if (drifts.length > 0) {
138
+ lines.push('');
139
+ lines.push('┌────────────────────────────── DRIFTS ───────────────────────────────┐');
140
+ lines.push('│ # │ Parameter │ Formal │ Actual │ Sev │');
141
+ lines.push('├─────────────────────────────────────────────────────────────────────┤');
142
+
143
+ for (let i = 0; i < drifts.length; i++) {
144
+ const drift = drifts[i];
145
+ const num = String(i + 1).padStart(2, ' ');
146
+ const param = pad(truncate(drift.formal_parameter_key || drift.title || '', 36), 36);
147
+ const formal = pad(truncate(String(drift.formal_value || ''), 6), 6);
148
+ const actual = pad(truncate(String(drift.actual_value || ''), 6), 6);
149
+ const sev = pad(truncate(drift.severity || 'info', 4), 4);
150
+ lines.push(`│ ${num} │ ${param} │ ${formal} │ ${actual} │ ${sev} │`);
151
+ }
152
+
153
+ lines.push('└─────────────────────────────────────────────────────────────────────┘');
154
+ }
155
+
156
+ // Render errors section
157
+ if (errorResults.length > 0) {
158
+ lines.push('');
159
+ lines.push(`Errors from sources (${errorResults.length} failed, did not block others):`);
160
+ for (const err of errorResults) {
161
+ lines.push(` ✗ ${err.source_label || err.source_type}: ${err.error || 'Unknown error'}`);
162
+ }
163
+ }
164
+
165
+ return lines.join('\n');
166
+ }
167
+
168
+ module.exports = { renderObserveOutput, classifySeverity, formatAge, truncate };
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * planning-paths.cjs — Centralized path resolver for .planning/ hierarchy.
6
+ *
7
+ * New layout (v0.27+):
8
+ * .planning/quorum/rounds/ quorum-rounds-session-*.jsonl
9
+ * .planning/quorum/correlations/ quorum-slot-corr-*.json
10
+ * .planning/telemetry/ conformance-events.jsonl, token-usage.jsonl
11
+ * .planning/milestones/ v*-MILESTONE-AUDIT.md, v*-INTEGRATION-*.{md,txt}
12
+ * .planning/archive/state-backups/ STATE.md.bak-*
13
+ * .planning/archive/designs/ dated docs, old roadmaps
14
+ *
15
+ * Legacy layout (pre-v0.27):
16
+ * All above files lived directly in .planning/
17
+ *
18
+ * API:
19
+ * resolve(root, type, params) → canonical (new) path
20
+ * resolveWithFallback(root, type, params) → new path if exists, else legacy path
21
+ * legacy(root, type, params) → legacy flat path
22
+ * needsMigration(root) → boolean
23
+ */
24
+
25
+ const fs = require('fs');
26
+ const path = require('path');
27
+
28
+ // ─── Path definitions ────────────────────────────────────────────────────────
29
+
30
+ const TYPES = {
31
+ // Quorum runtime (high-volume ephemeral)
32
+ 'quorum-rounds': {
33
+ canonical: (root, p) => path.join(root, '.planning', 'quorum', 'rounds', `quorum-rounds-${p.sessionId}.jsonl`),
34
+ legacy: (root, p) => path.join(root, '.planning', `quorum-rounds-${p.sessionId}.jsonl`),
35
+ },
36
+ 'quorum-correlation': {
37
+ canonical: (root, p) => path.join(root, '.planning', 'quorum', 'correlations', `quorum-slot-corr-${p.agentId}.json`),
38
+ legacy: (root, p) => path.join(root, '.planning', `quorum-slot-corr-${p.agentId}.json`),
39
+ },
40
+
41
+ // Quorum aggregate data
42
+ 'quorum-scoreboard': {
43
+ canonical: (root) => path.join(root, '.planning', 'quorum', 'scoreboard.json'),
44
+ legacy: (root) => path.join(root, '.planning', 'quorum-scoreboard.json'),
45
+ },
46
+ 'quorum-failures': {
47
+ canonical: (root) => path.join(root, '.planning', 'quorum', 'failures.json'),
48
+ legacy: (root) => path.join(root, '.planning', 'quorum-failures.json'),
49
+ },
50
+ 'quorum-debate': {
51
+ canonical: (root, p) => path.join(root, '.planning', 'quorum', 'debates', p.filename),
52
+ legacy: (root, p) => path.join(root, '.planning', 'debates', p.filename),
53
+ },
54
+
55
+ // Telemetry
56
+ 'conformance-events': {
57
+ canonical: (root) => path.join(root, '.planning', 'telemetry', 'conformance-events.jsonl'),
58
+ legacy: (root) => path.join(root, '.planning', 'conformance-events.jsonl'),
59
+ },
60
+ 'token-usage': {
61
+ canonical: (root) => path.join(root, '.planning', 'telemetry', 'token-usage.jsonl'),
62
+ legacy: (root) => path.join(root, '.planning', 'token-usage.jsonl'),
63
+ },
64
+
65
+ // Milestone artifacts (loose ones at root → milestones/)
66
+ 'milestone-audit': {
67
+ canonical: (root, p) => path.join(root, '.planning', 'milestones', `${p.version}-MILESTONE-AUDIT.md`),
68
+ legacy: (root, p) => path.join(root, '.planning', `${p.version}-MILESTONE-AUDIT.md`),
69
+ },
70
+ 'integration-check': {
71
+ canonical: (root, p) => path.join(root, '.planning', 'milestones', `${p.version}-INTEGRATION-CHECK.md`),
72
+ legacy: (root, p) => path.join(root, '.planning', `${p.version}-INTEGRATION-CHECK.md`),
73
+ },
74
+ 'integration-summary': {
75
+ canonical: (root, p) => path.join(root, '.planning', 'milestones', `${p.version}-INTEGRATION-SUMMARY.txt`),
76
+ legacy: (root, p) => path.join(root, '.planning', `${p.version}-INTEGRATION-SUMMARY.txt`),
77
+ },
78
+ 'integration-key-files': {
79
+ canonical: (root, p) => path.join(root, '.planning', 'milestones', `${p.version}-INTEGRATION-KEY-FILES.md`),
80
+ legacy: (root, p) => path.join(root, '.planning', `${p.version}-INTEGRATION-KEY-FILES.md`),
81
+ },
82
+ 'integration-report': {
83
+ canonical: (root, p) => path.join(root, '.planning', 'milestones', `${p.version}-INTEGRATION-REPORT.md`),
84
+ legacy: (root, p) => path.join(root, '.planning', `${p.version}-INTEGRATION-REPORT.md`),
85
+ },
86
+
87
+ // State backups
88
+ 'state-backup': {
89
+ canonical: (root, p) => path.join(root, '.planning', 'archive', 'state-backups', `STATE.md.bak-${p.timestamp}`),
90
+ legacy: (root, p) => path.join(root, '.planning', `STATE.md.bak-${p.timestamp}`),
91
+ },
92
+ };
93
+
94
+ // ─── Public API ──────────────────────────────────────────────────────────────
95
+
96
+ /**
97
+ * Returns the canonical (new) path for a given type.
98
+ * Ensures parent directory exists.
99
+ */
100
+ function resolve(root, type, params) {
101
+ const def = TYPES[type];
102
+ if (!def) throw new Error(`Unknown planning path type: ${type}`);
103
+ const p = def.canonical(root, params || {});
104
+ fs.mkdirSync(path.dirname(p), { recursive: true });
105
+ return p;
106
+ }
107
+
108
+ /**
109
+ * Returns the canonical path if the file exists there,
110
+ * otherwise falls back to the legacy path.
111
+ * Does NOT create directories — use for readers only.
112
+ */
113
+ function resolveWithFallback(root, type, params) {
114
+ const def = TYPES[type];
115
+ if (!def) throw new Error(`Unknown planning path type: ${type}`);
116
+ const canonical = def.canonical(root, params || {});
117
+ if (fs.existsSync(canonical)) return canonical;
118
+ const leg = def.legacy(root, params || {});
119
+ if (fs.existsSync(leg)) return leg;
120
+ // Neither exists — return canonical (caller will handle missing file)
121
+ return canonical;
122
+ }
123
+
124
+ /**
125
+ * Returns the legacy (flat) path.
126
+ */
127
+ function legacy(root, type, params) {
128
+ const def = TYPES[type];
129
+ if (!def) throw new Error(`Unknown planning path type: ${type}`);
130
+ return def.legacy(root, params || {});
131
+ }
132
+
133
+ /**
134
+ * Detect if the .planning/ directory has legacy flat layout.
135
+ * Returns true if any quorum-rounds-session or quorum-slot-corr files
136
+ * exist at the root level.
137
+ */
138
+ function needsMigration(root) {
139
+ const planDir = path.join(root, '.planning');
140
+ if (!fs.existsSync(planDir)) return false;
141
+ try {
142
+ const entries = fs.readdirSync(planDir);
143
+ return entries.some(e =>
144
+ e.startsWith('quorum-rounds-session-') ||
145
+ e.startsWith('quorum-slot-corr-') ||
146
+ e === 'conformance-events.jsonl' ||
147
+ e === 'token-usage.jsonl' ||
148
+ e === 'quorum-scoreboard.json' ||
149
+ e === 'quorum-failures.json' ||
150
+ e === 'debates' ||
151
+ /^v[\d.]+-MILESTONE-AUDIT\.md$/.test(e) ||
152
+ /^v[\d.]+-INTEGRATION-/.test(e) ||
153
+ /^STATE\.md\.bak-/.test(e)
154
+ );
155
+ } catch (_) {
156
+ return false;
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Returns all known type names.
162
+ */
163
+ function types() {
164
+ return Object.keys(TYPES);
165
+ }
166
+
167
+ module.exports = { resolve, resolveWithFallback, legacy, needsMigration, types, TYPES };