@nforma.ai/nforma 0.2.1 → 0.29.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 (193) hide show
  1. package/README.md +2 -2
  2. package/agents/{qgsd-codebase-mapper.md → nf-codebase-mapper.md} +1 -1
  3. package/agents/{qgsd-debugger.md → nf-debugger.md} +3 -3
  4. package/agents/{qgsd-executor.md → nf-executor.md} +14 -14
  5. package/agents/{qgsd-integration-checker.md → nf-integration-checker.md} +1 -1
  6. package/agents/{qgsd-phase-researcher.md → nf-phase-researcher.md} +6 -6
  7. package/agents/{qgsd-plan-checker.md → nf-plan-checker.md} +9 -9
  8. package/agents/{qgsd-planner.md → nf-planner.md} +9 -9
  9. package/agents/{qgsd-project-researcher.md → nf-project-researcher.md} +2 -2
  10. package/agents/{qgsd-quorum-orchestrator.md → nf-quorum-orchestrator.md} +33 -33
  11. package/agents/{qgsd-quorum-slot-worker.md → nf-quorum-slot-worker.md} +3 -3
  12. package/agents/{qgsd-quorum-synthesizer.md → nf-quorum-synthesizer.md} +3 -3
  13. package/agents/{qgsd-quorum-test-worker.md → nf-quorum-test-worker.md} +1 -1
  14. package/agents/{qgsd-quorum-worker.md → nf-quorum-worker.md} +6 -6
  15. package/agents/{qgsd-research-synthesizer.md → nf-research-synthesizer.md} +5 -5
  16. package/agents/{qgsd-roadmapper.md → nf-roadmapper.md} +3 -3
  17. package/agents/{qgsd-verifier.md → nf-verifier.md} +8 -8
  18. package/bin/accept-debug-invariant.cjs +2 -2
  19. package/bin/account-manager.cjs +10 -10
  20. package/bin/aggregate-requirements.cjs +1 -1
  21. package/bin/analyze-assumptions.cjs +3 -3
  22. package/bin/analyze-state-space.cjs +14 -14
  23. package/bin/assumption-register.cjs +146 -0
  24. package/bin/attribute-trace-divergence.cjs +1 -1
  25. package/bin/auth-drivers/gh-cli.cjs +1 -1
  26. package/bin/auth-drivers/pool.cjs +1 -1
  27. package/bin/autoClosePtoF.cjs +3 -3
  28. package/bin/budget-tracker.cjs +77 -0
  29. package/bin/build-layer-manifest.cjs +153 -0
  30. package/bin/call-quorum-slot.cjs +3 -3
  31. package/bin/ccr-secure-config.cjs +5 -5
  32. package/bin/check-bundled-sdks.cjs +1 -1
  33. package/bin/check-mcp-health.cjs +1 -1
  34. package/bin/check-provider-health.cjs +6 -6
  35. package/bin/check-spec-sync.cjs +26 -26
  36. package/bin/check-trace-schema-drift.cjs +5 -5
  37. package/bin/conformance-schema.cjs +2 -2
  38. package/bin/cross-layer-dashboard.cjs +297 -0
  39. package/bin/design-impact.cjs +377 -0
  40. package/bin/detect-coverage-gaps.cjs +7 -7
  41. package/bin/failure-mode-catalog.cjs +227 -0
  42. package/bin/failure-taxonomy.cjs +177 -0
  43. package/bin/formal-scope-scan.cjs +179 -0
  44. package/bin/gate-a-grounding.cjs +334 -0
  45. package/bin/gate-b-abstraction.cjs +243 -0
  46. package/bin/gate-c-validation.cjs +166 -0
  47. package/bin/generate-formal-specs.cjs +17 -17
  48. package/bin/generate-petri-net.cjs +3 -3
  49. package/bin/generate-tla-cfg.cjs +5 -5
  50. package/bin/git-heatmap.cjs +571 -0
  51. package/bin/harness-diagnostic.cjs +326 -0
  52. package/bin/hazard-model.cjs +261 -0
  53. package/bin/install-formal-tools.cjs +1 -1
  54. package/bin/install.js +184 -139
  55. package/bin/instrumentation-map.cjs +178 -0
  56. package/bin/invariant-catalog.cjs +437 -0
  57. package/bin/issue-classifier.cjs +2 -2
  58. package/bin/load-baseline-requirements.cjs +4 -4
  59. package/bin/manage-agents-core.cjs +32 -32
  60. package/bin/migrate-to-slots.cjs +39 -39
  61. package/bin/mismatch-register.cjs +217 -0
  62. package/bin/nForma.cjs +176 -81
  63. package/bin/{qgsd-solve.cjs → nf-solve.cjs} +327 -14
  64. package/bin/observe-config.cjs +8 -0
  65. package/bin/observe-debt-writer.cjs +1 -1
  66. package/bin/observe-handler-deps.cjs +356 -0
  67. package/bin/observe-handler-grafana.cjs +2 -17
  68. package/bin/observe-handler-internal.cjs +5 -5
  69. package/bin/observe-handler-logstash.cjs +2 -17
  70. package/bin/observe-handler-prometheus.cjs +2 -17
  71. package/bin/observe-handler-upstream.cjs +251 -0
  72. package/bin/observe-handlers.cjs +12 -33
  73. package/bin/observe-render.cjs +68 -22
  74. package/bin/observe-utils.cjs +37 -0
  75. package/bin/observed-fsm.cjs +324 -0
  76. package/bin/planning-paths.cjs +6 -0
  77. package/bin/polyrepo.cjs +1 -1
  78. package/bin/probe-quorum-slots.cjs +1 -1
  79. package/bin/promote-gate-maturity.cjs +274 -0
  80. package/bin/promote-model.cjs +1 -1
  81. package/bin/propose-debug-invariants.cjs +1 -1
  82. package/bin/quorum-cache.cjs +144 -0
  83. package/bin/quorum-consensus-gate.cjs +1 -1
  84. package/bin/quorum-preflight.cjs +89 -0
  85. package/bin/quorum-slot-dispatch.cjs +6 -6
  86. package/bin/requirements-core.cjs +1 -1
  87. package/bin/review-mcp-logs.cjs +1 -1
  88. package/bin/risk-heatmap.cjs +151 -0
  89. package/bin/run-account-manager-tlc.cjs +4 -4
  90. package/bin/run-account-pool-alloy.cjs +2 -2
  91. package/bin/run-alloy.cjs +2 -2
  92. package/bin/run-audit-alloy.cjs +2 -2
  93. package/bin/run-breaker-tlc.cjs +3 -3
  94. package/bin/run-formal-check.cjs +9 -9
  95. package/bin/run-formal-verify.cjs +30 -9
  96. package/bin/run-installer-alloy.cjs +2 -2
  97. package/bin/run-oscillation-tlc.cjs +4 -4
  98. package/bin/run-phase-tlc.cjs +1 -1
  99. package/bin/run-protocol-tlc.cjs +4 -4
  100. package/bin/run-quorum-composition-alloy.cjs +2 -2
  101. package/bin/run-sensitivity-sweep.cjs +2 -2
  102. package/bin/run-stop-hook-tlc.cjs +3 -3
  103. package/bin/run-tlc.cjs +21 -21
  104. package/bin/run-transcript-alloy.cjs +2 -2
  105. package/bin/secrets.cjs +5 -5
  106. package/bin/security-sweep.cjs +238 -0
  107. package/bin/sensitivity-report.cjs +3 -3
  108. package/bin/set-secret.cjs +5 -5
  109. package/bin/setup-telemetry-cron.sh +3 -3
  110. package/bin/stall-detector.cjs +126 -0
  111. package/bin/state-candidates.cjs +206 -0
  112. package/bin/sync-baseline-requirements.cjs +1 -1
  113. package/bin/telemetry-collector.cjs +1 -1
  114. package/bin/test-changed.cjs +111 -0
  115. package/bin/test-recipe-gen.cjs +250 -0
  116. package/bin/trace-corpus-stats.cjs +211 -0
  117. package/bin/unified-mcp-server.mjs +3 -3
  118. package/bin/update-scoreboard.cjs +1 -1
  119. package/bin/validate-memory.cjs +2 -2
  120. package/bin/validate-traces.cjs +10 -10
  121. package/bin/verify-quorum-health.cjs +66 -5
  122. package/bin/xstate-to-tla.cjs +4 -4
  123. package/bin/xstate-trace-walker.cjs +3 -3
  124. package/commands/{qgsd → nf}/add-phase.md +3 -3
  125. package/commands/{qgsd → nf}/add-requirement.md +3 -3
  126. package/commands/{qgsd → nf}/add-todo.md +3 -3
  127. package/commands/{qgsd → nf}/audit-milestone.md +4 -4
  128. package/commands/{qgsd → nf}/check-todos.md +3 -3
  129. package/commands/{qgsd → nf}/cleanup.md +3 -3
  130. package/commands/{qgsd → nf}/close-formal-gaps.md +2 -2
  131. package/commands/{qgsd → nf}/complete-milestone.md +9 -9
  132. package/commands/{qgsd → nf}/debug.md +9 -9
  133. package/commands/{qgsd → nf}/discuss-phase.md +3 -3
  134. package/commands/{qgsd → nf}/execute-phase.md +15 -15
  135. package/commands/{qgsd → nf}/fix-tests.md +3 -3
  136. package/commands/{qgsd → nf}/formal-test-sync.md +1 -1
  137. package/commands/{qgsd → nf}/health.md +3 -3
  138. package/commands/{qgsd → nf}/help.md +3 -3
  139. package/commands/{qgsd → nf}/insert-phase.md +3 -3
  140. package/commands/nf/join-discord.md +18 -0
  141. package/commands/{qgsd → nf}/list-phase-assumptions.md +2 -2
  142. package/commands/{qgsd → nf}/map-codebase.md +7 -7
  143. package/commands/{qgsd → nf}/map-requirements.md +3 -3
  144. package/commands/{qgsd → nf}/mcp-restart.md +3 -3
  145. package/commands/{qgsd → nf}/mcp-set-model.md +8 -8
  146. package/commands/{qgsd → nf}/mcp-setup.md +63 -63
  147. package/commands/{qgsd → nf}/mcp-status.md +3 -3
  148. package/commands/{qgsd → nf}/mcp-update.md +7 -7
  149. package/commands/{qgsd → nf}/new-milestone.md +8 -8
  150. package/commands/{qgsd → nf}/new-project.md +8 -8
  151. package/commands/{qgsd → nf}/observe.md +49 -16
  152. package/commands/{qgsd → nf}/pause-work.md +3 -3
  153. package/commands/{qgsd → nf}/plan-milestone-gaps.md +5 -5
  154. package/commands/{qgsd → nf}/plan-phase.md +6 -6
  155. package/commands/{qgsd → nf}/polyrepo.md +2 -2
  156. package/commands/{qgsd → nf}/progress.md +3 -3
  157. package/commands/{qgsd → nf}/queue.md +2 -2
  158. package/commands/{qgsd → nf}/quick.md +8 -8
  159. package/commands/{qgsd → nf}/quorum-test.md +10 -10
  160. package/commands/{qgsd → nf}/quorum.md +36 -86
  161. package/commands/{qgsd → nf}/reapply-patches.md +2 -2
  162. package/commands/{qgsd → nf}/remove-phase.md +3 -3
  163. package/commands/{qgsd → nf}/research-phase.md +12 -12
  164. package/commands/{qgsd → nf}/resume-work.md +3 -3
  165. package/commands/nf/review-requirements.md +31 -0
  166. package/commands/{qgsd → nf}/set-profile.md +3 -3
  167. package/commands/{qgsd → nf}/settings.md +6 -6
  168. package/commands/{qgsd → nf}/solve.md +35 -35
  169. package/commands/{qgsd → nf}/sync-baselines.md +4 -4
  170. package/commands/{qgsd → nf}/triage.md +10 -10
  171. package/commands/{qgsd → nf}/update.md +3 -3
  172. package/commands/{qgsd → nf}/verify-work.md +5 -5
  173. package/hooks/dist/config-loader.js +188 -32
  174. package/hooks/dist/conformance-schema.cjs +2 -2
  175. package/hooks/dist/gsd-context-monitor.js +118 -13
  176. package/hooks/dist/{qgsd-check-update.js → nf-check-update.js} +5 -5
  177. package/hooks/dist/{qgsd-circuit-breaker.js → nf-circuit-breaker.js} +35 -24
  178. package/hooks/dist/{qgsd-precompact.js → nf-precompact.js} +13 -13
  179. package/hooks/dist/{qgsd-prompt.js → nf-prompt.js} +110 -33
  180. package/hooks/dist/nf-session-start.js +185 -0
  181. package/hooks/dist/{qgsd-slot-correlator.js → nf-slot-correlator.js} +13 -5
  182. package/hooks/dist/{qgsd-spec-regen.js → nf-spec-regen.js} +17 -8
  183. package/hooks/dist/{qgsd-statusline.js → nf-statusline.js} +12 -3
  184. package/hooks/dist/{qgsd-stop.js → nf-stop.js} +152 -18
  185. package/hooks/dist/{qgsd-token-collector.js → nf-token-collector.js} +12 -4
  186. package/hooks/dist/unified-mcp-server.mjs +2 -2
  187. package/package.json +6 -4
  188. package/scripts/build-hooks.js +13 -6
  189. package/scripts/secret-audit.sh +1 -1
  190. package/scripts/verify-hooks-sync.cjs +90 -0
  191. package/templates/{qgsd.json → nf.json} +4 -4
  192. package/commands/qgsd/join-discord.md +0 -18
  193. package/hooks/dist/qgsd-session-start.js +0 -122
