@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,60 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // bin/read-policy.cjs
4
+ // Reads and validates .planning/formal/policy.yaml using lightweight regex extraction.
5
+ // No external YAML parser required — policy.yaml uses flat key: value structure.
6
+ // Requirements: CALIB-01, CALIB-04
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ /**
12
+ * Read and validate .planning/formal/policy.yaml.
13
+ * Throws Error with descriptive message on missing file or missing required fields.
14
+ *
15
+ * @param {string} policyPath Absolute path to policy.yaml
16
+ * @returns {{ cold_start: { min_ci_runs: number, min_quorum_rounds: number, min_days: number },
17
+ * steady_state: { mode: string },
18
+ * conservative_priors: { tp_rate: number, unavail: number } }}
19
+ */
20
+ function readPolicy(policyPath) {
21
+ if (!fs.existsSync(policyPath)) {
22
+ throw new Error('[read-policy] Policy file not found: ' + policyPath);
23
+ }
24
+
25
+ const yaml = fs.readFileSync(policyPath, 'utf8');
26
+
27
+ // Extract flat key: value using regex (ignores YAML comments)
28
+ const extract = (key, asFloat) => {
29
+ const match = yaml.match(new RegExp('^\\s*' + key + ':\\s*([\\d.]+)', 'm'));
30
+ if (!match) {
31
+ throw new Error('[read-policy] Policy missing required key: ' + key);
32
+ }
33
+ return asFloat ? parseFloat(match[1]) : parseInt(match[1], 10);
34
+ };
35
+
36
+ const extractStr = (key) => {
37
+ const match = yaml.match(new RegExp('^\\s*' + key + ':\\s*["\']?([\\w]+)["\']?', 'm'));
38
+ if (!match) {
39
+ throw new Error('[read-policy] Policy missing required key: ' + key);
40
+ }
41
+ return match[1];
42
+ };
43
+
44
+ return {
45
+ cold_start: {
46
+ min_ci_runs: extract('min_ci_runs', false),
47
+ min_quorum_rounds: extract('min_quorum_rounds', false),
48
+ min_days: extract('min_days', true),
49
+ },
50
+ steady_state: {
51
+ mode: extractStr('mode'),
52
+ },
53
+ conservative_priors: {
54
+ tp_rate: extract('tp_rate', true),
55
+ unavail: extract('unavail', true),
56
+ },
57
+ };
58
+ }
59
+
60
+ module.exports = { readPolicy };
@@ -0,0 +1,63 @@
1
+ 'use strict';
2
+ // bin/requirement-map.cjs
3
+ // Centralized mapping from check_id (used by run-formal-verify.cjs steps) to requirement IDs.
4
+ // Single source of truth for SCHEMA-03: runners import this to populate requirement_ids in NDJSON output.
5
+ //
6
+ // Maintenance: when adding a new formal check or requirement, update this map.
7
+ // The check_id values match those in run-formal-verify.cjs STEPS array and individual runner CHECK_ID_MAP objects.
8
+
9
+ const CHECK_ID_TO_REQUIREMENTS = {
10
+ // ── TLA+ checks ──────────────────────────────────────
11
+ 'tla:quorum-safety': ['QUORUM-01', 'QUORUM-02', 'QUORUM-03', 'SAFE-01', 'SAFE-02', 'SAFE-03', 'LOOP-01'],
12
+ 'tla:quorum-liveness': ['QUORUM-04', 'RECV-01'],
13
+ 'tla:mcp-environment': ['MCPENV-01', 'MCPENV-02', 'MCPENV-03'],
14
+ 'tla:breaker': ['DETECT-01', 'DETECT-02', 'DETECT-03'],
15
+ 'tla:oscillation': ['DETECT-04', 'DETECT-05', 'DETECT-06'],
16
+ 'tla:convergence': ['ORES-01', 'ORES-02', 'ORES-03', 'ORES-04', 'ORES-05'],
17
+ 'tla:deliberation': ['PLAN-01', 'PLAN-02', 'SAFE-03', 'IMPR-01', 'LOOP-02'],
18
+ 'tla:prefilter': ['PLAN-03', 'PLAN-04', 'PLAN-05', 'PLAN-06', 'LOOP-03'],
19
+ 'tla:account-manager': ['CRED-01', 'CRED-02', 'CRED-03', 'CRED-04', 'CRED-05', 'CRED-06'],
20
+ 'tla:stop-hook': ['STOP-01', 'STOP-02', 'STOP-03', 'STOP-04', 'STOP-05', 'STOP-06', 'STOP-07', 'SPEC-01'],
21
+ 'tla:recruiting-safety': ['SLOT-02', 'SLOT-03', 'SLOT-04'],
22
+ 'tla:recruiting-liveness': ['SLOT-05'],
23
+ 'tla:tui-navigation': [],
24
+
25
+ // ── Alloy checks ─────────────────────────────────────
26
+ // check_id values match those emitted by individual runner scripts:
27
+ 'alloy:quorum-votes': ['QUORUM-02', 'SAFE-01', 'SAFE-04'],
28
+ 'alloy:quorum-composition': ['SPEC-03', 'COMP-01'],
29
+ 'alloy:scoreboard': ['SCBD-01', 'SCBD-02', 'SCBD-03', 'SCBD-04'],
30
+ 'alloy:availability': ['CALIB-01', 'CALIB-02', 'CALIB-03'],
31
+ 'alloy:transcript': ['STOP-08', 'STOP-09', 'STOP-10', 'STOP-11'],
32
+ 'alloy:install-scope': ['INST-01', 'INST-02', 'INST-03', 'INST-04', 'INST-05'],
33
+ 'alloy:taxonomy-safety': ['SCBD-05', 'SCBD-06', 'SCBD-07'],
34
+ 'alloy:account-pool': ['CRED-07', 'CRED-08', 'CRED-09', 'CRED-10', 'CRED-11'],
35
+
36
+ // ── PRISM checks ─────────────────────────────────────
37
+ 'prism:quorum': ['PRM-01', 'QUORUM-04', 'LOOP-01'],
38
+ 'prism:oauth-rotation': ['PRM-AM-01', 'CRED-12'],
39
+ 'prism:mcp-availability': ['MCPENV-04', 'FAIL-01'],
40
+
41
+ // ── UPPAAL checks ────────────────────────────────────
42
+ 'uppaal:quorum-races': ['UPPAAL-01', 'UPPAAL-02', 'UPPAAL-03'],
43
+
44
+ // ── CI enforcement checks ────────────────────────────
45
+ 'ci:trace-redaction': ['REDACT-01'],
46
+ 'ci:trace-schema-drift': ['DRIFT-01'],
47
+ 'ci:liveness-fairness-lint': ['LIVE-01', 'LIVE-02'],
48
+ };
49
+
50
+ /**
51
+ * Get requirement IDs for a given check_id.
52
+ * Returns empty array for unknown check_ids (fail-open).
53
+ *
54
+ * @param {string} checkId - The check_id (e.g., 'tla:quorum-safety')
55
+ * @returns {string[]} Array of requirement IDs
56
+ */
57
+ function getRequirementIds(checkId) {
58
+ // Return a copy to prevent callers from mutating the shared source-of-truth array.
59
+ const found = CHECK_ID_TO_REQUIREMENTS[checkId];
60
+ return found ? found.slice() : [];
61
+ }
62
+
63
+ module.exports = { CHECK_ID_TO_REQUIREMENTS, getRequirementIds };
@@ -0,0 +1,247 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Pure data functions for requirements management.
5
+ * No blessed dependency — all functions are testable in isolation.
6
+ * Consumers: bin/qgsd.cjs (blessed TUI)
7
+ *
8
+ * Data sources (project-relative paths via process.cwd()):
9
+ * .planning/formal/requirements.json — 210 requirements in frozen envelope
10
+ * .planning/formal/model-registry.json — formal models with requirement links
11
+ * .planning/formal/check-results.ndjson — check results (NDJSON)
12
+ */
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const { getRequirementIds } = require('./requirement-map.cjs');
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Data loaders
20
+ // ---------------------------------------------------------------------------
21
+
22
+ function readRequirementsJson(basePath) {
23
+ const p = path.join(basePath || process.cwd(), '.planning', 'formal', 'requirements.json');
24
+ if (!fs.existsSync(p)) return { envelope: null, requirements: [] };
25
+ try {
26
+ const raw = JSON.parse(fs.readFileSync(p, 'utf8'));
27
+ const reqs = raw.requirements || [];
28
+ const envelope = {
29
+ aggregated_at: raw.aggregated_at || null,
30
+ content_hash: raw.content_hash || null,
31
+ frozen_at: raw.frozen_at || null,
32
+ };
33
+ return { envelope, requirements: reqs };
34
+ } catch (_) {
35
+ return { envelope: null, requirements: [] };
36
+ }
37
+ }
38
+
39
+ function readModelRegistry(basePath) {
40
+ const p = path.join(basePath || process.cwd(), '.planning', 'formal', 'model-registry.json');
41
+ if (!fs.existsSync(p)) return { version: null, last_sync: null, models: {} };
42
+ try {
43
+ const raw = JSON.parse(fs.readFileSync(p, 'utf8'));
44
+ return {
45
+ version: raw.version || null,
46
+ last_sync: raw.last_sync || null,
47
+ models: raw.models || {},
48
+ };
49
+ } catch (_) {
50
+ return { version: null, last_sync: null, models: {} };
51
+ }
52
+ }
53
+
54
+ function readCheckResults(basePath) {
55
+ const p = path.join(basePath || process.cwd(), '.planning', 'formal', 'check-results.ndjson');
56
+ if (!fs.existsSync(p)) return [];
57
+ try {
58
+ return fs.readFileSync(p, 'utf8')
59
+ .split('\n')
60
+ .filter(Boolean)
61
+ .map(line => { try { return JSON.parse(line); } catch (_) { return null; } })
62
+ .filter(Boolean);
63
+ } catch (_) {
64
+ return [];
65
+ }
66
+ }
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // Computation
70
+ // ---------------------------------------------------------------------------
71
+
72
+ function computeCoverage(requirements, registry, checkResults) {
73
+ const total = requirements.length;
74
+
75
+ // By status
76
+ const byStatus = {};
77
+ for (const r of requirements) {
78
+ const s = r.status || 'Unknown';
79
+ byStatus[s] = (byStatus[s] || 0) + 1;
80
+ }
81
+
82
+ // By category
83
+ const byCategory = {};
84
+ for (const r of requirements) {
85
+ const c = r.category || 'Uncategorized';
86
+ if (!byCategory[c]) byCategory[c] = { total: 0, complete: 0 };
87
+ byCategory[c].total++;
88
+ if (r.status === 'Complete') byCategory[c].complete++;
89
+ }
90
+
91
+ // Formal model coverage: which requirements have at least one formal model?
92
+ const reqsWithModels = new Set();
93
+ const models = (registry && registry.models) || {};
94
+ for (const entry of Object.values(models)) {
95
+ for (const reqId of (entry.requirements || [])) {
96
+ reqsWithModels.add(reqId);
97
+ }
98
+ }
99
+ // Also include requirements with direct formal_models field (SCHEMA-04)
100
+ for (const r of requirements) {
101
+ if (Array.isArray(r.formal_models) && r.formal_models.length > 0) {
102
+ reqsWithModels.add(r.id);
103
+ }
104
+ }
105
+ const withFormalModels = requirements.filter(r => reqsWithModels.has(r.id)).length;
106
+ const totalModels = Object.keys(models).length;
107
+
108
+ // Check results summary
109
+ const checksByResult = {};
110
+ for (const cr of (checkResults || [])) {
111
+ const res = cr.result || 'unknown';
112
+ checksByResult[res] = (checksByResult[res] || 0) + 1;
113
+ }
114
+
115
+ // Requirements with at least one check result (via requirement_ids or requirement-map)
116
+ const reqsWithChecks = new Set();
117
+ for (const cr of (checkResults || [])) {
118
+ // Direct requirement_ids on the check result
119
+ if (Array.isArray(cr.requirement_ids)) {
120
+ for (const rid of cr.requirement_ids) reqsWithChecks.add(rid);
121
+ }
122
+ // Reverse lookup via requirement-map
123
+ if (cr.check_id) {
124
+ for (const rid of getRequirementIds(cr.check_id)) reqsWithChecks.add(rid);
125
+ }
126
+ }
127
+ const withCheckResults = requirements.filter(r => reqsWithChecks.has(r.id)).length;
128
+
129
+ return {
130
+ total,
131
+ byStatus,
132
+ byCategory,
133
+ withFormalModels,
134
+ withCheckResults,
135
+ totalModels,
136
+ checksByResult,
137
+ };
138
+ }
139
+
140
+ function buildTraceability(reqId, requirements, registry, checkResults) {
141
+ const requirement = requirements.find(r => r.id === reqId);
142
+ if (!requirement) return null;
143
+
144
+ // Forward: find formal models that list this requirement
145
+ const formalModels = [];
146
+ const models = (registry && registry.models) || {};
147
+ for (const [modelPath, entry] of Object.entries(models)) {
148
+ if ((entry.requirements || []).includes(reqId)) {
149
+ formalModels.push({
150
+ path: modelPath,
151
+ description: entry.description || '',
152
+ version: entry.version || null,
153
+ });
154
+ }
155
+ }
156
+ // Also include models listed in requirement's own formal_models field (SCHEMA-04)
157
+ if (Array.isArray(requirement.formal_models)) {
158
+ for (const modelPath of requirement.formal_models) {
159
+ // Deduplicate: skip if already found via registry
160
+ if (!formalModels.some(fm => fm.path === modelPath)) {
161
+ // Try to get description from registry if available
162
+ const registryEntry = models[modelPath];
163
+ formalModels.push({
164
+ path: modelPath,
165
+ description: (registryEntry && registryEntry.description) || '',
166
+ version: (registryEntry && registryEntry.version) || null,
167
+ });
168
+ }
169
+ }
170
+ }
171
+
172
+ // Reverse: find check results linked to this requirement
173
+ const matchingChecks = [];
174
+ const unmappedCheckIds = [];
175
+ for (const cr of (checkResults || [])) {
176
+ // Direct match via requirement_ids field
177
+ const directIds = Array.isArray(cr.requirement_ids) ? cr.requirement_ids : [];
178
+ // Reverse lookup via requirement-map
179
+ const mapIds = cr.check_id ? getRequirementIds(cr.check_id) : [];
180
+ const allIds = new Set([...directIds, ...mapIds]);
181
+
182
+ if (allIds.has(reqId)) {
183
+ matchingChecks.push(cr);
184
+ }
185
+ }
186
+
187
+ // Find check_ids from requirement-map that link to this req but have no results
188
+ const { CHECK_ID_TO_REQUIREMENTS } = require('./requirement-map.cjs');
189
+ for (const [checkId, reqIds] of Object.entries(CHECK_ID_TO_REQUIREMENTS)) {
190
+ if (reqIds.includes(reqId)) {
191
+ const hasResult = (checkResults || []).some(cr => cr.check_id === checkId);
192
+ if (!hasResult) unmappedCheckIds.push(checkId);
193
+ }
194
+ }
195
+
196
+ return {
197
+ requirement,
198
+ formalModels,
199
+ checkResults: matchingChecks,
200
+ unmappedCheckIds,
201
+ };
202
+ }
203
+
204
+ function filterRequirements(requirements, filters) {
205
+ filters = filters || {};
206
+ let result = requirements;
207
+
208
+ if (filters.category) {
209
+ result = result.filter(r => (r.category || 'Uncategorized') === filters.category);
210
+ }
211
+ if (filters.status) {
212
+ result = result.filter(r => (r.status || 'Unknown') === filters.status);
213
+ }
214
+ if (filters.search) {
215
+ const s = filters.search.toLowerCase();
216
+ result = result.filter(r =>
217
+ (r.id || '').toLowerCase().includes(s) ||
218
+ (r.text || '').toLowerCase().includes(s)
219
+ );
220
+ }
221
+
222
+ return result;
223
+ }
224
+
225
+ function getUniqueCategories(requirements) {
226
+ const cats = new Set();
227
+ for (const r of requirements) {
228
+ cats.add(r.category || 'Uncategorized');
229
+ }
230
+ return [...cats].sort();
231
+ }
232
+
233
+ // ---------------------------------------------------------------------------
234
+ // Exports
235
+ // ---------------------------------------------------------------------------
236
+
237
+ module.exports = {
238
+ readRequirementsJson,
239
+ readModelRegistry,
240
+ readCheckResults,
241
+ computeCoverage,
242
+ buildTraceability,
243
+ filterRequirements,
244
+ getUniqueCategories,
245
+ };
246
+
247
+ module.exports._pure = module.exports;
@@ -0,0 +1,101 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * resolve-cli.cjs — CLI path resolution utility
5
+ *
6
+ * Resolves a bare CLI name to its full executable path using a priority-ordered search:
7
+ * 1. which <name> — system PATH lookup
8
+ * 2. Homebrew prefixes — /opt/homebrew/bin, /usr/local/bin
9
+ * 3. npm global bin — derived from `npm root -g`
10
+ * 4. Common system paths — /usr/bin, /usr/local/bin
11
+ * 5. Bare fallback — return name unchanged (let OS resolve at spawn time)
12
+ *
13
+ * Never throws. Always returns a non-empty string.
14
+ */
15
+
16
+ const { spawnSync } = require('child_process');
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+
20
+ /**
21
+ * Resolve a CLI name to its full executable path.
22
+ * @param {string} name - bare name like "codex", "gemini", "opencode"
23
+ * @returns {string} full path like "/opt/homebrew/bin/codex" or bare name if not found
24
+ */
25
+ function resolveCli(name) {
26
+ if (!name || typeof name !== 'string') return name || '';
27
+
28
+ // 1. which <name>
29
+ try {
30
+ const result = spawnSync('which', [name], { encoding: 'utf8', timeout: 3000 });
31
+ if (result.status === 0 && result.stdout) {
32
+ const found = result.stdout.trim();
33
+ if (found && found.length > 0) {
34
+ return found;
35
+ }
36
+ }
37
+ } catch (_) {
38
+ // which failed — continue to next strategy
39
+ }
40
+
41
+ // 2. Known Homebrew prefixes
42
+ const homebrewPrefixes = ['/opt/homebrew/bin', '/usr/local/bin'];
43
+ for (const prefix of homebrewPrefixes) {
44
+ const candidate = path.join(prefix, name);
45
+ try {
46
+ if (fs.existsSync(candidate)) {
47
+ return candidate;
48
+ }
49
+ } catch (_) {
50
+ // fs error — continue
51
+ }
52
+ }
53
+
54
+ // 3. npm global bin: npm root -g -> ../bin/<name>
55
+ try {
56
+ const npmResult = spawnSync('npm', ['root', '-g'], { encoding: 'utf8', timeout: 5000 });
57
+ if (npmResult.status === 0 && npmResult.stdout) {
58
+ const npmRoot = npmResult.stdout.trim();
59
+ if (npmRoot) {
60
+ const candidate = path.join(npmRoot, '..', 'bin', name);
61
+ if (fs.existsSync(candidate)) {
62
+ return candidate;
63
+ }
64
+ }
65
+ }
66
+ } catch (_) {
67
+ // npm failed — continue
68
+ }
69
+
70
+ // 4. Common system paths (deduplicated — /usr/local/bin was checked in step 2)
71
+ const systemPaths = ['/usr/bin'];
72
+ for (const dir of systemPaths) {
73
+ const candidate = path.join(dir, name);
74
+ try {
75
+ if (fs.existsSync(candidate)) {
76
+ return candidate;
77
+ }
78
+ } catch (_) {
79
+ // continue
80
+ }
81
+ }
82
+
83
+ // 5. Fallback: return bare name (let OS resolve at spawn time)
84
+ return name;
85
+ }
86
+
87
+ module.exports = { resolveCli };
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // Standalone CLI interface
91
+ // ---------------------------------------------------------------------------
92
+
93
+ if (require.main === module) {
94
+ const name = process.argv[2];
95
+ if (!name) {
96
+ console.error('Usage: node bin/resolve-cli.cjs <name>');
97
+ process.exit(1);
98
+ }
99
+ const resolved = resolveCli(name);
100
+ console.log(resolved);
101
+ }