@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
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
- // bin/qgsd-solve.cjs
3
+ // bin/nf-solve.cjs
4
4
  // Consistency solver orchestrator: sweeps Requirements->Formal->Tests->Code->Docs,
5
5
  // computes a residual vector per layer transition, and auto-closes gaps.
6
6
  //
7
- // Layer transitions (8 forward + 3 reverse):
7
+ // Layer transitions (8 forward + 3 reverse + 3 layer alignment):
8
8
  // R->F: Requirements without formal model coverage
9
9
  // F->T: Formal invariants without test backing
10
10
  // C->F: Code constants diverging from formal specs
@@ -17,22 +17,27 @@
17
17
  // C->R: Source modules in bin/hooks/ with no requirement tracing
18
18
  // T->R: Test files with no @req annotation or formal-test-sync mapping
19
19
  // D->R: Doc capability claims without requirement backing
20
+ // Layer alignment (cross-layer gate checks):
21
+ // L1->L2: Gate A grounding alignment score
22
+ // L2->L3: Gate B traceability alignment score
23
+ // L3->TC: Gate C validation alignment score
20
24
  //
21
25
  // Usage:
22
- // node bin/qgsd-solve.cjs # full sync, up to 3 iterations
23
- // node bin/qgsd-solve.cjs --report-only # single sweep, no mutations
24
- // node bin/qgsd-solve.cjs --max-iterations=1
25
- // node bin/qgsd-solve.cjs --json # machine-readable output
26
- // node bin/qgsd-solve.cjs --verbose # pipe child stderr to parent stderr
27
- // node bin/qgsd-solve.cjs --fast # skip F->C and T->C layers for sub-second iteration
26
+ // node bin/nf-solve.cjs # full sync, up to 3 iterations
27
+ // node bin/nf-solve.cjs --report-only # single sweep, no mutations
28
+ // node bin/nf-solve.cjs --max-iterations=1
29
+ // node bin/nf-solve.cjs --json # machine-readable output
30
+ // node bin/nf-solve.cjs --verbose # pipe child stderr to parent stderr
31
+ // node bin/nf-solve.cjs --fast # skip F->C and T->C layers for sub-second iteration
28
32
  //
29
33
  // Requirements: QUICK-140
30
34
 
31
35
  const fs = require('fs');
32
36
  const path = require('path');
37
+ const os = require('os');
33
38
  const { spawnSync } = require('child_process');
34
39
 
35
- const TAG = '[qgsd-solve]';
40
+ const TAG = '[nf-solve]';
36
41
  let ROOT = process.cwd();
37
42
  const SCRIPT_DIR = __dirname;
38
43
  const DEFAULT_MAX_ITERATIONS = 3;