@@ -22,7 +22,7 @@ const REPORT_MD_PATH = process.env.SENSITIVITY_MD_PATH ||
22
22
 
23
23
  const PARAM_ANNOTATIONS = {
24
24
  MaxSize: {
25
- codePath: 'hooks/qgsd-prompt.js FAN_OUT_COUNT; .planning/formal/tla/MCsafety.cfg MaxSize',
25
+ codePath: 'hooks/nf-prompt.js FAN_OUT_COUNT; .planning/formal/tla/MCsafety.cfg MaxSize',
26
26
  testCases: [
27
27
  'Test quorum at N=2 boundary: set FAN_OUT_COUNT=2 in providers.json and run quorum round',
28
28
  'Test quorum at N=1 (no quorum): verify workflow rejects insufficient available slots',
@@ -44,7 +44,7 @@ const PARAM_ANNOTATIONS = {
44
44
  ],
45
45
  },
46
46
  MaxDeliberation: {
47
- codePath: '.planning/formal/tla/MCsafety.cfg MaxDeliberation=7; src/machines/qgsd-workflow.machine.ts MaxDeliberation guard',
47
+ codePath: '.planning/formal/tla/MCsafety.cfg MaxDeliberation=7; src/machines/nf-workflow.machine.ts MaxDeliberation guard',
48
48
  testCases: [
49
49
  'Test quorum workflow with max deliberation rounds reached — verify DECIDED fallback',
50
50
  'Test rapid-fire slot responses (all within 1 deliberation round)',
@@ -89,7 +89,7 @@ function parseNDJSON(filePath) {
89
89
  function generateReport(records) {
90
90
  const now = new Date().toISOString();
91
91
  const lines = [
92
- '# Sensitivity Report — QGSD v0.20',
92
+ '# Sensitivity Report — nForma v0.20',
93
93
  '',
94
94
  'Generated: ' + now,
95
95
  'Source: .planning/formal/sensitivity-report.ndjson (' + records.length + ' records)',
@@ -4,8 +4,8 @@
4
4
  * set-secret.cjs
5
5
  * Usage: node bin/set-secret.cjs <KEY_NAME> <value>
6
6
  *
7
- * Stores KEY_NAME=value in the OS keychain under service "qgsd",
8
- * then syncs all qgsd secrets into ~/.claude.json mcpServers env blocks.
7
+ * Stores KEY_NAME=value in the OS keychain under service "nforma",
8
+ * then syncs all nforma secrets into ~/.claude.json mcpServers env blocks.
9
9
  */
10
10
  const { set, syncToClaudeJson, SERVICE } = require('./secrets.cjs');
11
11
 
@@ -19,11 +19,11 @@ const value = valueParts.join(' ');
19
19
  (async () => {
20
20
  try {
21
21
  await set(SERVICE, keyName, value);
22
- console.log(`[qgsd] Stored ${keyName} in keychain (service: ${SERVICE})`);
22
+ console.log(`[nf] Stored ${keyName} in keychain (service: ${SERVICE})`);
23
23
  await syncToClaudeJson(SERVICE);
24
- console.log('[qgsd] Synced keychain secrets to ~/.claude.json');
24
+ console.log('[nf] Synced keychain secrets to ~/.claude.json');
25
25
  } catch (e) {
26
- console.error('[qgsd] Error:', e.message);
26
+ console.error('[nf] Error:', e.message);
27
27
  process.exit(1);
28
28
  }
29
29
  })();
@@ -26,11 +26,11 @@ if crontab -l 2>/dev/null | grep -q "telemetry-collector"; then
26
26
  fi
27
27
 
28
28
  # Install cron entry: top of every hour
29
- (crontab -l 2>/dev/null; echo "0 * * * * $CRON_CMD >> /tmp/qgsd-telemetry.log 2>&1") | crontab -
29
+ (crontab -l 2>/dev/null; echo "0 * * * * $CRON_CMD >> /tmp/nf-telemetry.log 2>&1") | crontab -
30
30
 
31
31
  echo "Telemetry cron installed."
32
32
 
33
33
  # Windows: use Task Scheduler. Create a Basic Task that runs:
34
- # node C:\path\to\qgsd\bin\telemetry-collector.cjs
35
- # followed by: node C:\path\to\qgsd\bin\issue-classifier.cjs
34
+ # node C:\path\to\nforma\bin\telemetry-collector.cjs
35
+ # followed by: node C:\path\to\nforma\bin\issue-classifier.cjs
36
36
  # Trigger: Daily, repeat every 1 hour indefinitely.
@@ -0,0 +1,126 @@
1
+ 'use strict';
2
+ // bin/stall-detector.cjs
3
+ // Stall detection for quorum slots — INFORMATIONAL ONLY, never blocks.
4
+ // Uses only node:fs, node:path, node:child_process. No external dependencies.
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const { spawnSync } = require('child_process');
9
+
10
+ /**
11
+ * Detect stalled quorum slots from quorum-failures.json.
12
+ * @param {string} cwd - Project working directory.
13
+ * @param {object} config - Config object (needs stall_detection section).
14
+ * @returns {Array} Array of { slot, consecutiveTimeouts, lastSeen }.
15
+ */
16
+ function detectStalledSlots(cwd, config) {
17
+ try {
18
+ let failuresPath;
19
+ try {
20
+ const planningPaths = require(path.join(__dirname, 'planning-paths.cjs'));
21
+ failuresPath = planningPaths.resolveWithFallback(cwd, 'quorum-failures');
22
+ } catch {
23
+ failuresPath = path.join(cwd, '.planning', 'quorum', 'failures.json');
24
+ }
25
+
26
+ if (!fs.existsSync(failuresPath)) return [];
27
+
28
+ const data = JSON.parse(fs.readFileSync(failuresPath, 'utf8'));
29
+ if (!Array.isArray(data) || data.length === 0) return [];
30
+
31
+ // Group records by slot
32
+ const bySlot = {};
33
+ for (const record of data) {
34
+ const slot = record.slot || record.slot_name || 'unknown';
35
+ if (!bySlot[slot]) bySlot[slot] = [];
36
+ bySlot[slot].push(record);
37
+ }
38
+
39
+ const results = [];
40
+ for (const [slot, records] of Object.entries(bySlot)) {
41
+ // Count consecutive TIMEOUT entries from the end
42
+ let consecutive = 0;
43
+ let lastSeen = null;
44
+ for (let i = records.length - 1; i >= 0; i--) {
45
+ const r = records[i];
46
+ const reason = (r.reason || r.type || '').toUpperCase();
47
+ if (reason === 'TIMEOUT') {
48
+ consecutive++;
49
+ if (!lastSeen) lastSeen = r.ts || r.timestamp || null;
50
+ } else {
51
+ break;
52
+ }
53
+ }
54
+ if (consecutive >= 1) {
55
+ results.push({ slot, consecutiveTimeouts: consecutive, lastSeen });
56
+ }
57
+ }
58
+
59
+ return results;
60
+ } catch {
61
+ return []; // Fail-open
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Determine if stall should be escalated to user.
67
+ * @param {Array} stalledSlots - Output from detectStalledSlots.
68
+ * @param {object} config - Config object.
69
+ * @param {string} cwd - Project working directory.
70
+ * @returns {object} Escalation result.
71
+ */
72
+ function shouldEscalate(stalledSlots, config, cwd) {
73
+ const stallCfg = (config && config.stall_detection) || {};
74
+ const threshold = stallCfg.consecutive_threshold || 2;
75
+
76
+ const filtered = stalledSlots.filter(s => s.consecutiveTimeouts >= threshold);
77
+
78
+ if (filtered.length === 0) {
79
+ return { escalate: false, reason: 'below_threshold', stalledSlots: [] };
80
+ }
81
+
82
+ // Check for recent commit activity
83
+ if (stallCfg.check_commits !== false) {
84
+ try {
85
+ const result = spawnSync('git', ['rev-list', '--count', 'HEAD', '--since=10 minutes ago'], {
86
+ cwd: cwd || process.cwd(),
87
+ timeout: 3000,
88
+ encoding: 'utf8',
89
+ });
90
+ const count = parseInt(result.stdout.trim(), 10);
91
+ if (count > 0) {
92
+ return { escalate: false, reason: 'commits_active', stalledSlots: filtered };
93
+ }
94
+ } catch {
95
+ // Fail-open: if git fails, proceed with escalation check
96
+ }
97
+ }
98
+
99
+ return { escalate: true, stalledSlots: filtered };
100
+ }
101
+
102
+ /**
103
+ * Format a structured stall report for additionalContext injection.
104
+ * @param {object} escalationResult - Output from shouldEscalate.
105
+ * @returns {object|null} Stall report or null.
106
+ */
107
+ function formatStallReport(escalationResult) {
108
+ if (!escalationResult || !escalationResult.escalate) return null;
109
+
110
+ const slotNames = escalationResult.stalledSlots.map(s => s.slot).join(', ');
111
+
112
+ return {
113
+ type: 'stall_report',
114
+ ts: new Date().toISOString(),
115
+ stalled_slots: escalationResult.stalledSlots.map(s => ({
116
+ slot: s.slot,
117
+ consecutive_timeouts: s.consecutiveTimeouts,
118
+ last_seen: s.lastSeen,
119
+ recommendation: 'Check provider health: node bin/check-mcp-health.cjs',
120
+ })),
121
+ recommendation: 'Stalled slots detected with no new commits. Consider checking provider status or adjusting quorum composition.',
122
+ message: `STALL DETECTED: ${escalationResult.stalledSlots.length} slot(s) stalled (${slotNames}). Run \`node bin/check-mcp-health.cjs\` to diagnose.`,
123
+ };
124
+ }
125
+
126
+ module.exports = { detectStalledSlots, shouldEscalate, formatStallReport };
@@ -0,0 +1,206 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // bin/state-candidates.cjs
4
+ // Mines conformance traces for unmodeled state candidates.
5
+ // Identifies unmapped actions and suggests missing state transitions.
6
+ //
7
+ // Requirement: EVID-04
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ const ROOT = process.env.PROJECT_ROOT || path.join(__dirname, '..');
13
+ const EVIDENCE_DIR = path.join(ROOT, '.planning', 'formal', 'evidence');
14
+ const VOCAB_PATH = path.join(EVIDENCE_DIR, 'event-vocabulary.json');
15
+ const OUTPUT_PATH = path.join(EVIDENCE_DIR, 'state-candidates.json');
16
+
17
+ const JSON_FLAG = process.argv.includes('--json');
18
+
19
+ // ── Main ────────────────────────────────────────────────────────────────────
20
+
21
+ function main() {
22
+ if (!fs.existsSync(EVIDENCE_DIR)) {
23
+ fs.mkdirSync(EVIDENCE_DIR, { recursive: true });
24
+ }
25
+
26
+ // Import mapToXStateEvent from validate-traces.cjs
27
+ const { mapToXStateEvent } = require('./validate-traces.cjs');
28
+
29
+ // Read conformance events using planning-paths
30
+ const pp = require('./planning-paths.cjs');
31
+ const eventsPath = pp.resolve(process.cwd(), 'conformance-events');
32
+
33
+ if (!fs.existsSync(eventsPath)) {
34
+ console.error(`Conformance events file not found: ${eventsPath}`);
35
+ process.exit(1);
36
+ }
37
+
38
+ const raw = fs.readFileSync(eventsPath, 'utf8');
39
+ const lines = raw.split('\n').filter(l => l.trim());
40
+ const events = [];
41
+ for (const line of lines) {
42
+ try {
43
+ events.push(JSON.parse(line));
44
+ } catch (_) {}
45
+ }
46
+
47
+ // Load vocabulary
48
+ const vocab = JSON.parse(fs.readFileSync(VOCAB_PATH, 'utf8'));
49
+ const vocabActions = new Set(Object.keys(vocab.vocabulary));
50
+
51
+ // Sort by timestamp
52
+ events.sort((a, b) => {
53
+ const tsA = new Date(a.ts || a.timestamp).getTime();
54
+ const tsB = new Date(b.ts || b.timestamp).getTime();
55
+ return tsA - tsB;
56
+ });
57
+
58
+ // Identify unmapped actions: action is "undefined", not in vocabulary, or has no action field
59
+ const unmappedClusters = {};
60
+ let totalUnmapped = 0;
61
+
62
+ for (let i = 0; i < events.length; i++) {
63
+ const event = events[i];
64
+ const action = event.action || event.type || 'undefined';
65
+
66
+ // Check if this action is unmapped
67
+ const isUnmapped = !vocabActions.has(action);
68
+
69
+ if (isUnmapped) {
70
+ totalUnmapped++;
71
+
72
+ if (!unmappedClusters[action]) {
73
+ unmappedClusters[action] = {
74
+ action,
75
+ count: 0,
76
+ timestamps: [],
77
+ context_before: {},
78
+ context_after: {},
79
+ };
80
+ }
81
+
82
+ const cluster = unmappedClusters[action];
83
+ cluster.count++;
84
+
85
+ // Sample timestamps (first 5)
86
+ if (cluster.timestamps.length < 5) {
87
+ cluster.timestamps.push(event.ts || event.timestamp);
88
+ }
89
+
90
+ // Context: what actions appear before/after
91
+ if (i > 0) {
92
+ const prevAction = events[i - 1].action || events[i - 1].type || 'undefined';
93
+ cluster.context_before[prevAction] = (cluster.context_before[prevAction] || 0) + 1;
94
+ }
95
+ if (i < events.length - 1) {
96
+ const nextAction = events[i + 1].action || events[i + 1].type || 'undefined';
97
+ cluster.context_after[nextAction] = (cluster.context_after[nextAction] || 0) + 1;
98
+ }
99
+ }
100
+ }
101
+
102
+ // Build deduplicated candidates
103
+ const candidates = Object.values(unmappedClusters).map(cluster => {
104
+ // Suggest state variable based on context
105
+ const topBefore = Object.entries(cluster.context_before)
106
+ .sort((a, b) => b[1] - a[1])[0];
107
+ const topAfter = Object.entries(cluster.context_after)
108
+ .sort((a, b) => b[1] - a[1])[0];
109
+
110
+ let suggestedState = 'unknown';
111
+ let confidence = 'low';
112
+
113
+ if (topBefore && topBefore[0] === 'quorum_start') {
114
+ suggestedState = 'quorum_sub_state';
115
+ confidence = 'medium';
116
+ } else if (topAfter && topAfter[0] === 'quorum_complete') {
117
+ suggestedState = 'quorum_pre_completion';
118
+ confidence = 'medium';
119
+ } else if (cluster.action === 'undefined' || cluster.action === 'quorum_fallback_t1_required') {
120
+ suggestedState = 'instrumentation_gap';
121
+ confidence = 'high';
122
+ }
123
+
124
+ const allTs = cluster.timestamps.map(t => new Date(t).getTime()).filter(t => !isNaN(t));
125
+
126
+ return {
127
+ action: cluster.action,
128
+ count: cluster.count,
129
+ first_seen: allTs.length > 0 ? new Date(Math.min(...allTs)).toISOString() : null,
130
+ last_seen: allTs.length > 0 ? new Date(Math.max(...allTs)).toISOString() : null,
131
+ sample_timestamps: cluster.timestamps.slice(0, 5),
132
+ context_before: cluster.context_before,
133
+ context_after: cluster.context_after,
134
+ suggested_state: suggestedState,
135
+ confidence,
136
+ };
137
+ });
138
+
139
+ // Sort by count descending
140
+ candidates.sort((a, b) => b.count - a.count);
141
+
142
+ // Identify missing transitions (sequences of known actions not in XState)
143
+ const transitionPairs = {};
144
+ for (let i = 0; i < events.length - 1; i++) {
145
+ const fromAction = events[i].action || 'undefined';
146
+ const toAction = events[i + 1].action || 'undefined';
147
+ if (vocabActions.has(fromAction) && vocabActions.has(toAction)) {
148
+ const key = `${fromAction}→${toAction}`;
149
+ transitionPairs[key] = (transitionPairs[key] || 0) + 1;
150
+ }
151
+ }
152
+
153
+ // Try mapping through XState to find unmodeled transitions
154
+ const missingTransitions = [];
155
+ for (const [key, count] of Object.entries(transitionPairs)) {
156
+ const [fromAction, toAction] = key.split('→');
157
+ // Check if XState maps both actions
158
+ try {
159
+ const fromEvent = mapToXStateEvent({ action: fromAction });
160
+ const toEvent = mapToXStateEvent({ action: toAction });
161
+ if (!fromEvent || !toEvent) {
162
+ missingTransitions.push({
163
+ from_action: fromAction,
164
+ to_action: toAction,
165
+ count,
166
+ note: `XState mapping missing: from=${fromEvent || 'null'}, to=${toEvent || 'null'}`,
167
+ });
168
+ }
169
+ } catch (_) {}
170
+ }
171
+
172
+ // Build result
173
+ const result = {
174
+ schema_version: '1',
175
+ generated: new Date().toISOString(),
176
+ total_unmapped_events: totalUnmapped,
177
+ candidates,
178
+ missing_transitions: missingTransitions,
179
+ summary: `${totalUnmapped} unmapped events found. ` +
180
+ `${candidates.length} candidate(s): ` +
181
+ candidates.slice(0, 3).map(c => `${c.action} (${c.count}x)`).join(', ') +
182
+ (candidates.length > 3 ? `, +${candidates.length - 3} more` : '') + '. ' +
183
+ `${missingTransitions.length} potentially missing transitions.`,
184
+ };
185
+
186
+ fs.writeFileSync(OUTPUT_PATH, JSON.stringify(result, null, 2) + '\n', 'utf8');
187
+
188
+ if (JSON_FLAG) {
189
+ console.log(JSON.stringify(result, null, 2));
190
+ } else {
191
+ console.log('State Candidates Generated');
192
+ console.log(` Unmapped events: ${totalUnmapped}`);
193
+ console.log(` Candidates: ${candidates.length}`);
194
+ for (const c of candidates.slice(0, 5)) {
195
+ console.log(` ${c.action}: ${c.count}x (suggested: ${c.suggested_state}, confidence: ${c.confidence})`);
196
+ }
197
+ console.log(` Missing transitions: ${missingTransitions.length}`);
198
+ }
199
+ }
200
+
201
+ // Export for testing
202
+ module.exports = { main };
203
+
204
+ if (require.main === module) {
205
+ main();
206
+ }
@@ -82,7 +82,7 @@ function _syncFromBaseline(baseline, projectRoot) {
82
82
  phase: 'baseline',
83
83
  status: 'Pending',
84
84
  provenance: {
85
- source_file: 'qgsd-baseline',
85
+ source_file: 'nf-baseline',
86
86
  milestone: 'baseline',
87
87
  },
88
88
  };
@@ -4,7 +4,7 @@
4
4
  /**
5
5
  * telemetry-collector.cjs
6
6
  *
7
- * Pure disk I/O telemetry collector for QGSD.
7
+ * Pure disk I/O telemetry collector for nForma.
8
8
  * Reads existing log sources, aggregates stats, writes .planning/telemetry/report.json.
9
9
  *
10
10
  * Sources:
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Run only tests affected by changed source files.
6
+ *
7
+ * Usage:
8
+ * node bin/test-changed.cjs # changes vs HEAD (staged + unstaged)
9
+ * node bin/test-changed.cjs --since=main # changes vs main branch
10
+ * node bin/test-changed.cjs --since=HEAD~3 # changes in last 3 commits
11
+ */
12
+
13
+ const { execFileSync, spawn } = require('child_process');
14
+ const path = require('path');
15
+ const fs = require('fs');
16
+
17
+ const ROOT = path.resolve(__dirname, '..');
18
+
19
+ // ── Parse args ──────────────────────────────────────────────────────────
20
+ const args = process.argv.slice(2);
21
+ const sinceFlag = args.find(a => a.startsWith('--since='));
22
+ const since = sinceFlag ? sinceFlag.split('=')[1] : null;
23
+ const dryRun = args.includes('--dry-run');
24
+ const verbose = args.includes('--verbose');
25
+
26
+ // ── 1. Get changed files ───────────────────────────────────────────────
27
+ function getChangedFiles() {
28
+ const files = new Set();
29
+
30
+ if (since) {
31
+ const committed = execFileSync('git', ['diff', '--name-only', `${since}...HEAD`], { cwd: ROOT, encoding: 'utf8' });
32
+ committed.trim().split('\n').filter(Boolean).forEach(f => files.add(f));
33
+ }
34
+
35
+ // Always include unstaged + staged changes
36
+ const unstaged = execFileSync('git', ['diff', '--name-only'], { cwd: ROOT, encoding: 'utf8' });
37
+ const staged = execFileSync('git', ['diff', '--name-only', '--cached'], { cwd: ROOT, encoding: 'utf8' });
38
+
39
+ unstaged.trim().split('\n').filter(Boolean).forEach(f => files.add(f));
40
+ staged.trim().split('\n').filter(Boolean).forEach(f => files.add(f));
41
+
42
+ return [...files];
43
+ }
44
+
45
+ // ── 2. Map source files → test files ────────────────────────────────────
46
+ function mapToTestFiles(changedFiles) {
47
+ const testFiles = new Set();
48
+
49
+ for (const file of changedFiles) {
50
+ // Skip non-JS files
51
+ if (!/\.(cjs|mjs|js)$/.test(file)) continue;
52
+
53
+ // If the file IS a test file, include it directly
54
+ if (/\.test\.(cjs|mjs|js)$/.test(file)) {
55
+ const abs = path.resolve(ROOT, file);
56
+ if (fs.existsSync(abs)) testFiles.add(abs);
57
+ continue;
58
+ }
59
+
60
+ // Map source → test: foo.cjs → foo.test.cjs
61
+ const ext = path.extname(file);
62
+ const base = file.slice(0, -ext.length);
63
+ const testPath = `${base}.test${ext}`;
64
+ const abs = path.resolve(ROOT, testPath);
65
+ if (fs.existsSync(abs)) {
66
+ testFiles.add(abs);
67
+ }
68
+
69
+ // Also check .test.cjs variant for .js sources
70
+ if (ext !== '.cjs') {
71
+ const altPath = `${base}.test.cjs`;
72
+ const altAbs = path.resolve(ROOT, altPath);
73
+ if (fs.existsSync(altAbs)) testFiles.add(altAbs);
74
+ }
75
+ }
76
+
77
+ return [...testFiles].sort();
78
+ }
79
+
80
+ // ── 3. Run ──────────────────────────────────────────────────────────────
81
+ const changed = getChangedFiles();
82
+ if (verbose) {
83
+ console.log(`Changed files (since ${since || 'working tree'}):`);
84
+ changed.forEach(f => console.log(` ${f}`));
85
+ console.log();
86
+ }
87
+
88
+ const tests = mapToTestFiles(changed);
89
+
90
+ if (tests.length === 0) {
91
+ console.log('No affected test files found.');
92
+ process.exit(0);
93
+ }
94
+
95
+ console.log(`Running ${tests.length} affected test(s):`);
96
+ tests.forEach(t => console.log(` ${path.relative(ROOT, t)}`));
97
+ console.log();
98
+
99
+ if (dryRun) {
100
+ console.log('(dry-run mode — skipping execution)');
101
+ process.exit(0);
102
+ }
103
+
104
+ const child = spawn('node', ['--test', ...tests], {
105
+ cwd: ROOT,
106
+ stdio: 'inherit',
107
+ });
108
+
109
+ child.on('close', (code) => {
110
+ process.exit(code ?? 1);
111
+ });