@nforma.ai/nforma 0.2.1 → 0.28.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 (201) 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-slot-dispatch.cjs +6 -6
  85. package/bin/requirements-core.cjs +1 -1
  86. package/bin/review-mcp-logs.cjs +1 -1
  87. package/bin/risk-heatmap.cjs +151 -0
  88. package/bin/run-account-manager-tlc.cjs +4 -4
  89. package/bin/run-account-pool-alloy.cjs +2 -2
  90. package/bin/run-alloy.cjs +2 -2
  91. package/bin/run-audit-alloy.cjs +2 -2
  92. package/bin/run-breaker-tlc.cjs +3 -3
  93. package/bin/run-formal-check.cjs +9 -9
  94. package/bin/run-formal-verify.cjs +30 -9
  95. package/bin/run-installer-alloy.cjs +2 -2
  96. package/bin/run-oscillation-tlc.cjs +4 -4
  97. package/bin/run-phase-tlc.cjs +1 -1
  98. package/bin/run-protocol-tlc.cjs +4 -4
  99. package/bin/run-quorum-composition-alloy.cjs +2 -2
  100. package/bin/run-sensitivity-sweep.cjs +2 -2
  101. package/bin/run-stop-hook-tlc.cjs +3 -3
  102. package/bin/run-tlc.cjs +21 -21
  103. package/bin/run-transcript-alloy.cjs +2 -2
  104. package/bin/secrets.cjs +5 -5
  105. package/bin/security-sweep.cjs +238 -0
  106. package/bin/sensitivity-report.cjs +3 -3
  107. package/bin/set-secret.cjs +5 -5
  108. package/bin/setup-telemetry-cron.sh +3 -3
  109. package/bin/stall-detector.cjs +126 -0
  110. package/bin/state-candidates.cjs +206 -0
  111. package/bin/sync-baseline-requirements.cjs +1 -1
  112. package/bin/telemetry-collector.cjs +1 -1
  113. package/bin/test-changed.cjs +111 -0
  114. package/bin/test-recipe-gen.cjs +250 -0
  115. package/bin/trace-corpus-stats.cjs +211 -0
  116. package/bin/unified-mcp-server.mjs +3 -3
  117. package/bin/update-scoreboard.cjs +1 -1
  118. package/bin/validate-memory.cjs +2 -2
  119. package/bin/validate-traces.cjs +10 -10
  120. package/bin/verify-quorum-health.cjs +66 -5
  121. package/bin/xstate-to-tla.cjs +4 -4
  122. package/bin/xstate-trace-walker.cjs +3 -3
  123. package/commands/{qgsd → nf}/add-phase.md +3 -3
  124. package/commands/{qgsd → nf}/add-requirement.md +3 -3
  125. package/commands/{qgsd → nf}/add-todo.md +3 -3
  126. package/commands/{qgsd → nf}/audit-milestone.md +4 -4
  127. package/commands/{qgsd → nf}/check-todos.md +3 -3
  128. package/commands/{qgsd → nf}/cleanup.md +3 -3
  129. package/commands/{qgsd → nf}/close-formal-gaps.md +2 -2
  130. package/commands/{qgsd → nf}/complete-milestone.md +9 -9
  131. package/commands/{qgsd → nf}/debug.md +9 -9
  132. package/commands/{qgsd → nf}/discuss-phase.md +3 -3
  133. package/commands/{qgsd → nf}/execute-phase.md +15 -15
  134. package/commands/{qgsd → nf}/fix-tests.md +3 -3
  135. package/commands/{qgsd → nf}/formal-test-sync.md +1 -1
  136. package/commands/{qgsd → nf}/health.md +3 -3
  137. package/commands/{qgsd → nf}/help.md +3 -3
  138. package/commands/{qgsd → nf}/insert-phase.md +3 -3
  139. package/commands/nf/join-discord.md +18 -0
  140. package/commands/{qgsd → nf}/list-phase-assumptions.md +2 -2
  141. package/commands/{qgsd → nf}/map-codebase.md +7 -7
  142. package/commands/{qgsd → nf}/map-requirements.md +3 -3
  143. package/commands/{qgsd → nf}/mcp-restart.md +3 -3
  144. package/commands/{qgsd → nf}/mcp-set-model.md +8 -8
  145. package/commands/{qgsd → nf}/mcp-setup.md +63 -63
  146. package/commands/{qgsd → nf}/mcp-status.md +3 -3
  147. package/commands/{qgsd → nf}/mcp-update.md +7 -7
  148. package/commands/{qgsd → nf}/new-milestone.md +8 -8
  149. package/commands/{qgsd → nf}/new-project.md +8 -8
  150. package/commands/{qgsd → nf}/observe.md +49 -16
  151. package/commands/{qgsd → nf}/pause-work.md +3 -3
  152. package/commands/{qgsd → nf}/plan-milestone-gaps.md +5 -5
  153. package/commands/{qgsd → nf}/plan-phase.md +6 -6
  154. package/commands/{qgsd → nf}/polyrepo.md +2 -2
  155. package/commands/{qgsd → nf}/progress.md +3 -3
  156. package/commands/{qgsd → nf}/queue.md +2 -2
  157. package/commands/{qgsd → nf}/quick.md +8 -8
  158. package/commands/{qgsd → nf}/quorum-test.md +10 -10
  159. package/commands/{qgsd → nf}/quorum.md +40 -40
  160. package/commands/{qgsd → nf}/reapply-patches.md +2 -2
  161. package/commands/{qgsd → nf}/remove-phase.md +3 -3
  162. package/commands/{qgsd → nf}/research-phase.md +12 -12
  163. package/commands/{qgsd → nf}/resume-work.md +3 -3
  164. package/commands/nf/review-requirements.md +31 -0
  165. package/commands/{qgsd → nf}/set-profile.md +3 -3
  166. package/commands/{qgsd → nf}/settings.md +6 -6
  167. package/commands/{qgsd → nf}/solve.md +35 -35
  168. package/commands/{qgsd → nf}/sync-baselines.md +4 -4
  169. package/commands/{qgsd → nf}/triage.md +10 -10
  170. package/commands/{qgsd → nf}/update.md +3 -3
  171. package/commands/{qgsd → nf}/verify-work.md +5 -5
  172. package/hooks/dist/config-loader.js +188 -32
  173. package/hooks/dist/conformance-schema.cjs +2 -2
  174. package/hooks/dist/gsd-context-monitor.js +118 -13
  175. package/hooks/dist/{qgsd-check-update.js → nf-check-update.js} +5 -5
  176. package/hooks/dist/{qgsd-circuit-breaker.js → nf-circuit-breaker.js} +35 -24
  177. package/hooks/dist/nf-circuit-breaker.test.js +1002 -0
  178. package/hooks/dist/{qgsd-precompact.js → nf-precompact.js} +13 -13
  179. package/hooks/dist/nf-precompact.test.js +227 -0
  180. package/hooks/dist/{qgsd-prompt.js → nf-prompt.js} +110 -33
  181. package/hooks/dist/nf-prompt.test.js +698 -0
  182. package/hooks/dist/nf-session-start.js +185 -0
  183. package/hooks/dist/nf-session-start.test.js +354 -0
  184. package/hooks/dist/{qgsd-slot-correlator.js → nf-slot-correlator.js} +13 -5
  185. package/hooks/dist/nf-slot-correlator.test.js +85 -0
  186. package/hooks/dist/{qgsd-spec-regen.js → nf-spec-regen.js} +17 -8
  187. package/hooks/dist/nf-spec-regen.test.js +73 -0
  188. package/hooks/dist/{qgsd-statusline.js → nf-statusline.js} +12 -3
  189. package/hooks/dist/nf-statusline.test.js +157 -0
  190. package/hooks/dist/{qgsd-stop.js → nf-stop.js} +152 -18
  191. package/hooks/dist/nf-stop.test.js +1388 -0
  192. package/hooks/dist/{qgsd-token-collector.js → nf-token-collector.js} +12 -4
  193. package/hooks/dist/nf-token-collector.test.js +262 -0
  194. package/hooks/dist/unified-mcp-server.mjs +2 -2
  195. package/package.json +4 -4
  196. package/scripts/build-hooks.js +13 -6
  197. package/scripts/secret-audit.sh +1 -1
  198. package/scripts/verify-hooks-sync.cjs +90 -0
  199. package/templates/{qgsd.json → nf.json} +4 -4
  200. package/commands/qgsd/join-discord.md +0 -18
  201. package/hooks/dist/qgsd-session-start.js +0 -122
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // bin/instrumentation-map.cjs
4
+ // Scans hooks for conformance event emission points and maps to state variables.
5
+ // Validates discovered actions against event-vocabulary.json.
6
+ //
7
+ // Requirement: EVID-01
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, 'instrumentation-map.json');
16
+
17
+ const JSON_FLAG = process.argv.includes('--json');
18
+
19
+ // ── Hook scanning ───────────────────────────────────────────────────────────
20
+
21
+ const HOOK_FILES = [
22
+ 'hooks/nf-prompt.js',
23
+ 'hooks/nf-stop.js',
24
+ 'hooks/nf-circuit-breaker.js',
25
+ ];
26
+
27
+ // Also scan observe handlers
28
+ const OBSERVE_GLOB = 'bin/observe-handler-';
29
+
30
+ /**
31
+ * Scan a file for conformance event emission points.
32
+ * Looks for appendFileSync calls near conformance-events and extracts action types.
33
+ */
34
+ function scanFile(filePath) {
35
+ const absPath = path.join(ROOT, filePath);
36
+ if (!fs.existsSync(absPath)) return [];
37
+
38
+ const lines = fs.readFileSync(absPath, 'utf8').split('\n');
39
+ const emissionPoints = [];
40
+
41
+ for (let i = 0; i < lines.length; i++) {
42
+ const line = lines[i];
43
+
44
+ // Look for action: 'something' patterns (conformance event construction)
45
+ const actionMatch = line.match(/action:\s+['"]([^'"]+)['"]/);
46
+ if (actionMatch) {
47
+ const action = actionMatch[1];
48
+
49
+ // Look for state variable context in nearby lines (within 10 lines)
50
+ const contextLines = lines.slice(Math.max(0, i - 10), Math.min(lines.length, i + 10)).join('\n');
51
+ const stateVars = [];
52
+
53
+ // Extract state-related variables
54
+ if (/from_state|fromState|from:/i.test(contextLines)) stateVars.push('from_state');
55
+ if (/to_state|toState|to:/i.test(contextLines)) stateVars.push('to_state');
56
+ if (/verdict|decision/i.test(contextLines)) stateVars.push('verdict');
57
+ if (/fanOut|fan_out|quorumSize/i.test(contextLines)) stateVars.push('quorum_size');
58
+ if (/oscillation|breaker/i.test(contextLines)) stateVars.push('breaker_state');
59
+
60
+ emissionPoints.push({
61
+ file: filePath,
62
+ line_number: i + 1,
63
+ action,
64
+ state_variables: stateVars,
65
+ vocabulary_match: null, // filled later
66
+ xstate_event: null, // filled later
67
+ });
68
+ }
69
+
70
+ // Also detect type: 'quorum_fallback_*' patterns
71
+ const typeMatch = line.match(/type:\s+['"]([^'"]+)['"]/);
72
+ if (typeMatch && /conformance|quorum_fallback/.test(typeMatch[1])) {
73
+ emissionPoints.push({
74
+ file: filePath,
75
+ line_number: i + 1,
76
+ action: typeMatch[1],
77
+ state_variables: [],
78
+ vocabulary_match: null,
79
+ xstate_event: null,
80
+ });
81
+ }
82
+ }
83
+
84
+ return emissionPoints;
85
+ }
86
+
87
+ // ── Main ────────────────────────────────────────────────────────────────────
88
+
89
+ function main() {
90
+ // Ensure output directory
91
+ if (!fs.existsSync(EVIDENCE_DIR)) {
92
+ fs.mkdirSync(EVIDENCE_DIR, { recursive: true });
93
+ }
94
+
95
+ // Load vocabulary
96
+ const vocab = JSON.parse(fs.readFileSync(VOCAB_PATH, 'utf8'));
97
+ const vocabActions = vocab.vocabulary;
98
+ const vocabKeys = Object.keys(vocabActions).filter(k => k !== 'undefined');
99
+
100
+ // Scan hook files
101
+ let allEmissions = [];
102
+ for (const hookFile of HOOK_FILES) {
103
+ const points = scanFile(hookFile);
104
+ allEmissions.push(...points);
105
+ }
106
+
107
+ // Scan observe handlers
108
+ const binDir = path.join(ROOT, 'bin');
109
+ if (fs.existsSync(binDir)) {
110
+ const entries = fs.readdirSync(binDir);
111
+ for (const entry of entries) {
112
+ if (entry.startsWith('observe-handler-') && entry.endsWith('.cjs') && !entry.includes('.test.')) {
113
+ const points = scanFile(path.join('bin', entry));
114
+ allEmissions.push(...points);
115
+ }
116
+ }
117
+ }
118
+
119
+ // Validate against vocabulary and fill in xstate_event
120
+ const mappedActions = new Set();
121
+ for (const ep of allEmissions) {
122
+ if (ep.action in vocabActions) {
123
+ ep.vocabulary_match = true;
124
+ const vocabEntry = vocabActions[ep.action];
125
+ ep.xstate_event = vocabEntry.xstate_event; // may be null (e.g., mcp_call)
126
+ if (ep.xstate_event === null) {
127
+ ep.no_xstate_mapping = true;
128
+ }
129
+ mappedActions.add(ep.action);
130
+ } else {
131
+ ep.vocabulary_match = false;
132
+ }
133
+ }
134
+
135
+ // Coverage calculation: mapped vocabulary actions / total vocabulary actions (excluding "undefined")
136
+ const coveragePct = vocabKeys.length > 0
137
+ ? Math.round((mappedActions.size / vocabKeys.length) * 1000) / 10
138
+ : 0;
139
+
140
+ const unmappedActions = vocabKeys.filter(k => !mappedActions.has(k));
141
+
142
+ // Build output
143
+ const result = {
144
+ schema_version: '1',
145
+ generated: new Date().toISOString(),
146
+ emission_points: allEmissions,
147
+ coverage: {
148
+ total_vocabulary_actions: vocabKeys.length,
149
+ mapped_actions: mappedActions.size,
150
+ coverage_pct: coveragePct,
151
+ },
152
+ unmapped_actions: unmappedActions,
153
+ summary: `Found ${allEmissions.length} emission points across ${HOOK_FILES.length} hooks and observe handlers. ` +
154
+ `Coverage: ${mappedActions.size}/${vocabKeys.length} vocabulary actions mapped (${coveragePct}%). ` +
155
+ `Unmapped: ${unmappedActions.join(', ') || 'none'}.`,
156
+ };
157
+
158
+ fs.writeFileSync(OUTPUT_PATH, JSON.stringify(result, null, 2) + '\n', 'utf8');
159
+
160
+ if (JSON_FLAG) {
161
+ console.log(JSON.stringify(result, null, 2));
162
+ } else {
163
+ console.log(`Instrumentation Map Generated`);
164
+ console.log(` Emission points: ${allEmissions.length}`);
165
+ console.log(` Coverage: ${mappedActions.size}/${vocabKeys.length} (${coveragePct}%)`);
166
+ if (unmappedActions.length > 0) {
167
+ console.log(` Unmapped: ${unmappedActions.join(', ')}`);
168
+ }
169
+ }
170
+ }
171
+
172
+ // Export for testing
173
+ module.exports = { scanFile, HOOK_FILES };
174
+
175
+ // Run if invoked directly
176
+ if (require.main === module) {
177
+ main();
178
+ }
@@ -0,0 +1,437 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // bin/invariant-catalog.cjs
4
+ // Builds a unified invariant catalog aggregating declared (TLA+ .cfg, spec/invariants.md)
5
+ // and observed (trace-mined) invariants into a queryable JSON catalog.
6
+ // Requirements: SEM-01
7
+ //
8
+ // Usage:
9
+ // node bin/invariant-catalog.cjs # print summary to stdout
10
+ // node bin/invariant-catalog.cjs --json # print full catalog JSON to stdout
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ const ROOT = process.env.PROJECT_ROOT || path.join(__dirname, '..');
16
+ const FORMAL = path.join(ROOT, '.planning', 'formal');
17
+ const TLA_DIR = path.join(FORMAL, 'tla');
18
+ const SPEC_DIR = path.join(FORMAL, 'spec');
19
+ const OUT_DIR = path.join(FORMAL, 'semantics');
20
+ const OUT_FILE = path.join(OUT_DIR, 'invariant-catalog.json');
21
+ const CONFORMANCE_PATH = path.join(ROOT, '.planning', 'telemetry', 'conformance-events.jsonl');
22
+ const TRACE_STATS_PATH = path.join(FORMAL, 'evidence', 'trace-corpus-stats.json');
23
+
24
+ const JSON_FLAG = process.argv.includes('--json');
25
+
26
+ // ── Source A: TLC .cfg files ─────────────────────────────────────────────────
27
+
28
+ function parseCfgFiles() {
29
+ const invariants = [];
30
+ const cfgFiles = fs.readdirSync(TLA_DIR).filter(f => f.startsWith('MC') && f.endsWith('.cfg'));
31
+
32
+ for (const cfgFile of cfgFiles) {
33
+ const content = fs.readFileSync(path.join(TLA_DIR, cfgFile), 'utf8');
34
+ const lines = content.split('\n');
35
+ const config = cfgFile.replace(/\.cfg$/, '');
36
+ let inBlock = false;
37
+
38
+ for (let i = 0; i < lines.length; i++) {
39
+ const line = lines[i];
40
+ const trimmed = line.trim();
41
+
42
+ // Single-line: INVARIANT Name
43
+ const singleMatch = trimmed.match(/^INVARIANT\s+(\S+)$/);
44
+ if (singleMatch) {
45
+ invariants.push({
46
+ name: singleMatch[1],
47
+ source: 'tla_cfg',
48
+ source_file: `tla/${cfgFile}`,
49
+ type: 'declared',
50
+ formalism: 'tla',
51
+ config,
52
+ });
53
+ continue;
54
+ }
55
+
56
+ // Single-line: PROPERTY Name
57
+ const propMatch = trimmed.match(/^PROPERTY\s+(\S+)$/);
58
+ if (propMatch) {
59
+ invariants.push({
60
+ name: propMatch[1],
61
+ source: 'tla_cfg',
62
+ source_file: `tla/${cfgFile}`,
63
+ type: 'declared',
64
+ formalism: 'tla',
65
+ config,
66
+ });
67
+ continue;
68
+ }
69
+
70
+ // Block: INVARIANTS keyword
71
+ if (trimmed === 'INVARIANTS') {
72
+ inBlock = true;
73
+ continue;
74
+ }
75
+
76
+ // Block: PROPERTIES keyword
77
+ if (trimmed === 'PROPERTIES') {
78
+ inBlock = true;
79
+ continue;
80
+ }
81
+
82
+ // End block on next keyword, empty line, or non-indented line
83
+ if (inBlock) {
84
+ const isIndented = line.length > 0 && (line[0] === ' ' || line[0] === '\t');
85
+ if (!trimmed || (!isIndented && /^[A-Z]/.test(trimmed))) {
86
+ inBlock = false;
87
+ // re-process this line (might be a keyword)
88
+ if (trimmed.match(/^INVARIANT\s*/)) i--;
89
+ else if (trimmed.match(/^PROPERTY\s*/)) i--;
90
+ else if (trimmed.match(/^SPECIFICATION\s*/)) { /* skip */ }
91
+ else if (trimmed.match(/^CONSTANTS?\s*/)) { /* skip */ }
92
+ else if (trimmed.match(/^CONSTRAINT\s*/)) { /* skip */ }
93
+ else if (trimmed.match(/^CHECK_DEADLOCK\s*/)) { /* skip */ }
94
+ continue;
95
+ }
96
+ invariants.push({
97
+ name: trimmed,
98
+ source: 'tla_cfg',
99
+ source_file: `tla/${cfgFile}`,
100
+ type: 'declared',
101
+ formalism: 'tla',
102
+ config,
103
+ });
104
+ }
105
+ }
106
+ }
107
+ return invariants;
108
+ }
109
+
110
+ // ── Source B: spec/*/invariants.md files ──────────────────────────────────────
111
+
112
+ function parseSpecInvariants() {
113
+ const invariants = [];
114
+ if (!fs.existsSync(SPEC_DIR)) return invariants;
115
+
116
+ const specDirs = fs.readdirSync(SPEC_DIR).filter(d =>
117
+ fs.statSync(path.join(SPEC_DIR, d)).isDirectory()
118
+ );
119
+
120
+ for (const dir of specDirs) {
121
+ const invFile = path.join(SPEC_DIR, dir, 'invariants.md');
122
+ if (!fs.existsSync(invFile)) continue;
123
+
124
+ const content = fs.readFileSync(invFile, 'utf8');
125
+ const lines = content.split('\n');
126
+ let currentName = null;
127
+ let propertyExpr = null;
128
+
129
+ for (const line of lines) {
130
+ // ## section headers = invariant names
131
+ const headerMatch = line.match(/^##\s+(.+)$/);
132
+ if (headerMatch) {
133
+ // Save previous if exists
134
+ if (currentName) {
135
+ const formalism = (propertyExpr && /(<>|PROPERTY|liveness|LivenessProperty|Reachable|Eventually|Terminates|Progress)/i.test(propertyExpr || currentName))
136
+ ? 'liveness' : 'safety';
137
+ invariants.push({
138
+ name: currentName,
139
+ source: 'spec_invariants_md',
140
+ source_file: `spec/${dir}/invariants.md`,
141
+ type: 'declared',
142
+ formalism,
143
+ property_expression: propertyExpr || null,
144
+ });
145
+ }
146
+ currentName = headerMatch[1].trim();
147
+ propertyExpr = null;
148
+ continue;
149
+ }
150
+
151
+ // **Property:** `expression`
152
+ const propMatch = line.match(/\*\*Property:\*\*\s*`([^`]+)`/);
153
+ if (propMatch && currentName) {
154
+ propertyExpr = propMatch[1];
155
+ }
156
+ }
157
+
158
+ // Save last entry
159
+ if (currentName) {
160
+ const formalism = (propertyExpr && /<>|PROPERTY|liveness|LivenessProperty|Reachable|Eventually|Terminates|Progress/i.test(propertyExpr || currentName))
161
+ ? 'liveness' : 'safety';
162
+ invariants.push({
163
+ name: currentName,
164
+ source: 'spec_invariants_md',
165
+ source_file: `spec/${dir}/invariants.md`,
166
+ type: 'declared',
167
+ formalism,
168
+ property_expression: propertyExpr || null,
169
+ });
170
+ }
171
+ }
172
+ return invariants;
173
+ }
174
+
175
+ // ── Source C: Observed invariants (curated trace checks) ─────────────────────
176
+
177
+ function mineObservedInvariants() {
178
+ const invariants = [];
179
+
180
+ // Load trace-corpus-stats.json for aggregate data
181
+ let traceStats = null;
182
+ if (fs.existsSync(TRACE_STATS_PATH)) {
183
+ traceStats = JSON.parse(fs.readFileSync(TRACE_STATS_PATH, 'utf8'));
184
+ }
185
+
186
+ // Load conformance-events.jsonl for sequence-based checks
187
+ let events = [];
188
+ if (fs.existsSync(CONFORMANCE_PATH)) {
189
+ const raw = fs.readFileSync(CONFORMANCE_PATH, 'utf8').trim().split('\n');
190
+ for (const line of raw) {
191
+ try { events.push(JSON.parse(line)); } catch { /* skip malformed */ }
192
+ }
193
+ }
194
+
195
+ if (!traceStats && events.length === 0) {
196
+ process.stderr.write('Warning: No trace data available for observed invariants\n');
197
+ return invariants;
198
+ }
199
+
200
+ const totalSessions = traceStats ? traceStats.sessions.length : 0;
201
+
202
+ // Check 1: quorum_start always precedes quorum_complete within a session
203
+ // Requires conformance-events.jsonl for event ordering per session
204
+ if (events.length > 0 && traceStats) {
205
+ let holds = true;
206
+ let sessionsChecked = 0;
207
+
208
+ for (const session of traceStats.sessions) {
209
+ const sessionStart = new Date(session.start).getTime();
210
+ const sessionEnd = new Date(session.end).getTime();
211
+ const sessionEvents = events.filter(e => {
212
+ const t = new Date(e.ts).getTime();
213
+ return t >= sessionStart && t <= sessionEnd;
214
+ });
215
+
216
+ const relevant = sessionEvents.filter(e =>
217
+ e.action === 'quorum_start' || e.action === 'quorum_complete'
218
+ );
219
+
220
+ let startCount = 0;
221
+ for (const evt of relevant) {
222
+ if (evt.action === 'quorum_start') startCount++;
223
+ if (evt.action === 'quorum_complete') {
224
+ if (startCount === 0) { holds = false; break; }
225
+ }
226
+ }
227
+ if (!holds) break;
228
+ sessionsChecked++;
229
+ }
230
+
231
+ if (holds) {
232
+ invariants.push({
233
+ name: 'quorum_start_precedes_complete',
234
+ source: 'trace_mining',
235
+ type: 'observed',
236
+ property_expression: 'quorum_start always precedes quorum_complete within a session',
237
+ confidence: 'curated',
238
+ evidence_sessions: sessionsChecked,
239
+ });
240
+ }
241
+ }
242
+
243
+ // Check 2: circuit_break events only occur during active quorum sessions
244
+ // Requires conformance-events.jsonl to check session context
245
+ if (events.length > 0 && traceStats) {
246
+ let holds = true;
247
+ let sessionsChecked = 0;
248
+
249
+ for (const session of traceStats.sessions) {
250
+ const sessionStart = new Date(session.start).getTime();
251
+ const sessionEnd = new Date(session.end).getTime();
252
+ const sessionEvents = events.filter(e => {
253
+ const t = new Date(e.ts).getTime();
254
+ return t >= sessionStart && t <= sessionEnd;
255
+ });
256
+
257
+ const circuitBreaks = sessionEvents.filter(e => e.action === 'circuit_break');
258
+ const hasQuorumStart = sessionEvents.some(e => e.action === 'quorum_start');
259
+
260
+ if (circuitBreaks.length > 0 && !hasQuorumStart) {
261
+ holds = false;
262
+ break;
263
+ }
264
+ sessionsChecked++;
265
+ }
266
+
267
+ if (holds) {
268
+ invariants.push({
269
+ name: 'circuit_break_within_quorum_session',
270
+ source: 'trace_mining',
271
+ type: 'observed',
272
+ property_expression: 'circuit_break events only occur during active quorum sessions',
273
+ confidence: 'curated',
274
+ evidence_sessions: sessionsChecked,
275
+ });
276
+ }
277
+ }
278
+
279
+ // Check 3: no session has more quorum_complete than quorum_start
280
+ // Can use trace-corpus-stats.json aggregate counts
281
+ if (traceStats) {
282
+ let holds = true;
283
+ let sessionsChecked = 0;
284
+
285
+ for (const session of traceStats.sessions) {
286
+ const starts = session.actions.quorum_start || 0;
287
+ const completes = session.actions.quorum_complete || 0;
288
+ if (completes > starts) {
289
+ holds = false;
290
+ break;
291
+ }
292
+ sessionsChecked++;
293
+ }
294
+
295
+ if (holds) {
296
+ invariants.push({
297
+ name: 'complete_bounded_by_start',
298
+ source: 'trace_mining',
299
+ type: 'observed',
300
+ property_expression: 'no session has more quorum_complete events than quorum_start events',
301
+ confidence: 'curated',
302
+ evidence_sessions: sessionsChecked,
303
+ });
304
+ }
305
+ }
306
+
307
+ // Check 4: sessions with quorum_complete also have quorum_block
308
+ // Requires trace-corpus-stats.json -- quorum must block before completing
309
+ if (traceStats) {
310
+ let holds = true;
311
+ let sessionsChecked = 0;
312
+
313
+ for (const session of traceStats.sessions) {
314
+ const blocks = (session.actions.quorum_block || 0) + (session.actions.quorum_block_r3_2 || 0);
315
+ const completes = session.actions.quorum_complete || 0;
316
+ if (completes > 0 && blocks === 0) {
317
+ holds = false;
318
+ break;
319
+ }
320
+ sessionsChecked++;
321
+ }
322
+
323
+ if (holds) {
324
+ invariants.push({
325
+ name: 'complete_requires_block',
326
+ source: 'trace_mining',
327
+ type: 'observed',
328
+ property_expression: 'sessions with quorum_complete also have quorum_block events',
329
+ confidence: 'curated',
330
+ evidence_sessions: sessionsChecked,
331
+ });
332
+ }
333
+ }
334
+
335
+ // Check 5: event action counts are consistent (sum of actions = event_count)
336
+ // Requires trace-corpus-stats.json -- structural data integrity check
337
+ if (traceStats) {
338
+ let holds = true;
339
+ let sessionsChecked = 0;
340
+
341
+ for (const session of traceStats.sessions) {
342
+ const sum = Object.values(session.actions).reduce((a, b) => a + b, 0);
343
+ if (sum !== session.event_count) {
344
+ holds = false;
345
+ break;
346
+ }
347
+ sessionsChecked++;
348
+ }
349
+
350
+ if (holds) {
351
+ invariants.push({
352
+ name: 'action_count_consistency',
353
+ source: 'trace_mining',
354
+ type: 'observed',
355
+ property_expression: 'sum of per-action counts equals total event_count for every session',
356
+ confidence: 'curated',
357
+ evidence_sessions: sessionsChecked,
358
+ });
359
+ }
360
+ }
361
+
362
+ return invariants;
363
+ }
364
+
365
+ // ── Deduplication ────────────────────────────────────────────────────────────
366
+
367
+ function deduplicateInvariants(rawInvariants) {
368
+ const map = new Map();
369
+
370
+ for (const inv of rawInvariants) {
371
+ // For tla_cfg: deduplicate by (name, config/model)
372
+ // For others: deduplicate by (name, source)
373
+ const modelKey = inv.config || inv.source_file || inv.source;
374
+ const key = `${inv.name}::${modelKey}`;
375
+
376
+ if (map.has(key)) {
377
+ const existing = map.get(key);
378
+ if (!existing.check_references) {
379
+ existing.check_references = [existing.source_file || existing.source];
380
+ }
381
+ existing.check_references.push(inv.source_file || inv.source);
382
+ } else {
383
+ map.set(key, { ...inv });
384
+ }
385
+ }
386
+
387
+ return [...map.values()];
388
+ }
389
+
390
+ // ── Main ─────────────────────────────────────────────────────────────────────
391
+
392
+ function main() {
393
+ const cfgInvariants = parseCfgFiles();
394
+ const specInvariants = parseSpecInvariants();
395
+ const observedInvariants = mineObservedInvariants();
396
+
397
+ const allRaw = [...cfgInvariants, ...specInvariants, ...observedInvariants];
398
+ const deduped = deduplicateInvariants(allRaw);
399
+
400
+ const byType = { declared: 0, observed: 0 };
401
+ const bySource = { tla_cfg: 0, spec_invariants_md: 0, trace_mining: 0 };
402
+
403
+ for (const inv of deduped) {
404
+ byType[inv.type] = (byType[inv.type] || 0) + 1;
405
+ bySource[inv.source] = (bySource[inv.source] || 0) + 1;
406
+ }
407
+
408
+ const catalog = {
409
+ schema_version: '1',
410
+ generated: new Date().toISOString(),
411
+ invariants: deduped,
412
+ summary: {
413
+ total_raw: allRaw.length,
414
+ total_deduplicated: deduped.length,
415
+ by_type: byType,
416
+ by_source: bySource,
417
+ },
418
+ };
419
+
420
+ // Ensure output directory exists
421
+ if (!fs.existsSync(OUT_DIR)) fs.mkdirSync(OUT_DIR, { recursive: true });
422
+ fs.writeFileSync(OUT_FILE, JSON.stringify(catalog, null, 2) + '\n');
423
+
424
+ if (JSON_FLAG) {
425
+ process.stdout.write(JSON.stringify(catalog, null, 2) + '\n');
426
+ } else {
427
+ console.log(`Invariant Catalog written to ${path.relative(ROOT, OUT_FILE)}`);
428
+ console.log(` Raw: ${allRaw.length} Deduplicated: ${deduped.length}`);
429
+ console.log(` By type: declared=${byType.declared} observed=${byType.observed}`);
430
+ console.log(` By source: tla_cfg=${bySource.tla_cfg} spec_invariants_md=${bySource.spec_invariants_md} trace_mining=${bySource.trace_mining}`);
431
+ }
432
+ }
433
+
434
+ // Export for testing
435
+ module.exports = { parseCfgFiles, parseSpecInvariants, mineObservedInvariants, deduplicateInvariants };
436
+
437
+ if (require.main === module) main();
@@ -77,7 +77,7 @@ if (circuitBreaker.active === true) {
77
77
  id: 'circuit-breaker-active',
78
78
  priority: 90,
79
79
  description: 'Circuit breaker is currently active — oscillation was detected and execution is paused.',
80
- action: 'Run /qgsd:debug to diagnose the oscillation root cause, then run `npx qgsd --reset-breaker`.',
80
+ action: 'Run /nf:debug to diagnose the oscillation root cause, then run `npx nforma --reset-breaker`.',
81
81
  surfaced: false,
82
82
  detectedAt: now,
83
83
  });
@@ -131,7 +131,7 @@ if (!circuitBreaker.active && (circuitBreaker.triggerCount || 0) > 3) {
131
131
  id: 'circuit-breaker-repeated-triggers',
132
132
  priority: 50,
133
133
  description: `Circuit breaker has triggered ${circuitBreaker.triggerCount} times — recurring oscillation pattern detected.`,
134
- action: 'Run /qgsd:discuss-phase to review recent commit patterns; consider adding explicit done-criteria to plans.',
134
+ action: 'Run /nf:discuss-phase to review recent commit patterns; consider adding explicit done-criteria to plans.',
135
135
  surfaced: false,
136
136
  detectedAt: now,
137
137
  });
@@ -8,11 +8,11 @@ const path = require('path');
8
8
  * Load baseline requirements filtered by project profile.
9
9
  *
10
10
  * @param {string} profile - One of: web, mobile, desktop, api, cli, library
11
- * @param {string} [basePath] - Path to baseline-requirements directory, defaults to qgsd-core/defaults/baseline-requirements
11
+ * @param {string} [basePath] - Path to baseline-requirements directory, defaults to core/defaults/baseline-requirements
12
12
  * @returns {Object} { profile, label, description, categories: [...], total }
13
13
  */
14
14
  function loadBaselineRequirements(profile, basePath) {
15
- const defaultBasePath = path.resolve(__dirname, '../qgsd-core/defaults/baseline-requirements');
15
+ const defaultBasePath = path.resolve(__dirname, '../core/defaults/baseline-requirements');
16
16
  const basePathToUse = basePath || defaultBasePath;
17
17
 
18
18
  // Read index.json
@@ -105,7 +105,7 @@ function loadBaselineRequirements(profile, basePath) {
105
105
  * @returns {Object} { profile, label, intent, categories, packs_applied, total }
106
106
  */
107
107
  function loadBaselineRequirementsFromIntent(intent, basePath) {
108
- const defaultBasePath = path.resolve(__dirname, '../qgsd-core/defaults/baseline-requirements');
108
+ const defaultBasePath = path.resolve(__dirname, '../core/defaults/baseline-requirements');
109
109
  const basePathToUse = basePath || defaultBasePath;
110
110
 
111
111
  // Validate base_profile
@@ -231,7 +231,7 @@ if (require.main === module) {
231
231
  const args = process.argv.slice(2);
232
232
 
233
233
  if (args.includes('--list-profiles')) {
234
- const indexPath = path.resolve(__dirname, '../qgsd-core/defaults/baseline-requirements/index.json');
234
+ const indexPath = path.resolve(__dirname, '../core/defaults/baseline-requirements/index.json');
235
235
  const indexContent = fs.readFileSync(indexPath, 'utf8');
236
236
  const index = JSON.parse(indexContent);
237
237
  const profiles = Object.entries(index.profiles).map(([key, val]) => ({