@@ -249,7 +254,7 @@ function discoverDocFiles() {
249
254
  const subWarnings = detectUninitializedSubmodules(marker.docs);
250
255
  for (const w of subWarnings) {
251
256
  console.error(
252
- `[qgsd-solve] WARNING: docs.${w.docKey} overlaps submodule "${w.name}" ` +
257
+ `[nf-solve] WARNING: docs.${w.docKey} overlaps submodule "${w.name}" ` +
253
258
  `(${w.submodule}) which is not initialized. Run: git submodule update --init ${w.submodule}`
254
259
  );
255
260
  }
@@ -748,6 +753,15 @@ function sweepTtoC() {
748
753
  }
749
754
 
750
755
  // Runner mode: node-test (default)
756
+ // V8 coverage collection: create temp dir and set NODE_V8_COVERAGE env var
757
+ let covDir = null;
758
+ try {
759
+ covDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nf-solve-cov-'));
760
+ spawnOpts.env = Object.assign({}, process.env, { NODE_V8_COVERAGE: covDir });
761
+ } catch (e) {
762
+ covDir = null; // fail-open: continue without coverage
763
+ }
764
+
751
765
  let result;
752
766
  try {
753
767
  result = spawnSync(process.execPath, ['--test'], spawnOpts);
@@ -789,6 +803,26 @@ function sweepTtoC() {
789
803
  todoCount = 0;
790
804
  }
791
805
 
806
+ // Collect V8 coverage data from temp directory (fail-open)
807
+ let coverageData = null;
808
+ try {
809
+ if (covDir && fs.existsSync(covDir)) {
810
+ const covFiles = fs.readdirSync(covDir).filter(f => f.endsWith('.json'));
811
+ coverageData = [];
812
+ for (const cf of covFiles) {
813
+ const raw = fs.readFileSync(path.join(covDir, cf), 'utf8');
814
+ coverageData.push(JSON.parse(raw));
815
+ }
816
+ if (coverageData.length === 0) coverageData = null;
817
+ }
818
+ } catch (e) {
819
+ coverageData = null; // fail-open
820
+ } finally {
821
+ try {
822
+ if (covDir) fs.rmSync(covDir, { recursive: true, force: true });
823
+ } catch (e) { /* ignore cleanup errors */ }
824
+ }
825
+
792
826
  // Scope-based auto-detection: if scope is "generated-stubs-only", check if all failures
793
827
  // are outside .planning/formal/generated-stubs/
794
828
  if (tToCConfig.runner === 'node-test' && tToCConfig.scope === 'generated-stubs-only' && failCount > 0) {
@@ -805,6 +839,7 @@ function sweepTtoC() {
805
839
  todo: todoCount,
806
840
  runner_mismatch: true,
807
841
  warning: 'All ' + failLines.length + ' failures are outside generated-stubs scope — likely runner mismatch',
842
+ v8_coverage: coverageData,
808
843
  },
809
844
  };
810
845
  }
@@ -818,10 +853,93 @@ function sweepTtoC() {
818
853
  failed: failCount,
819
854
  skipped: skipCount,
820
855
  todo: todoCount,
856
+ v8_coverage: coverageData,
821
857
  },
822
858
  };
823
859
  }
824
860
 
