@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,438 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // bin/validate-traces.cjs
4
+ // Replays .planning/conformance-events.jsonl through the XState machine
5
+ // and reports a deviation score (% of traces that are valid XState executions).
6
+ //
7
+ // MCPENV-03: also validates MCP interaction metadata for mcp_call events.
8
+ // Schema: .planning/formal/trace/trace.schema.json
9
+ //
10
+ // Exit code 0: no divergences found (or log file missing)
11
+ // Exit code 1: one or more divergences found
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+
16
+ // ── Confidence tier constants and helpers ─────────────────────────────────────
17
+
18
+ const CONFIDENCE_THRESHOLDS = {
19
+ low: { min_rounds: 0, min_days: 0 },
20
+ medium: { min_rounds: 500, min_days: 14 },
21
+ high: { min_rounds: 10000, min_days: 90 },
22
+ };
23
+
24
+ function computeConfidenceTier(n_rounds, window_days) {
25
+ if (n_rounds >= CONFIDENCE_THRESHOLDS.high.min_rounds && window_days >= CONFIDENCE_THRESHOLDS.high.min_days) return 'high';
26
+ if (n_rounds >= CONFIDENCE_THRESHOLDS.medium.min_rounds && window_days >= CONFIDENCE_THRESHOLDS.medium.min_days) return 'medium';
27
+ return 'low';
28
+ }
29
+
30
+ function readScoreboardMeta() {
31
+ const pp = require('./planning-paths.cjs');
32
+ const scoreboardPath = pp.resolveWithFallback(process.cwd(), 'quorum-scoreboard');
33
+ try {
34
+ const raw = fs.readFileSync(scoreboardPath, 'utf8');
35
+ const sb = JSON.parse(raw);
36
+ const rounds = Array.isArray(sb.rounds) ? sb.rounds : [];
37
+ const n_rounds = rounds.length;
38
+ if (n_rounds === 0) return { n_rounds: 0, window_days: 0 };
39
+ const dates = rounds.map(r => new Date(r.date).getTime()).filter(t => !isNaN(t));
40
+ const window_days = dates.length < 2 ? 0 : Math.floor((Math.max(...dates) - Math.min(...dates)) / 86400000);
41
+ return { n_rounds, window_days };
42
+ } catch (_) {
43
+ return { n_rounds: 0, window_days: 0 };
44
+ }
45
+ }
46
+
47
+ // Validates MCP-specific metadata fields for mcp_call events (MCPENV-03).
48
+ // Schema: .planning/formal/trace/trace.schema.json
49
+ // Returns true if valid, or an array of error strings if invalid.
50
+ // Non-mcp_call events are always valid (returns true immediately).
51
+ function validateMCPMetadata(event) {
52
+ if (!event || event.action !== 'mcp_call') return true; // not an MCP event — skip
53
+
54
+ const errors = [];
55
+ if (!event.request_id || typeof event.request_id !== 'string') {
56
+ errors.push('mcp_call missing or invalid request_id (expected string, e.g. round1:codex-1:1, got: ' + JSON.stringify(event.request_id) + ')');
57
+ }
58
+ if (!event.peer || typeof event.peer !== 'string') {
59
+ errors.push('mcp_call missing or invalid peer (expected slot name string, e.g. codex-1, got: ' + JSON.stringify(event.peer) + ')');
60
+ }
61
+ const validMCPOutcomes = ['success', 'fail', 'timeout', 'reorder'];
62
+ if (!validMCPOutcomes.includes(event.mcp_outcome)) {
63
+ errors.push('mcp_call missing or invalid mcp_outcome (expected: success|fail|timeout|reorder, got: ' + JSON.stringify(event.mcp_outcome) + ')');
64
+ }
65
+ if (typeof event.attempt !== 'number' || !Number.isInteger(event.attempt) || event.attempt < 1) {
66
+ errors.push('mcp_call missing or invalid attempt (expected integer >= 1, got: ' + JSON.stringify(event.attempt) + ')');
67
+ }
68
+ return errors.length === 0 ? true : errors;
69
+ }
70
+
71
+ // Maps a conformance event action to the XState event type and payload.
72
+ // Returns null if the action is unmappable (schema violation).
73
+ function mapToXStateEvent(event) {
74
+ if (!event || typeof event.action !== 'string') return null;
75
+ switch (event.action) {
76
+ case 'quorum_start':
77
+ return { type: 'QUORUM_START', slotsAvailable: event.slots_available || 0 };
78
+ case 'quorum_complete':
79
+ return { type: 'VOTES_COLLECTED', successCount: event.vote_result || 0 };
80
+ case 'quorum_block':
81
+ return { type: 'DECIDE', outcome: 'BLOCK' };
82
+ case 'deliberation_round':
83
+ return { type: 'VOTES_COLLECTED', successCount: event.vote_result || 0 };
84
+ case 'circuit_break':
85
+ return { type: 'CIRCUIT_BREAK' };
86
+ default:
87
+ return null;
88
+ }
89
+ }
90
+
91
+ // buildTTrace: construct a structured TTrace record for a divergent conformance event.
92
+ // Uses xstate-trace-walker to get guard evaluation details.
93
+ // IMPORTANT: roundEvents is the full ordered array of conformance events for this round_id group
94
+ // (or just [event] for standalone events with no round_id). Guards are evaluated with the actor
95
+ // replayed through all preceding events in the group, so guard context at event N reflects
96
+ // context accumulated from events 1..N-1 — not from a fresh IDLE snapshot.
97
+ //
98
+ // STANDALONE EVENT TRADE-OFF (intentional approximation):
99
+ // When an event has no round_id, roundEvents = [event] — a single-event group with no
100
+ // preceding context. The actor starts fresh (IDLE) before evaluating the guard.
101
+ // This is an acceptable methodological approximation because:
102
+ // 1. Standalone events represent ad-hoc or non-session actions where cross-event context
103
+ // is not meaningful — evaluating them in isolation accurately reflects their runtime
104
+ // execution environment.
105
+ // 2. The accuracy cost is bounded: only guards that depend on context set by earlier
106
+ // events in the same round will be misclassified. Guards that depend only on the
107
+ // event's own payload (the majority) are evaluated correctly.
108
+ // 3. The alternative (silently dropping standalone events) would introduce undetected
109
+ // divergence blind spots — the approximation is always preferable to omission.
110
+ // Consequence: a standalone guard failure may be classified as impl-bug when it is a
111
+ // methodology artifact. attribute-trace-divergence.cjs should treat standalone TTrace
112
+ // records with lower specBugConfidence when no preceding context is present.
113
+ //
114
+ // scoreboardMeta and confidence are passed from the outer loop.
115
+ // walker is the result of require('./xstate-trace-walker.cjs') — passed in to keep helper pure.
116
+ function buildTTrace(event, actualState, expectedStateName, divergenceType, scoreboardMeta, confidence, walker, machine, roundEvents) {
117
+ let guardEvaluations = [];
118
+ if (walker && machine) {
119
+ try {
120
+ const _path = require('path');
121
+ const _fs = require('fs');
122
+ const _machinePath = (() => {
123
+ const repoDist = _path.join(__dirname, '..', 'dist', 'machines', 'qgsd-workflow.machine.cjs');
124
+ const installDist = _path.join(__dirname, 'dist', 'machines', 'qgsd-workflow.machine.cjs');
125
+ return _fs.existsSync(repoDist) ? repoDist : installDist;
126
+ })();
127
+ const { createActor } = require(_machinePath);
128
+
129
+ // Find the index of this event in its round group
130
+ const groupEvents = roundEvents || [event];
131
+ const eventIndex = groupEvents.indexOf(event);
132
+ const precedingEvents = groupEvents.slice(0, eventIndex);
133
+
134
+ // Replay preceding events through a single actor to accumulate session context.
135
+ // Guards evaluated here will see slotsAvailable, polledCount, etc. as set by
136
+ // QUORUM_START and earlier events — not from a fresh IDLE snapshot.
137
+ const xstateEvt = mapToXStateEvent(event);
138
+ if (xstateEvt) {
139
+ const actor = createActor(machine);
140
+ actor.start();
141
+ for (const prev of precedingEvents) {
142
+ const prevEvt = mapToXStateEvent(prev);
143
+ if (prevEvt) actor.send(prevEvt);
144
+ }
145
+ const beforeSnap = actor.getSnapshot();
146
+ actor.stop();
147
+ const walkerResult = walker.evaluateTransitions(beforeSnap, xstateEvt, machine);
148
+ guardEvaluations = walkerResult.possibleTransitions.map(t => ({
149
+ guardName: t.guardName || 'none',
150
+ passed: t.guardPassed,
151
+ context: t.guardContext,
152
+ }));
153
+ }
154
+ } catch (_) { /* fail-open — guard evaluation failure must not break validation */ }
155
+ }
156
+ return {
157
+ event,
158
+ actualState,
159
+ expectedState: expectedStateName,
160
+ guardEvaluations,
161
+ divergenceType,
162
+ confidence,
163
+ observation_window: {
164
+ n_rounds: scoreboardMeta.n_rounds,
165
+ window_days: scoreboardMeta.window_days,
166
+ },
167
+ };
168
+ }
169
+
170
+ // Returns the expected XState state after this event, based on the event's outcome field.
171
+ //
172
+ // METHODOLOGY FIX (H1 — fresh-actor blindspot correction):
173
+ // The validator uses a fresh XState actor (starting in IDLE) for each event.
174
+ // This is only meaningful for events that legitimately start from IDLE state.
175
+ // Events with phase !== 'IDLE' happen mid-session (e.g., phase='DECIDING') and
176
+ // CANNOT be valid as standalone fresh-actor traces:
177
+ // - quorum_block (DECIDE event) from IDLE → stays IDLE (no DECIDE transition from IDLE)
178
+ // - quorum_complete (VOTES_COLLECTED event) from IDLE → stays IDLE (no transition from IDLE)
179
+ // Returning null for these events causes them to be counted as VALID (skipped),
180
+ // which is the correct behavior: a mid-session event cannot be falsified by a
181
+ // fresh-actor trace from IDLE.
182
+ //
183
+ // Only quorum_start (phase=IDLE) and deliberation_round can start from IDLE legitimately.
184
+ // All other events require session context accumulated from preceding events.
185
+ function expectedState(event) {
186
+ // quorum_start always starts from IDLE — the only truly standalone-valid event type
187
+ if (event.action === 'quorum_start') return 'COLLECTING_VOTES';
188
+
189
+ // deliberation_round: these happen mid-session (phase=DECIDING) too
190
+ // but are already excluded by the phase check below
191
+ if (event.action === 'deliberation_round') return 'DELIBERATING';
192
+
193
+ // circuit_break self-loops to IDLE — no state transition occurs
194
+ if (event.action === 'circuit_break') return 'IDLE';
195
+
196
+ // H1 fix: skip validation for mid-session events (phase !== 'IDLE').
197
+ // These events require cross-event context that a fresh actor starting in IDLE cannot provide.
198
+ // Returning null causes the validator to count them as valid (not divergent),
199
+ // which is the methodologically correct choice for standalone fresh-actor validation.
200
+ if (event.phase && event.phase !== 'IDLE') return null;
201
+
202
+ if (event.outcome === 'APPROVE') return 'DECIDED';
203
+ if (event.outcome === 'BLOCK') return 'DECIDED';
204
+ return null; // cannot determine — will count as valid (skip)
205
+ }
206
+
207
+ // Builds the observation_window object for a check-result NDJSON record (EVID-02).
208
+ // Reads scoreboard to derive window_start (earliest round date), window_end (now),
209
+ // n_traces (round count), n_events (conformance event count), window_days (span).
210
+ // Fail-open: if scoreboard is missing or malformed, returns sensible zero defaults.
211
+ function buildObservationWindow(scoreboardMeta, n_events) {
212
+ let window_start = new Date().toISOString();
213
+ try {
214
+ const pp2 = require('./planning-paths.cjs');
215
+ const sbPath = pp2.resolveWithFallback(process.cwd(), 'quorum-scoreboard');
216
+ if (fs.existsSync(sbPath)) {
217
+ const sb = JSON.parse(fs.readFileSync(sbPath, 'utf8'));
218
+ const rounds = Array.isArray(sb.rounds) ? sb.rounds : [];
219
+ if (rounds.length > 0) {
220
+ const dates = rounds.map(r => new Date(r.date).getTime()).filter(t => !isNaN(t));
221
+ if (dates.length > 0) {
222
+ window_start = new Date(Math.min(...dates)).toISOString();
223
+ }
224
+ }
225
+ }
226
+ } catch (_) { /* fail-open */ }
227
+ return {
228
+ window_start,
229
+ window_end: new Date().toISOString(),
230
+ n_traces: scoreboardMeta.n_rounds,
231
+ n_events,
232
+ window_days: scoreboardMeta.window_days,
233
+ };
234
+ }
235
+
236
+ if (require.main === module) {
237
+ const _startMs = Date.now();
238
+ const { writeCheckResult } = require('./write-check-result.cjs');
239
+ // Machine CJS path: in the repo, ../dist/machines/ (bin/ → dist/machines/)
240
+ // When installed at ~/.claude/qgsd-bin/, ./dist/machines/ (qgsd-bin/ → qgsd-bin/dist/machines/)
241
+ const machinePath = (function () {
242
+ const repoDist = path.join(__dirname, '..', 'dist', 'machines', 'qgsd-workflow.machine.cjs');
243
+ const installDist = path.join(__dirname, 'dist', 'machines', 'qgsd-workflow.machine.cjs');
244
+ if (fs.existsSync(repoDist)) return repoDist;
245
+ if (fs.existsSync(installDist)) return installDist;
246
+ throw new Error('[validate-traces] Cannot find qgsd-workflow.machine.cjs in ' + repoDist + ' or ' + installDist);
247
+ })();
248
+ const { createActor, qgsdWorkflowMachine } = require(machinePath);
249
+
250
+ // Load walker for TTrace export (DIAG-01) — lazy-loaded here to avoid breaking module.exports usage
251
+ let walker = null;
252
+ try {
253
+ walker = require('./xstate-trace-walker.cjs');
254
+ } catch (_) { /* walker not available — TTrace export will use empty guardEvaluations */ }
255
+
256
+ const pp = require('./planning-paths.cjs');
257
+ const logPath = pp.resolveWithFallback(process.cwd(), 'conformance-events');
258
+
259
+ if (!fs.existsSync(logPath)) {
260
+ process.stdout.write('[validate-traces] No conformance log at: ' + logPath + ' — nothing to validate\n');
261
+ const _obs0 = buildObservationWindow({ n_rounds: 0, window_days: 0 }, 0);
262
+ try {
263
+ writeCheckResult({
264
+ tool: 'validate-traces', formalism: 'trace', result: 'pass',
265
+ check_id: 'ci:conformance-traces', surface: 'ci',
266
+ property: 'Conformance event replay through XState machine',
267
+ runtime_ms: Date.now() - _startMs,
268
+ summary: 'pass: no conformance log found — nothing to validate',
269
+ observation_window: _obs0,
270
+ metadata: { reason: 'no-log' },
271
+ });
272
+ } catch (e) { process.stderr.write('[validate-traces] Warning: failed to write check result: ' + e.message + '\n'); }
273
+ process.exit(0);
274
+ }
275
+
276
+ const raw = fs.readFileSync(logPath, 'utf8');
277
+ const lines = raw.split('\n').filter(l => l.trim().length > 0);
278
+
279
+ if (lines.length === 0) {
280
+ process.stdout.write('[validate-traces] Conformance log is empty — deviation score: 100.0% (0/0)\n');
281
+ const _obs1 = buildObservationWindow({ n_rounds: 0, window_days: 0 }, 0);
282
+ try {
283
+ writeCheckResult({
284
+ tool: 'validate-traces', formalism: 'trace', result: 'pass',
285
+ check_id: 'ci:conformance-traces', surface: 'ci',
286
+ property: 'Conformance event replay through XState machine',
287
+ runtime_ms: Date.now() - _startMs,
288
+ summary: 'pass: conformance log is empty — nothing to validate',
289
+ observation_window: _obs1,
290
+ metadata: { reason: 'empty-log' },
291
+ });
292
+ } catch (e) { process.stderr.write('[validate-traces] Warning: failed to write check result: ' + e.message + '\n'); }
293
+ process.exit(0);
294
+ }
295
+
296
+ // Read scoreboard metadata ONCE before the loop (avoid repeated file reads)
297
+ const scoreboardMeta = readScoreboardMeta();
298
+ const confidence = computeConfidenceTier(scoreboardMeta.n_rounds, scoreboardMeta.window_days);
299
+
300
+ // Parse all events first so we can group by round_id for session-based buildTTrace calls.
301
+ const parsedEvents = [];
302
+ let valid = 0;
303
+ const divergences = [];
304
+
305
+ // First pass: parse JSON lines
306
+ for (let i = 0; i < lines.length; i++) {
307
+ const line = lines[i];
308
+ let event;
309
+ try {
310
+ event = JSON.parse(line);
311
+ event._lineIndex = i; // internal index for standalone event keying
312
+ parsedEvents.push(event);
313
+ } catch (parseErr) {
314
+ divergences.push({
315
+ line,
316
+ reason: 'json_parse_error: ' + parseErr.message,
317
+ ...scoreboardMeta,
318
+ confidence,
319
+ });
320
+ }
321
+ }
322
+
323
+ // Group events by round_id for session-based guard evaluation in buildTTrace.
324
+ // Events without round_id form singleton groups (standalone events).
325
+ const roundGroups = new Map(); // round_id_key → events[]
326
+ for (const evt of parsedEvents) {
327
+ const key = evt.round_id || evt.session_id || '__standalone__' + evt._lineIndex;
328
+ if (!roundGroups.has(key)) roundGroups.set(key, []);
329
+ roundGroups.get(key).push(evt);
330
+ }
331
+
332
+ // Second pass: validate each parsed event
333
+ for (const event of parsedEvents) {
334
+ // MCPENV-03: validate MCP interaction metadata for mcp_call events
335
+ const mcpErrors = validateMCPMetadata(event);
336
+ if (mcpErrors !== true) {
337
+ divergences.push({
338
+ event,
339
+ reason: 'mcp_field_validation',
340
+ errors: mcpErrors,
341
+ divergenceType: 'mcp_field_validation',
342
+ ...scoreboardMeta,
343
+ confidence,
344
+ });
345
+ continue;
346
+ }
347
+
348
+ const xstateEvent = mapToXStateEvent(event);
349
+ if (!xstateEvent) {
350
+ divergences.push({
351
+ event,
352
+ reason: 'unmappable_action: ' + event.action,
353
+ divergenceType: 'unmappable_action',
354
+ ...scoreboardMeta,
355
+ confidence,
356
+ });
357
+ continue;
358
+ }
359
+
360
+ // Fresh actor per event — each conformance event is a single-step trace
361
+ const actor = createActor(qgsdWorkflowMachine);
362
+ actor.start();
363
+ actor.send(xstateEvent);
364
+ const snapshot = actor.getSnapshot();
365
+ actor.stop();
366
+
367
+ const expected = expectedState(event);
368
+ if (expected === null || snapshot.matches(expected)) {
369
+ valid++;
370
+ } else {
371
+ // Build TTrace record with session-based guard evaluation (DIAG-01)
372
+ const roundKey = event.round_id || event.session_id || '__standalone__' + event._lineIndex;
373
+ const roundEvents = roundGroups.get(roundKey) || [event];
374
+ divergences.push(
375
+ buildTTrace(event, snapshot.value, expected, 'state_mismatch', scoreboardMeta, confidence, walker, qgsdWorkflowMachine, roundEvents)
376
+ );
377
+ }
378
+ }
379
+
380
+ const total = lines.length;
381
+ const score = ((valid / total) * 100).toFixed(1);
382
+ const observationWindow = buildObservationWindow(scoreboardMeta, total);
383
+
384
+ // Export first 10 state_mismatch TTrace records to .planning/formal/.divergences.json (DIAG-01)
385
+ // Atomic write (tmp + rename) consistent with project pattern.
386
+ const ttraceDivergences = divergences
387
+ .filter(d => d.divergenceType === 'state_mismatch')
388
+ .slice(0, 10);
389
+ if (ttraceDivergences.length > 0) {
390
+ const divergencesPath = path.join(process.cwd(), '.planning', 'formal', '.divergences.json');
391
+ const tmpPath = divergencesPath + '.tmp.' + Date.now();
392
+ try {
393
+ fs.writeFileSync(tmpPath, JSON.stringify(ttraceDivergences, null, 2), 'utf8');
394
+ fs.renameSync(tmpPath, divergencesPath);
395
+ } catch (e) {
396
+ process.stderr.write('[validate-traces] Warning: failed to write .divergences.json: ' + e.message + '\n');
397
+ }
398
+ }
399
+
400
+ process.stdout.write('[validate-traces] Deviation score: ' + score + '% valid (' + valid + '/' + total + ' traces)\n');
401
+
402
+ if (divergences.length > 0) {
403
+ process.stdout.write('[validate-traces] ' + divergences.length + ' divergence(s) found:\n');
404
+ for (const d of divergences) {
405
+ process.stdout.write(' ' + JSON.stringify(d) + '\n');
406
+ }
407
+ try {
408
+ writeCheckResult({
409
+ tool: 'validate-traces', formalism: 'trace', result: 'fail',
410
+ check_id: 'ci:conformance-traces', surface: 'ci',
411
+ property: 'Conformance event replay through XState machine',
412
+ runtime_ms: Date.now() - _startMs,
413
+ summary: 'fail: ' + divergences.length + ' divergence(s) in ' + total + ' traces (' + (Date.now() - _startMs) + 'ms)',
414
+ observation_window: observationWindow,
415
+ metadata: { divergences: divergences.length, total },
416
+ triage_tags: ['trace-divergence'],
417
+ });
418
+ } catch (e) { process.stderr.write('[validate-traces] Warning: failed to write check result: ' + e.message + '\n'); }
419
+ process.exit(1);
420
+ }
421
+
422
+ try {
423
+ writeCheckResult({
424
+ tool: 'validate-traces', formalism: 'trace', result: 'pass',
425
+ check_id: 'ci:conformance-traces', surface: 'ci',
426
+ property: 'Conformance event replay through XState machine',
427
+ runtime_ms: Date.now() - _startMs,
428
+ summary: 'pass: ' + valid + '/' + total + ' traces valid (' + (Date.now() - _startMs) + 'ms)',
429
+ observation_window: observationWindow,
430
+ metadata: { valid, total },
431
+ });
432
+ } catch (e) { process.stderr.write('[validate-traces] Warning: failed to write check result: ' + e.message + '\n'); }
433
+ process.exit(0);
434
+ }
435
+
436
+ if (typeof module !== 'undefined') {
437
+ module.exports = { computeConfidenceTier, CONFIDENCE_THRESHOLDS, validateMCPMetadata, buildTTrace };
438
+ }
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // bin/verify-formal-results.cjs
4
+ // NDJSON parser and Formal Verification section generator.
5
+ // Exports: parseNDJSON, groupByFormalism, generateFVSection
6
+ // Requirements: VERIFY-01, VERIFY-02
7
+
8
+ const fs = require('fs');
9
+
10
+ /**
11
+ * Parse .planning/formal/check-results.ndjson line-by-line.
12
+ * Skips empty lines and malformed JSON (fail-open per PLAN-03 pattern).
13
+ * @param {string} ndjsonPath — absolute or relative path to NDJSON file
14
+ * @returns {object[]} — array of parsed result objects (may be empty)
15
+ */
16
+ function parseNDJSON(ndjsonPath) {
17
+ let content;
18
+ try {
19
+ content = fs.readFileSync(ndjsonPath, 'utf8');
20
+ } catch (e) {
21
+ // File missing or unreadable — fail-open, return empty
22
+ return [];
23
+ }
24
+ const lines = content.trim().split('\n');
25
+ const results = [];
26
+ for (const line of lines) {
27
+ if (!line.trim()) continue; // skip empty lines
28
+ try {
29
+ results.push(JSON.parse(line));
30
+ } catch (e) {
31
+ // Skip malformed NDJSON lines — warn but do not throw
32
+ process.stderr.write('[verify-formal-results] warning: skipping malformed NDJSON line: ' + e.message + '\n');
33
+ }
34
+ }
35
+ return results;
36
+ }
37
+
38
+ /**
39
+ * Group result objects by formalism with pass/fail/warn/inconclusive counts.
40
+ * Uses result.formalism as dynamic key — no hardcoded formalism list.
41
+ * @param {object[]} results — parsed NDJSON result objects
42
+ * @returns {Object.<string, {pass: number, fail: number, warn: number, inconclusive: number}>}
43
+ */
44
+ function groupByFormalism(results) {
45
+ const grouped = {};
46
+ for (const result of results) {
47
+ const formalism = result.formalism || 'unknown';
48
+ if (!grouped[formalism]) {
49
+ grouped[formalism] = { pass: 0, fail: 0, warn: 0, inconclusive: 0 };
50
+ }
51
+ const resultType = result.result;
52
+ if (resultType in grouped[formalism]) {
53
+ grouped[formalism][resultType]++;
54
+ }
55
+ }
56
+ return grouped;
57
+ }
58
+
59
+ /**
60
+ * Generate a ## Formal Verification markdown section from grouped counts.
61
+ * @param {Object} grouped — output of groupByFormalism
62
+ * @param {string} command — the command that was run (e.g. 'run-formal-verify --only=tla')
63
+ * @param {string} timestamp — ISO timestamp of when FV was run
64
+ * @returns {string} — markdown string starting with '## Formal Verification'
65
+ */
66
+ function generateFVSection(grouped, command, timestamp) {
67
+ const lines = ['## Formal Verification', ''];
68
+ lines.push(`**Command:** \`${command}\``);
69
+ lines.push(`**Completed:** ${timestamp}`);
70
+
71
+ // Determine overall status: fail > inconclusive > pass
72
+ let overallStatus = 'pass';
73
+ for (const counts of Object.values(grouped)) {
74
+ if (counts.fail > 0) { overallStatus = 'fail'; break; }
75
+ if (counts.inconclusive > 0 && overallStatus !== 'fail') overallStatus = 'inconclusive';
76
+ }
77
+ lines.push(`**Overall Status:** ${overallStatus}`);
78
+ lines.push('');
79
+
80
+ // Per-formalism tables
81
+ for (const [formalism, counts] of Object.entries(grouped)) {
82
+ const label = formalism.toUpperCase();
83
+ lines.push(`### ${label} Results`);
84
+ lines.push('');
85
+ lines.push('| Result | Count | Notes |');
86
+ lines.push('|--------|-------|-------|');
87
+ lines.push(`| pass | ${counts.pass} | ${counts.pass > 0 ? 'Checks verified' : 'None'} |`);
88
+ lines.push(`| fail | ${counts.fail} | ${counts.fail > 0 ? 'Critical: investigation needed' : 'None'} |`);
89
+ lines.push(`| warn | ${counts.warn} | ${counts.warn > 0 ? 'Advisory: review recommended' : 'None'} |`);
90
+ lines.push(`| inconclusive | ${counts.inconclusive} | ${counts.inconclusive > 0 ? 'Requires fairness assumptions or additional data' : 'None'} |`);
91
+ lines.push('');
92
+ }
93
+
94
+ // Summary by result type (totals across all formalisms)
95
+ let totalPass = 0, totalFail = 0, totalWarn = 0, totalInconclusive = 0;
96
+ for (const counts of Object.values(grouped)) {
97
+ totalPass += counts.pass;
98
+ totalFail += counts.fail;
99
+ totalWarn += counts.warn;
100
+ totalInconclusive += counts.inconclusive;
101
+ }
102
+ const totalChecks = totalPass + totalFail + totalWarn + totalInconclusive;
103
+
104
+ lines.push('### Summary by Result Type');
105
+ lines.push('');
106
+ lines.push('| Result | Count | Notes |');
107
+ lines.push('|--------|-------|-------|');
108
+ lines.push(`| pass | ${totalPass} | ${totalPass > 0 ? 'All formal properties verified' : 'None'} |`);
109
+ lines.push(`| fail | ${totalFail} | ${totalFail > 0 ? 'Critical: formal properties violated' : 'None'} |`);
110
+ lines.push(`| warn | ${totalWarn} | ${totalWarn > 0 ? 'Advisory items' : 'None'} |`);
111
+ lines.push(`| inconclusive | ${totalInconclusive} | ${totalInconclusive > 0 ? 'Requires additional verification' : 'None'} |`);
112
+ lines.push('');
113
+ lines.push(`**Conclusion:** ${
114
+ overallStatus === 'pass'
115
+ ? `All ${totalChecks} formal properties verified.`
116
+ : overallStatus === 'inconclusive'
117
+ ? `${totalInconclusive} inconclusive result(s); review fairness assumptions.`
118
+ : `${totalFail} formal propert${totalFail === 1 ? 'y' : 'ies'} failed; implementation requires revision.`
119
+ }`);
120
+
121
+ return lines.join('\n');
122
+ }
123
+
124
+ module.exports = { parseNDJSON, groupByFormalism, generateFVSection };