861
+ /**
862
+ * Cross-reference V8 coverage data against formal-test-sync recipe source_files.
863
+ * Identifies "false green" properties: tests pass but exercise none of the implementing source files.
864
+ * Returns { available: false } when coverage data is null/undefined.
865
+ */
866
+ function crossReferenceFormalCoverage(v8CoverageData) {
867
+ if (!v8CoverageData) return { available: false };
868
+
869
+ try {
870
+ const syncData = loadFormalTestSync();
871
+ const recipes = (syncData && syncData.recipes) ? syncData.recipes : [];
872
+
873
+ // Build set of covered absolute file paths from V8 data
874
+ const coveredFiles = new Set();
875
+ for (const entry of v8CoverageData) {
876
+ const results = entry.result || [];
877
+ for (const r of results) {
878
+ if (!r.url) continue;
879
+ const filePath = r.url.startsWith('file://') ? r.url.slice(7) : r.url;
880
+ const resolved = path.resolve(filePath);
881
+ // A file is "covered" if ANY function range has count > 0
882
+ const hasCoverage = (r.functions || []).some(fn =>
883
+ (fn.ranges || []).some(range => range.count > 0)
884
+ );
885
+ if (hasCoverage) coveredFiles.add(resolved);
886
+ }
887
+ }
888
+
889
+ const coverageRatios = [];
890
+ const falseGreens = [];
891
+ let propertiesWithTests = 0;
892
+ let fullyCovered = 0;
893
+ let partiallyCovered = 0;
894
+ let uncovered = 0;
895
+
896
+ for (const recipe of recipes) {
897
+ const sourceFiles = recipe.source_files_absolute || [];
898
+ if (sourceFiles.length === 0) continue;
899
+ const hasTest = !!(recipe.test_file || recipe.test_files);
900
+ if (hasTest) propertiesWithTests++;
901
+
902
+ let coveredCount = 0;
903
+ for (const sf of sourceFiles) {
904
+ if (coveredFiles.has(path.resolve(sf))) coveredCount++;
905
+ }
906
+ const ratio = coveredCount / sourceFiles.length;
907
+ const propName = recipe.property || recipe.invariant || recipe.id || 'unknown';
908
+
909
+ coverageRatios.push({ property: propName, ratio: ratio });
910
+
911
+ if (ratio === 0 && hasTest) {
912
+ falseGreens.push({
913
+ property: propName,
914
+ test_file: recipe.test_file || (recipe.test_files || [])[0] || 'unknown',
915
+ source_files: sourceFiles,
916
+ covered: 0,
917
+ });
918
+ uncovered++;
919
+ } else if (ratio < 1) {
920
+ partiallyCovered++;
921
+ } else {
922
+ fullyCovered++;
923
+ }
924
+ }
925
+
926
+ return {
927
+ available: true,
928
+ total_properties: recipes.length,
929
+ properties_with_tests: propertiesWithTests,
930
+ false_greens: falseGreens,
931
+ coverage_ratios: coverageRatios,
932
+ summary: {
933
+ fully_covered: fullyCovered,
934
+ partially_covered: partiallyCovered,
935
+ uncovered: uncovered,
936
+ },
937
+ };
938
+ } catch (e) {
939
+ return { available: false };
940
+ }
941
+ }
942
+
825
943
  /**
826
944
  * F->C: Formal verification to Code.
827
945
  * Returns { residual: N, detail: {...} }
@@ -1818,11 +1936,147 @@ function assembleReverseCandidates(c_to_r, t_to_r, d_to_r) {
1818
1936
  };
1819
1937
  }
1820
1938
 
1939
+ // ── Layer alignment sweeps ────────────────────────────────────────────────────
1940
+
1941
+ /**
1942
+ * L1->L2: Gate A grounding alignment score.
1943
+ * Spawns gate-a-grounding.cjs --json and computes normalized 0-10 residual.
1944
+ * Returns { residual: N, detail: {...} }
1945
+ */
1946
+ function sweepL1toL2() {
1947
+ if (fastMode) {
1948
+ return { residual: -1, detail: { skipped: true, reason: 'fast mode' } };
1949
+ }
1950
+
1951
+ const result = spawnTool('bin/gate-a-grounding.cjs', ['--json']);
1952
+
1953
+ // Gate scripts exit 1 when target_met is false but still produce valid JSON.
1954
+ // Only bail on spawn errors (no stdout to parse).
1955
+ if (!result.ok && !result.stdout) {
1956
+ return { residual: -1, detail: { error: true, stderr: (result.stderr || '').slice(0, 500) } };
1957
+ }
1958
+
1959
+ try {
1960
+ const data = JSON.parse(result.stdout);
1961
+ const score = data.grounding_score || 0;
1962
+ const residual = Math.ceil((1 - score) * 10);
1963
+ return {
1964
+ residual: residual,
1965
+ detail: {
1966
+ grounding_score: score,
1967
+ target: 0.8,
1968
+ gap: 0.8 - score,
1969
+ unexplained_breakdown: {
1970
+ instrumentation_bug: (data.unexplained_counts && data.unexplained_counts.instrumentation_bug) || 0,
1971
+ model_gap: (data.unexplained_counts && data.unexplained_counts.model_gap) || 0,
1972
+ genuine_violation: (data.unexplained_counts && data.unexplained_counts.genuine_violation) || 0,
1973
+ },
1974
+ },
1975
+ };
1976
+ } catch (err) {
1977
+ return { residual: -1, detail: { error: true, stderr: 'JSON parse failed: ' + err.message } };
1978
+ }
1979
+ }
1980
+
1981
+ /**
1982
+ * L2->L3: Gate B traceability alignment score.
1983
+ * Spawns gate-b-abstraction.cjs --json and computes normalized 0-10 residual.
1984
+ * Returns { residual: N, detail: {...} }
1985
+ */
1986
+ function sweepL2toL3() {
1987
+ if (fastMode) {
1988
+ return { residual: -1, detail: { skipped: true, reason: 'fast mode' } };
1989
+ }
1990
+
1991
+ const result = spawnTool('bin/gate-b-abstraction.cjs', ['--json']);
1992
+
1993
+ // Gate scripts exit 1 when target_met is false but still produce valid JSON.
1994
+ // Only bail on spawn errors (no stdout to parse).
1995
+ if (!result.ok && !result.stdout) {
1996
+ return { residual: -1, detail: { error: true } };
1997
+ }
1998
+
1999
+ try {
2000
+ const data = JSON.parse(result.stdout);
2001
+ const score = data.gate_b_score || 0;
2002
+ const orphanedCount = data.orphaned_entries || 0;
2003
+ const rawResidual = Math.ceil((1 - score) * 10) + orphanedCount;
2004
+ const residual = Math.min(rawResidual, 10);
2005
+ return {
2006
+ residual: residual,
2007
+ detail: {
2008
+ gate_b_score: score,
2009
+ orphaned_count: orphanedCount,
2010
+ residual_capped: rawResidual > 10,
2011
+ },
2012
+ };
2013
+ } catch (err) {
2014
+ return { residual: -1, detail: { error: true } };
2015
+ }
2016
+ }
2017
+
2018
+ /**
2019
+ * L3->TC: Gate C validation alignment score.
2020
+ * Spawns gate-c-validation.cjs --json and computes normalized 0-10 residual.
2021
+ * Checks test-recipes.json staleness before scoring.
2022
+ * Returns { residual: N, detail: {...} }
2023
+ */
2024
+ function sweepL3toTC() {
2025
+ if (fastMode) {
2026
+ return { residual: -1, detail: { skipped: true, reason: 'fast mode' } };
2027
+ }
2028
+
2029
+ // Check if test-recipes.json exists and staleness
2030
+ const recipesPath = path.join(ROOT, '.planning', 'formal', 'test-recipes', 'test-recipes.json');
2031
+ const catalogPath = path.join(ROOT, '.planning', 'formal', 'reasoning', 'failure-mode-catalog.json');
2032
+
2033
+ if (fs.existsSync(recipesPath) && fs.existsSync(catalogPath)) {
2034
+ try {
2035
+ const recipesMtime = fs.statSync(recipesPath).mtimeMs;
2036
+ const catalogMtime = fs.statSync(catalogPath).mtimeMs;
2037
+ if (recipesMtime < catalogMtime) {
2038
+ if (reportOnly) {
2039
+ process.stderr.write(TAG + ' WARNING: test-recipes.json is stale; run test-recipe-gen.cjs to update\n');
2040
+ } else {
2041
+ spawnTool('bin/test-recipe-gen.cjs', []);
2042
+ }
2043
+ }
2044
+ } catch (e) {
2045
+ // fail-open: skip staleness check
2046
+ }
2047
+ }
2048
+
2049
+ const result = spawnTool('bin/gate-c-validation.cjs', ['--json']);
2050
+
2051
+ // Gate scripts exit 1 when target_met is false but still produce valid JSON.
2052
+ // Only bail on spawn errors (no stdout to parse).
2053
+ if (!result.ok && !result.stdout) {
2054
+ return { residual: -1, detail: { error: true, stderr: (result.stderr || '').slice(0, 500) } };
2055
+ }
2056
+
2057
+ try {
2058
+ const data = JSON.parse(result.stdout);
2059
+ const score = data.gate_c_score || 0;
2060
+ const residual = Math.ceil((1 - score) * 10);
2061
+ return {
2062
+ residual: residual,
2063
+ detail: {
2064
+ gate_c_score: score,
2065
+ unvalidated_count: data.unvalidated_entries || 0,
2066
+ total_failure_modes: data.total_entries || 0,
2067
+ total_recipes: data.validated_entries || 0,
2068
+ },
2069
+ };
2070
+ } catch (err) {
2071
+ return { residual: -1, detail: { error: true, stderr: 'JSON parse failed: ' + err.message } };
2072
+ }
2073
+ }
2074
+
1821
2075
  // ── Residual computation ─────────────────────────────────────────────────────
1822
2076
 
1823
2077
  /**
1824
- * Computes residual vector for all layer transitions (8 forward + 3 reverse).
1825
- * Returns residual object with forward layers + reverse discovery layers.
2078
+ * Computes residual vector for all layer transitions (8 forward + 3 reverse + 3 layer alignment).
2079
+ * Returns residual object with forward layers + reverse discovery layers + layer alignment.
1826
2080
  */
1827
2081
  function computeResidual() {
1828
2082
  const r_to_f = sweepRtoF();
@@ -1831,6 +2085,12 @@ function computeResidual() {
1831
2085
  const t_to_c = fastMode
1832
2086
  ? { residual: -1, detail: { skipped: true, reason: 'fast mode' } }
1833
2087
  : sweepTtoC();
2088
+
2089
+ // Cross-reference V8 coverage against formal-test-sync recipe source_files
2090
+ if (t_to_c.detail && t_to_c.detail.v8_coverage) {
2091
+ t_to_c.detail.formal_coverage = crossReferenceFormalCoverage(t_to_c.detail.v8_coverage);
2092
+ }
2093
+
1834
2094
  const f_to_c = fastMode
1835
2095
  ? { residual: -1, detail: { skipped: true, reason: 'fast mode' } }
1836
2096
  : sweepFtoC();
@@ -1861,6 +2121,17 @@ function computeResidual() {
1861
2121
  // Assemble deduplicated reverse candidates
1862
2122
  const assembled_candidates = assembleReverseCandidates(c_to_r, t_to_r, d_to_r);
1863
2123
 
2124
+ // Layer alignment sweeps (cross-layer gate checks) — skip in fast mode
2125
+ const skipLayer = { residual: -1, detail: { skipped: true, reason: 'fast mode' } };
2126
+ const l1_to_l2 = fastMode ? skipLayer : sweepL1toL2();
2127
+ const l2_to_l3 = fastMode ? skipLayer : sweepL2toL3();
2128
+ const l3_to_tc = fastMode ? skipLayer : sweepL3toTC();
2129
+
2130
+ const layer_total =
2131
+ (l1_to_l2.residual >= 0 ? l1_to_l2.residual : 0) +
2132
+ (l2_to_l3.residual >= 0 ? l2_to_l3.residual : 0) +
2133
+ (l3_to_tc.residual >= 0 ? l3_to_tc.residual : 0);
2134
+
1864
2135
  return {
1865
2136
  r_to_f,
1866
2137
  f_to_t,
@@ -1873,8 +2144,12 @@ function computeResidual() {
1873
2144
  c_to_r,
1874
2145
  t_to_r,
1875
2146
  d_to_r,
2147
+ l1_to_l2,
2148
+ l2_to_l3,
2149
+ l3_to_tc,
1876
2150
  assembled_candidates,
1877
2151
  total,
2152
+ layer_total,
1878
2153
  reverse_discovery_total,
1879
2154
  timestamp: new Date().toISOString(),
1880
2155
  };
@@ -1999,7 +2274,7 @@ function healthIndicator(residual) {
1999
2274
  function formatReport(iterations, finalResidual, converged) {
2000
2275
  const lines = [];
2001
2276
 
2002
- lines.push('[qgsd-solve] Consistency Solver Report');
2277
+ lines.push('[nf-solve] Consistency Solver Report');
2003
2278
  lines.push('');
2004
2279
  lines.push(
2005
2280
  'Iterations: ' +
@@ -2097,6 +2372,30 @@ function formatReport(iterations, finalResidual, converged) {
2097
2372
  }
2098
2373
  }
2099
2374
  }
2375
+
2376
+ // Layer Alignment section
2377
+ if (finalResidual.l1_to_l2 || finalResidual.l2_to_l3 || finalResidual.l3_to_tc) {
2378
+ lines.push('');
2379
+ lines.push('Layer Alignment (cross-layer gate checks):');
2380
+ lines.push('─────────────────────────────────────────────');
2381
+
2382
+ const layerRows = [
2383
+ { label: 'L1 -> L2 (Gate A)', residual: finalResidual.l1_to_l2 ? finalResidual.l1_to_l2.residual : -1 },
2384
+ { label: 'L2 -> L3 (Gate B)', residual: finalResidual.l2_to_l3 ? finalResidual.l2_to_l3.residual : -1 },
2385
+ { label: 'L3 -> TC (Gate C)', residual: finalResidual.l3_to_tc ? finalResidual.l3_to_tc.residual : -1 },
2386
+ ];
2387
+
2388
+ for (const row of layerRows) {
2389
+ const res = row.residual >= 0 ? row.residual : '?';
2390
+ const health = healthIndicator(row.residual);
2391
+ const line = row.label.padEnd(28) + String(res).padStart(4) + ' ' + health;
2392
+ lines.push(line);
2393
+ }
2394
+
2395
+ const layerTotal = finalResidual.layer_total || 0;
2396
+ lines.push('─────────────────────────────────────────────');
2397
+ lines.push('Layer total: ' + layerTotal);
2398
+ }
2100
2399
  lines.push('');
2101
2400
 
2102
2401
  // Per-layer detail sections (only non-zero)
@@ -2161,6 +2460,16 @@ function formatReport(iterations, finalResidual, converged) {
2161
2460
  lines.push('');
2162
2461
  }
2163
2462
 
2463
+ // F->T->C coverage summary (shown regardless of T->C residual)
2464
+ if (finalResidual.t_to_c.detail && finalResidual.t_to_c.detail.formal_coverage &&
2465
+ finalResidual.t_to_c.detail.formal_coverage.available === true) {
2466
+ const fc = finalResidual.t_to_c.detail.formal_coverage;
2467
+ lines.push(' F->T->C coverage: ' + fc.summary.fully_covered + '/' +
2468
+ fc.total_properties + ' properties fully traced (' +
2469
+ fc.false_greens.length + ' false greens)');
2470
+ lines.push('');
2471
+ }
2472
+
2164
2473
  if (finalResidual.f_to_c.residual > 0 || (finalResidual.f_to_c.detail && finalResidual.f_to_c.detail.inconclusive > 0)) {
2165
2474
  lines.push('## F -> C (Formal -> Code)');
2166
2475
  const detail = finalResidual.f_to_c.detail;
@@ -2324,7 +2633,7 @@ function truncateResidualDetail(residual) {
2324
2633
  */
2325
2634
  function formatJSON(iterations, finalResidual, converged) {
2326
2635
  const health = {};
2327
- for (const key of ['r_to_f', 'f_to_t', 'c_to_f', 't_to_c', 'f_to_c', 'r_to_d', 'd_to_c', 'p_to_f', 'c_to_r', 't_to_r', 'd_to_r']) {
2636
+ for (const key of ['r_to_f', 'f_to_t', 'c_to_f', 't_to_c', 'f_to_c', 'r_to_d', 'd_to_c', 'p_to_f', 'c_to_r', 't_to_r', 'd_to_r', 'l1_to_l2', 'l2_to_l3', 'l3_to_tc']) {
2328
2637
  const res = finalResidual[key] ? finalResidual[key].residual : -1;
2329
2638
  health[key] = healthIndicator(res).split(/\s+/)[1]; // Extract GREEN/YELLOW/RED/UNKNOWN
2330
2639
  }
@@ -2466,8 +2775,12 @@ module.exports = {
2466
2775
  sweepCtoR,
2467
2776
  sweepTtoR,
2468
2777
  sweepDtoR,
2778
+ sweepL1toL2,
2779
+ sweepL2toL3,
2780
+ sweepL3toTC,
2469
2781
  assembleReverseCandidates,
2470
2782
  classifyCandidate,
2783
+ crossReferenceFormalCoverage,
2471
2784
  };
2472
2785
 
2473
2786
  // ── Entry point ──────────────────────────────────────────────────────────────
@@ -11,6 +11,10 @@ const path = require('node:path');
11
11
  const ISSUE_TYPES = ['github', 'sentry', 'sentry-feedback', 'bash'];
12
12
  // Source types that default to "drift"
13
13
  const DRIFT_TYPES = ['prometheus', 'grafana', 'logstash'];
14
+ // Source types that default to "upstream"
15
+ const UPSTREAM_TYPES = ['upstream'];
16
+ // Source types that default to "deps"
17
+ const DEPS_TYPES = ['deps'];
14
18
 
15
19
  /**
16
20
  * Parse a YAML value string into appropriate JS type
@@ -324,6 +328,10 @@ function loadObserveConfig(configPath, basePath) {
324
328
  source.issue_type = 'issue';
325
329
  } else if (DRIFT_TYPES.includes(source.type)) {
326
330
  source.issue_type = 'drift';
331
+ } else if (UPSTREAM_TYPES.includes(source.type)) {
332
+ source.issue_type = 'upstream';
333
+ } else if (DEPS_TYPES.includes(source.type)) {
334
+ source.issue_type = 'deps';
327
335
  }
328
336
  }
329
337
 
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Debt ledger write-through for /qgsd:observe
2
+ * Debt ledger write-through for /nf:observe
3
3
  * Upserts observations to .planning/formal/debt.json by fingerprint using v0.27-01 functions
4
4
  * Then runs dedup engine and formal reference linker (v0.27-03)
5
5
  *