@3f-consulting/spec-controller-core 0.1.0-alpha.4 → 0.1.0-alpha.6

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 (120) hide show
  1. package/README.md +3 -1
  2. package/dist/assemble/evidenceReconciliationFromSources.d.ts +4 -13
  3. package/dist/assemble/evidenceReconciliationFromSources.d.ts.map +1 -1
  4. package/dist/assemble/evidenceReconciliationFromSources.js +17 -22
  5. package/dist/assemble/evidenceReconciliationFromSources.js.map +1 -1
  6. package/dist/domain/checkCharacteristics.d.ts +22 -15
  7. package/dist/domain/checkCharacteristics.d.ts.map +1 -1
  8. package/dist/domain/checkCharacteristics.js +22 -15
  9. package/dist/domain/checkCharacteristics.js.map +1 -1
  10. package/dist/domain/evidenceReconciliation.d.ts +26 -2
  11. package/dist/domain/evidenceReconciliation.d.ts.map +1 -1
  12. package/dist/domain/evidenceReconciliation.js +164 -61
  13. package/dist/domain/evidenceReconciliation.js.map +1 -1
  14. package/dist/domain/fitnessChecks.d.ts +24 -35
  15. package/dist/domain/fitnessChecks.d.ts.map +1 -1
  16. package/dist/domain/fitnessChecks.js +3 -30
  17. package/dist/domain/fitnessChecks.js.map +1 -1
  18. package/dist/domain/journal.d.ts +13 -103
  19. package/dist/domain/journal.d.ts.map +1 -1
  20. package/dist/domain/journal.js +6 -19
  21. package/dist/domain/journal.js.map +1 -1
  22. package/dist/domain/ledger.d.ts.map +1 -1
  23. package/dist/domain/ledger.js +45 -34
  24. package/dist/domain/ledger.js.map +1 -1
  25. package/dist/domain/posting.d.ts.map +1 -1
  26. package/dist/domain/posting.js +4 -7
  27. package/dist/domain/posting.js.map +1 -1
  28. package/dist/domain/qualityCheckFaultMessages.d.ts +7 -2
  29. package/dist/domain/qualityCheckFaultMessages.d.ts.map +1 -1
  30. package/dist/domain/qualityCheckFaultMessages.js +7 -2
  31. package/dist/domain/qualityCheckFaultMessages.js.map +1 -1
  32. package/dist/domain/qualityCheckReconciliation.d.ts +21 -0
  33. package/dist/domain/qualityCheckReconciliation.d.ts.map +1 -1
  34. package/dist/domain/qualityCheckReconciliation.js +40 -105
  35. package/dist/domain/qualityCheckReconciliation.js.map +1 -1
  36. package/dist/domain/qualityCheckReconciliationFault.d.ts +3 -1
  37. package/dist/domain/qualityCheckReconciliationFault.d.ts.map +1 -1
  38. package/dist/domain/qualityCheckReconciliationFault.js +3 -1
  39. package/dist/domain/qualityCheckReconciliationFault.js.map +1 -1
  40. package/dist/index.d.ts +0 -1
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +0 -1
  43. package/dist/index.js.map +1 -1
  44. package/dist/internal.d.ts +2 -10
  45. package/dist/internal.d.ts.map +1 -1
  46. package/dist/internal.js +24 -71
  47. package/dist/internal.js.map +1 -1
  48. package/dist/parse/checkScriptsRunInCI.d.ts +8 -7
  49. package/dist/parse/checkScriptsRunInCI.d.ts.map +1 -1
  50. package/dist/parse/checkScriptsRunInCI.js +7 -6
  51. package/dist/parse/checkScriptsRunInCI.js.map +1 -1
  52. package/dist/parse/ciStepRecord.d.ts +5 -17
  53. package/dist/parse/ciStepRecord.d.ts.map +1 -1
  54. package/dist/parse/ciStepRecord.js +7 -27
  55. package/dist/parse/ciStepRecord.js.map +1 -1
  56. package/dist/parse/corpusCodeFaults.d.ts +4 -4
  57. package/dist/parse/corpusCodeFaults.js +4 -4
  58. package/dist/parse/corpusFaults.d.ts +24 -36
  59. package/dist/parse/corpusFaults.d.ts.map +1 -1
  60. package/dist/parse/corpusFaults.js +16 -32
  61. package/dist/parse/corpusFaults.js.map +1 -1
  62. package/dist/parse/corpusGrammarFaults.d.ts +11 -73
  63. package/dist/parse/corpusGrammarFaults.d.ts.map +1 -1
  64. package/dist/parse/corpusGrammarFaults.js +27 -58
  65. package/dist/parse/corpusGrammarFaults.js.map +1 -1
  66. package/dist/parse/runningStaticChecks.d.ts +3 -2
  67. package/dist/parse/runningStaticChecks.d.ts.map +1 -1
  68. package/dist/parse/runningStaticChecks.js +3 -2
  69. package/dist/parse/runningStaticChecks.js.map +1 -1
  70. package/dist/parse/scenarios.d.ts +27 -12
  71. package/dist/parse/scenarios.d.ts.map +1 -1
  72. package/dist/parse/scenarios.js +47 -85
  73. package/dist/parse/scenarios.js.map +1 -1
  74. package/dist/parse/targetCorpus.d.ts +21 -11
  75. package/dist/parse/targetCorpus.d.ts.map +1 -1
  76. package/dist/parse/targetCorpus.js +16 -19
  77. package/dist/parse/targetCorpus.js.map +1 -1
  78. package/dist/parse/targetManifest.d.ts +10 -85
  79. package/dist/parse/targetManifest.d.ts.map +1 -1
  80. package/dist/parse/targetManifest.js +47 -137
  81. package/dist/parse/targetManifest.js.map +1 -1
  82. package/dist/provenance/provenance.d.ts +8 -11
  83. package/dist/provenance/provenance.d.ts.map +1 -1
  84. package/dist/provenance/provenance.js +2 -3
  85. package/dist/provenance/provenance.js.map +1 -1
  86. package/dist/render/evidenceGrid.d.ts +45 -0
  87. package/dist/render/evidenceGrid.d.ts.map +1 -0
  88. package/dist/render/evidenceGrid.js +145 -0
  89. package/dist/render/evidenceGrid.js.map +1 -0
  90. package/dist/render/markdown.d.ts.map +1 -1
  91. package/dist/render/markdown.js +85 -190
  92. package/dist/render/markdown.js.map +1 -1
  93. package/dist/render/runVerdict.d.ts +3 -2
  94. package/dist/render/runVerdict.d.ts.map +1 -1
  95. package/dist/render/runVerdict.js +3 -2
  96. package/dist/render/runVerdict.js.map +1 -1
  97. package/dist/render/staticChecks.d.ts.map +1 -1
  98. package/dist/render/staticChecks.js +10 -1
  99. package/dist/render/staticChecks.js.map +1 -1
  100. package/package.json +1 -1
  101. package/dist/domain/thresholds.d.ts +0 -123
  102. package/dist/domain/thresholds.d.ts.map +0 -1
  103. package/dist/domain/thresholds.js +0 -185
  104. package/dist/domain/thresholds.js.map +0 -1
  105. package/dist/parse/checkStaticEvidenceObligations.d.ts +0 -32
  106. package/dist/parse/checkStaticEvidenceObligations.d.ts.map +0 -1
  107. package/dist/parse/checkStaticEvidenceObligations.js +0 -78
  108. package/dist/parse/checkStaticEvidenceObligations.js.map +0 -1
  109. package/dist/parse/corpusClaimFaults.d.ts +0 -104
  110. package/dist/parse/corpusClaimFaults.d.ts.map +0 -1
  111. package/dist/parse/corpusClaimFaults.js +0 -128
  112. package/dist/parse/corpusClaimFaults.js.map +0 -1
  113. package/dist/parse/measurements.d.ts +0 -47
  114. package/dist/parse/measurements.d.ts.map +0 -1
  115. package/dist/parse/measurements.js +0 -65
  116. package/dist/parse/measurements.js.map +0 -1
  117. package/dist/render/measuredItem.d.ts +0 -30
  118. package/dist/render/measuredItem.d.ts.map +0 -1
  119. package/dist/render/measuredItem.js +0 -43
  120. package/dist/render/measuredItem.js.map +0 -1
@@ -24,8 +24,7 @@ const INPUT_FLAGS = [
24
24
  // APPENDED, so no flag that was already rendered moves (@SCN-RPT-027). Widening this
25
25
  // list is the whole of the change: `presentInputs`, `provenanceObject`,
26
26
  // `projectProvenanceObject`, the markdown header and the JSON object all iterate it, so
27
- // the two surfaces gain the streams in step and neither can drift from the other.
28
- { key: "measurements", flag: "--measurements" },
27
+ // the two surfaces gain the stream in step and neither can drift from the other.
29
28
  { key: "ciSteps", flag: "--ci-steps" },
30
29
  ];
31
30
  /** Format an instant as second-precision UTC, e.g. `2026-06-27T08:17:53Z`. */
@@ -82,7 +81,7 @@ export function projectProvenanceObject(obj) {
82
81
  // The flags with no value, derived in the SAME pass over the SAME canonical list that
83
82
  // produces the flags with one (@SCN-RPT-028). One list, one traversal, two complementary
84
83
  // outputs — so a flag can never be named on both lines, nor missing from both, and adding
85
- // an eighth input flag reaches the absence line without anyone remembering to.
84
+ // another input flag reaches the absence line without anyone remembering to.
86
85
  const absentInputs = [];
87
86
  for (const { key, flag } of INPUT_FLAGS) {
88
87
  const value = obj.inputs[key];
@@ -1 +1 @@
1
- {"version":3,"file":"provenance.js","sourceRoot":"","sources":["../../src/provenance/provenance.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"provenance.js","sourceRoot":"","sources":["../../src/provenance/provenance.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * THE LEDGER'S EVIDENCE GRID — the corpus-driven evidence columns the Scenario Reconciliations table
3
+ * gains when the runtime-observation axis is in play, and the two-sided cell under each.
4
+ *
5
+ * Its own module rather than a block in `markdown.ts`, moved out whole at 3F-3524 so the ledger
6
+ * section's split into functions under the complexity and function-length ceilings did not push
7
+ * the file past its `max-lines`; `staticChecks.ts` and `runVerdict.ts` are the precedent. NOTHING
8
+ * about the grid changed in the move: same columns, same headers, same cells.
9
+ */
10
+ import type { ScenarioReconciliation, RuntimeQualityCheck } from "../model.js";
11
+ /**
12
+ * One corpus-driven evidence column in the ledger: the lint (static) column,
13
+ * or a runtime quality-check column. The column SET is corpus-driven — only the quality-checks
14
+ * the run raises or observes appear, so e2e / performance show up only when present.
15
+ */
16
+ export type EvidenceColumn = {
17
+ kind: "lint";
18
+ } | {
19
+ kind: "runtime";
20
+ qualityCheck: RuntimeQualityCheck;
21
+ };
22
+ /**
23
+ * The corpus-driven evidence columns: lint first (when any scenario names a static check), then
24
+ * each runtime quality-check present as an EVIDENCE OBLIGATION or as a posted EVIDENCE OBSERVATION
25
+ * on a scenario row, in canonical quality-check order.
26
+ */
27
+ export declare function evidenceColumns(scenarios: readonly ScenarioReconciliation[]): EvidenceColumn[];
28
+ /** The column's header — the tag word, title-cased (Lint / Unit / Integration / …). */
29
+ export declare function columnHeader(col: EvidenceColumn): string;
30
+ /**
31
+ * One evidence cell — the two-sided `<obligation>/<observation>` encoding. The LEFT side is the
32
+ * EVIDENCE OBLIGATION (a single quality-check letter iff the scenario obligated this check, else `-`); the RIGHT
33
+ * side is the OBSERVATION count (the multiplicity of posted credits at this check, else `-`). It
34
+ * projects the two-sided ledger row — it reconstructs nothing. It counts BACKING (presence), NOT
35
+ * quality: a failing runtime evidenceObligation still shows its credit count (the failing-ness lives
36
+ * in the Verdict + Reconciling-items columns), so `U/-` is unambiguously named-but-missing (zero
37
+ * evidenceObservations). A `-/N` cell is a NO-EVIDENCE-OBLIGATION observation (a credit at a check the scenario
38
+ * did not name, @SCN-LDG-008); a doubly-empty `-/-` renders BLANK (`""`) — no evidence obligation, no credit.
39
+ * The lint cell reads the scenario's static evidenceObligation outcomes (lint is not a counted
40
+ * evidence observation): its right side is the count of static checks PRESENT — `balanced` or the
41
+ * watermelon (the script exists; its inadequacy is quality, in Reconciling-items 🍉), NOT a posted
42
+ * credit. The count exposes backing, not quality — never rank by it.
43
+ */
44
+ export declare function evidenceCell(s: ScenarioReconciliation, col: EvidenceColumn): string;
45
+ //# sourceMappingURL=evidenceGrid.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"evidenceGrid.d.ts","sourceRoot":"","sources":["../../src/render/evidenceGrid.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,sBAAsB,EAAgB,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAS7F;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,YAAY,EAAE,mBAAmB,CAAA;CAAE,CAAC;AAEvG;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,SAAS,sBAAsB,EAAE,GAC3C,cAAc,EAAE,CAgClB;AAED,uFAAuF;AACvF,wBAAgB,YAAY,CAAC,GAAG,EAAE,cAAc,GAAG,MAAM,CAGxD;AAiBD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,YAAY,CAC1B,CAAC,EAAE,sBAAsB,EACzB,GAAG,EAAE,cAAc,GAClB,MAAM,CAMR"}
@@ -0,0 +1,145 @@
1
+ /**
2
+ * THE LEDGER'S EVIDENCE GRID — the corpus-driven evidence columns the Scenario Reconciliations table
3
+ * gains when the runtime-observation axis is in play, and the two-sided cell under each.
4
+ *
5
+ * Its own module rather than a block in `markdown.ts`, moved out whole at 3F-3524 so the ledger
6
+ * section's split into functions under the complexity and function-length ceilings did not push
7
+ * the file past its `max-lines`; `staticChecks.ts` and `runVerdict.ts` are the precedent. NOTHING
8
+ * about the grid changed in the move: same columns, same headers, same cells.
9
+ */
10
+ import { RUNTIME_QUALITY_CHECKS } from "../domain/ledger.js";
11
+ import { isScenario, staticCheckReconciliationsOf, presentStaticCheckCountOf, } from "../domain/scenarioReconciliation.js";
12
+ import { evidenceObligationsOf } from "../domain/journal.js";
13
+ /**
14
+ * The corpus-driven evidence columns: lint first (when any scenario names a static check), then
15
+ * each runtime quality-check present as an EVIDENCE OBLIGATION or as a posted EVIDENCE OBSERVATION
16
+ * on a scenario row, in canonical quality-check order.
17
+ */
18
+ export function evidenceColumns(scenarios) {
19
+ // Only scenario rows carry evidence obligations that drive columns — a phantom / suspense
20
+ // row's credit-side items are not evidence obligations, so they never add a column.
21
+ const scenarioRows = scenarios.filter(isScenario);
22
+ // ASKED (3F-2203) — whether a row has static-check sub-rows is the same model cut the grid cell
23
+ // and the lint table read. Whether a COLUMN then appears is the renderer's own business; what a
24
+ // row's lint sub-rows ARE is not.
25
+ const hasStatic = scenarioRows.some((s) => staticCheckReconciliationsOf(s).length > 0);
26
+ const qualityChecksPresent = new Set();
27
+ // A column appears for every runtime quality-check present in EVIDENCE OBLIGATION or OBSERVATION on a scenario
28
+ // row. Both are read off the Ledger: a named or no-evidence-obligation quality-check surfaces as a
29
+ // `QualityCheckReconciliation` at that check; every observed credit surfaces as a posted credit on
30
+ // the row (@SCN-LDG-022). The retired census's `attributed[]` re-represented those posted credits
31
+ // — it added no quality-check the rows do not already carry (a matched/no-evidence-obligation credit always
32
+ // has its `QualityCheckReconciliation`), so folding the rows keeps the column set byte-identical.
33
+ for (const s of scenarioRows) {
34
+ for (const o of s.qualityCheckReconciliations) {
35
+ // A both-missing item carries no quality-check — it drives no column.
36
+ if (o.qualityCheck && o.qualityCheck !== "lint")
37
+ qualityChecksPresent.add(o.qualityCheck);
38
+ }
39
+ for (const o of s.evidenceObservations) {
40
+ if (o.qualityCheck !== "lint")
41
+ qualityChecksPresent.add(o.qualityCheck);
42
+ }
43
+ }
44
+ const columns = [];
45
+ if (hasStatic)
46
+ columns.push({ kind: "lint" });
47
+ for (const qualityCheck of RUNTIME_QUALITY_CHECKS) {
48
+ if (qualityChecksPresent.has(qualityCheck))
49
+ columns.push({ kind: "runtime", qualityCheck });
50
+ }
51
+ return columns;
52
+ }
53
+ /** The column's header — the tag word, title-cased (Lint / Unit / Integration / …). */
54
+ export function columnHeader(col) {
55
+ const word = col.kind === "lint" ? "lint" : col.qualityCheck;
56
+ return word.charAt(0).toUpperCase() + word.slice(1);
57
+ }
58
+ /**
59
+ * The quality-check → single-letter obligation glyph, parallel to `columnHeader` — the LEFT side
60
+ * of the two-sided cell (the evidence obligation). A single letter names WHICH check is obligated; the right
61
+ * side (after the `/`) carries the OBSERVATION count (the credit). Lint's left MAY carry a count
62
+ * suffix (`L2`/`L3`) when a scenario names more than one static check — the evidence-obligation
63
+ * count; the runtime letters stay countless (one obligation per tier).
64
+ */
65
+ const QUALITY_CHECK_LETTER = {
66
+ lint: "L",
67
+ unit: "U",
68
+ integration: "I",
69
+ e2e: "E",
70
+ performance: "P",
71
+ };
72
+ /**
73
+ * One evidence cell — the two-sided `<obligation>/<observation>` encoding. The LEFT side is the
74
+ * EVIDENCE OBLIGATION (a single quality-check letter iff the scenario obligated this check, else `-`); the RIGHT
75
+ * side is the OBSERVATION count (the multiplicity of posted credits at this check, else `-`). It
76
+ * projects the two-sided ledger row — it reconstructs nothing. It counts BACKING (presence), NOT
77
+ * quality: a failing runtime evidenceObligation still shows its credit count (the failing-ness lives
78
+ * in the Verdict + Reconciling-items columns), so `U/-` is unambiguously named-but-missing (zero
79
+ * evidenceObservations). A `-/N` cell is a NO-EVIDENCE-OBLIGATION observation (a credit at a check the scenario
80
+ * did not name, @SCN-LDG-008); a doubly-empty `-/-` renders BLANK (`""`) — no evidence obligation, no credit.
81
+ * The lint cell reads the scenario's static evidenceObligation outcomes (lint is not a counted
82
+ * evidence observation): its right side is the count of static checks PRESENT — `balanced` or the
83
+ * watermelon (the script exists; its inadequacy is quality, in Reconciling-items 🍉), NOT a posted
84
+ * credit. The count exposes backing, not quality — never rank by it.
85
+ */
86
+ export function evidenceCell(s, col) {
87
+ // A PHANTOM / suspense row has no evidence obligation of its own — its
88
+ // whole story is the credit-side reconciling item. Every evidence cell is neither-named-nor-
89
+ // matched, so it reads `—`; the citation shows in Reconciling items.
90
+ if (!isScenario(s))
91
+ return "—";
92
+ return col.kind === "lint" ? lintCell(s) : runtimeCell(s, col.qualityCheck);
93
+ }
94
+ /**
95
+ * The two sides joined — except a doubly-empty cell (no evidence obligation, no credit), which
96
+ * renders BLANK: the column is simply absent for this scenario.
97
+ */
98
+ function twoSided(left, right) {
99
+ return left === "-" && right === "-" ? "" : `${left}/${right}`;
100
+ }
101
+ /** The lint column's cell on a scenario row. */
102
+ function lintCell(s) {
103
+ // LEFT: a lint obligation is NAMED iff a lint QualityCheckReconciliation exists on the row
104
+ // (`L`; `L2`/`L3` when the row names >1 static check — the evidence-obligation count, one
105
+ // lint QCR per `@lint=<guard>` obligation). RIGHT: the PRESENT-guard count — the
106
+ // static checks `satisfied` or watermelon. Lint's right stays this static-outcome derivation
107
+ // (NOT `evidenceObservations`): lint carries no credit multiplicity, so the count is exact.
108
+ // BOTH SIDES ASKED (3F-2203) — the named-guard sub-rows and the present-guard count are
109
+ // model facts, read off the row. "Present" spans satisfied AND watermelon (a watermelon guard
110
+ // EXISTS; its inadequacy is quality, shown in Reconciling items 🍉) — an outcome-vocabulary
111
+ // rule, which is why the renderer no longer states it.
112
+ const statics = staticCheckReconciliationsOf(s);
113
+ const left = statics.length > 0
114
+ ? QUALITY_CHECK_LETTER.lint + (statics.length > 1 ? String(statics.length) : "")
115
+ : "-";
116
+ const present = presentStaticCheckCountOf(s);
117
+ const right = present > 0 ? String(present) : "-";
118
+ return twoSided(left, right);
119
+ }
120
+ /** A runtime quality-check column's cell on a scenario row. */
121
+ function runtimeCell(s, qualityCheck) {
122
+ // LEFT — EVIDENCE OBLIGATION is ASKED of the Ledger, not reconstructed. `s.evidenceObligations` is the debit
123
+ // side the scenario RAISED (@SCN-LDG-023) — the fact itself — filtered to the ACTIVE ones: a
124
+ // `<qc>pending` obligation names NOTHING (it partitions off to a one-sided Pending Item), so it
125
+ // is not an evidence obligation. Named → the check's letter; else `-`.
126
+ // ASKED, not re-encoded (3F-2203). "A pending obligation names nothing" is the model's rule
127
+ // (canon, Pending Item — a `<qc>pending` tag raises NO Evidence Obligation), and it is the SAME
128
+ // rule `evidenceObligationPairsOf` applies. The renderer stated the
129
+ // `!pending` clause itself before: a third home for one rule, agreeing by copy.
130
+ const named = evidenceObligationsOf(s.evidenceObligations).some((o) => o.qualityCheck === qualityCheck);
131
+ const left = named ? QUALITY_CHECK_LETTER[qualityCheck] : "-";
132
+ // RIGHT — the OBSERVATION count is the MULTIPLICITY of POSTED CREDITS at this quality-check on THIS
133
+ // row (@SCN-LDG-022) — read straight off the Ledger's `evidenceObservations`, NEVER from
134
+ // `qualityCheckReconciliations` (which collapse to ONE matched row however many credits cite the
135
+ // pair, and would undercount duplicate citations). BOTH the named and no-evidence-obligation arms read this
136
+ // one source: a named check's credits are matched-attributed, a no-evidence-obligation check's credits post
137
+ // as no-evidence-obligation rows — the same posted credits either way, so `evidenceObservations` is the single
138
+ // multiplicity-correct count. (Provably equal to the retired split off `qualityCheckReconciliations`
139
+ // for the no-evidence-obligation arm — the reconciler posts one no-evidence-obligation QCR per citing credit — but that
140
+ // derivation collapses duplicate citations, so the credits are the correct source.)
141
+ const observed = s.evidenceObservations.filter((o) => o.qualityCheck === qualityCheck).length;
142
+ const right = observed > 0 ? String(observed) : "-";
143
+ return twoSided(left, right);
144
+ }
145
+ //# sourceMappingURL=evidenceGrid.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"evidenceGrid.js","sourceRoot":"","sources":["../../src/render/evidenceGrid.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/render/markdown.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAiBH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAU5E,OAAO,EAAuB,KAAK,WAAW,EAAE,MAAM,4BAA4B,CAAC;AACnF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAoLlF;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GAAG,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAqlBvD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAExD;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,sBAAsB,EAAE,sBAAsB,EAC9C,GAAG,EAAE,UAAU,EACf,mBAAmB,CAAC,EAAE,mBAAmB,EACzC,YAAY,CAAC,EAAE,YAAY,GAC1B,MAAM,CAER"}
1
+ {"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/render/markdown.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAcH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAI5E,OAAO,EAAuB,KAAK,WAAW,EAAE,MAAM,4BAA4B,CAAC;AACnF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AA2LlF;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GAAG,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AA6dvD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAExD;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,sBAAsB,EAAE,sBAAsB,EAC9C,GAAG,EAAE,UAAU,EACf,mBAAmB,CAAC,EAAE,mBAAmB,EACzC,YAAY,CAAC,EAAE,YAAY,GAC1B,MAAM,CAER"}
@@ -25,12 +25,9 @@
25
25
  // decomposition walks (3F-1806): both live in the model, so the renderer cannot drift from the
26
26
  // union. Internal core imports (NOT on the frozen v1 barrel).
27
27
  import { OUTCOME_DESCRIPTION, OUT_OF_BALANCE_OUTCOMES, OUTCOME_SIDE_PROFILE } from "../model.js";
28
- import { measuredSuffix } from "./measuredItem.js";
29
- import { RUNTIME_QUALITY_CHECKS } from "../domain/ledger.js";
30
28
  // THE MODEL DERIVATIONS THE RENDERER READS (3F-2203) — it projects these; it does not compute them.
31
29
  import { brokenItemsOf, lintReconciliationsOf } from "../domain/ledger.js";
32
- import { isScenario, staticCheckReconciliationsOf, presentStaticCheckCountOf, } from "../domain/scenarioReconciliation.js";
33
- import { evidenceObligationsOf } from "../domain/journal.js";
30
+ import { isScenario } from "../domain/scenarioReconciliation.js";
34
31
  import { assembleRenderInput } from "../assemble/renderInput.js";
35
32
  // The run-integrity predicate (@SCN-LDG-018) — the existential scan for a `broken` reconciling
36
33
  // item. The renderer asks the predicate and prints the HEADLINE word: predicate-name ≠
@@ -49,6 +46,7 @@ import { reconcilerSection } from "./reconcilerSteps.js";
49
46
  import { stepAccountingSection } from "./stepAccounting.js";
50
47
  import { testVolumeSection } from "./testVolume.js";
51
48
  import { featureCodeOf } from "../lib/featureCode.js";
49
+ import { evidenceColumns, columnHeader, evidenceCell } from "./evidenceGrid.js";
52
50
  /** A short status glyph per reconciling-item outcome — a visual cue alongside the word. */
53
51
  const ITEM_GLYPH = {
54
52
  satisfied: "✓",
@@ -107,6 +105,14 @@ function renderItem(item) {
107
105
  return `${ITEM_GLYPH["no-scn-code"]} ${OUTCOME_DESCRIPTION["no-scn-code"]}`;
108
106
  }
109
107
  const subject = item.qualityCheck;
108
+ const suffix = creditSuffix(item) ?? "";
109
+ return `${subject} ${ITEM_GLYPH[item.outcome]} ${OUTCOME_DESCRIPTION[item.outcome]}${suffix}`;
110
+ }
111
+ /**
112
+ * The credit arms of a reconciling item's where-to-fix, or `undefined` when none applies and the
113
+ * item renders bare.
114
+ */
115
+ function creditSuffix(item) {
110
116
  // A CREDIT-SIDE item surfaces its WHERE-TO-FIX in the reconciliation cell (ruling
111
117
  // Q5): a lint credit (the uncited static-check folded onto the suspense row, ruling
112
118
  // Q4) shows its static check handle; a runtime credit (phantom `cites-unknown-scenario` or
@@ -120,10 +126,6 @@ function renderItem(item) {
120
126
  // joins. (`broken` and `both-missing` are (None, None), so both terms are read.)
121
127
  const profile = OUTCOME_SIDE_PROFILE[item.outcome];
122
128
  const isCredit = profile.obligation === "None" && profile.observation === "Exists";
123
- // A MEASURED item is DEBIT-side, so its where-to-fix — its two numbers — starts here rather than
124
- // in the credit arms below (`measuredItem.ts`, @SCN-CIP-008). It is "" for every other item, so
125
- // the credit arms overwrite it exactly as they overwrote the empty default.
126
- let suffix = measuredSuffix(item);
127
129
  // The `broken` credit (@SCN-RPT-021) carries BOTH halves of its where-to-fix: the locator of the
128
130
  // evidence that never ran (a test file, or a feature file) AND the message it carries. The
129
131
  // message's HEADLINE LINE only — a vitest collection message is multi-line (message + stack) and
@@ -134,14 +136,13 @@ function renderItem(item) {
134
136
  const where = [item.locator, item.message === undefined ? undefined : headlineOf(item.message)]
135
137
  .filter((part) => part !== undefined && part.length > 0)
136
138
  .join(": ");
137
- if (where.length > 0)
138
- suffix = ` (${cellSafe(where)})`;
139
+ return where.length > 0 ? ` (${cellSafe(where)})` : undefined;
139
140
  }
140
- else if (isCredit && item.staticCheck)
141
- suffix = ` (${item.staticCheck})`;
142
- else if (isCredit && item.locator !== undefined)
143
- suffix = ` (${item.locator})`;
144
- return `${subject} ${ITEM_GLYPH[item.outcome]} ${OUTCOME_DESCRIPTION[item.outcome]}${suffix}`;
141
+ if (isCredit && item.staticCheck)
142
+ return ` (${item.staticCheck})`;
143
+ if (isCredit && item.locator !== undefined)
144
+ return ` (${item.locator})`;
145
+ return undefined;
145
146
  }
146
147
  /**
147
148
  * The first non-empty line of a runner message — its HEADLINE (`Error: Cannot find package
@@ -384,14 +385,16 @@ const evidenceReconciliationSection = (report) => {
384
385
  // decomposition above — its own line, never a `↳` row under the out-of-balance table. Read
385
386
  // off the materialised `summary.pending`; rendered only when the run genuinely carries Pending
386
387
  // Items (the omit-when-empty precedent), so a zero-pending report stays byte-identical.
387
- const pending = report.summary.pending;
388
- if (pending > 0) {
389
- lines.push("");
390
- lines.push(`⏳ ${pending} Pending Item(s) — deferred, tallied apart from balanced and out-of-balance.`);
391
- }
388
+ lines.push(...pendingTally(report.summary.pending));
392
389
  lines.push("");
393
390
  return lines;
394
391
  };
392
+ /** The Pending Items tally line, preceded by its blank — or nothing at all when none is pending. */
393
+ function pendingTally(pending) {
394
+ return pending > 0
395
+ ? ["", `⏳ ${pending} Pending Item(s) — deferred, tallied apart from balanced and out-of-balance.`]
396
+ : [];
397
+ }
395
398
  /**
396
399
  * Lint evidence obligations (@SCN-RPT-005; re-cut under Summary views with a total line, labelled
397
400
  * by the @lint TAG) — lifts every `@lint` evidence obligation out of the per-scenario trial
@@ -476,128 +479,6 @@ const byFeatureSection = (report) => {
476
479
  lines.push("");
477
480
  return lines;
478
481
  };
479
- /**
480
- * The corpus-driven evidence columns: lint first (when any scenario names a static check), then
481
- * each runtime quality-check present as an EVIDENCE OBLIGATION or as a posted EVIDENCE OBSERVATION
482
- * on a scenario row, in canonical quality-check order.
483
- */
484
- function evidenceColumns(scenarios) {
485
- // Only scenario rows carry evidence obligations that drive columns — a phantom / suspense
486
- // row's credit-side items are not evidence obligations, so they never add a column.
487
- const scenarioRows = scenarios.filter(isScenario);
488
- // ASKED (3F-2203) — whether a row has static-check sub-rows is the same model cut the grid cell
489
- // and the lint table read. Whether a COLUMN then appears is the renderer's own business; what a
490
- // row's lint sub-rows ARE is not.
491
- const hasStatic = scenarioRows.some((s) => staticCheckReconciliationsOf(s).length > 0);
492
- const qualityChecksPresent = new Set();
493
- // A column appears for every runtime quality-check present in EVIDENCE OBLIGATION or OBSERVATION on a scenario
494
- // row. Both are read off the Ledger: a named or no-evidence-obligation quality-check surfaces as a
495
- // `QualityCheckReconciliation` at that check; every observed credit surfaces as a posted credit on
496
- // the row (@SCN-LDG-022). The retired census's `attributed[]` re-represented those posted credits
497
- // — it added no quality-check the rows do not already carry (a matched/no-evidence-obligation credit always
498
- // has its `QualityCheckReconciliation`), so folding the rows keeps the column set byte-identical.
499
- for (const s of scenarioRows) {
500
- for (const o of s.qualityCheckReconciliations) {
501
- // A both-missing item carries no quality-check — it drives no column.
502
- if (o.qualityCheck && o.qualityCheck !== "lint")
503
- qualityChecksPresent.add(o.qualityCheck);
504
- }
505
- for (const o of s.evidenceObservations) {
506
- if (o.qualityCheck !== "lint")
507
- qualityChecksPresent.add(o.qualityCheck);
508
- }
509
- }
510
- const columns = [];
511
- if (hasStatic)
512
- columns.push({ kind: "lint" });
513
- for (const qualityCheck of RUNTIME_QUALITY_CHECKS) {
514
- if (qualityChecksPresent.has(qualityCheck))
515
- columns.push({ kind: "runtime", qualityCheck });
516
- }
517
- return columns;
518
- }
519
- /** The column's header — the tag word, title-cased (Lint / Unit / Integration / …). */
520
- function columnHeader(col) {
521
- const word = col.kind === "lint" ? "lint" : col.qualityCheck;
522
- return word.charAt(0).toUpperCase() + word.slice(1);
523
- }
524
- /**
525
- * The quality-check → single-letter obligation glyph, parallel to `columnHeader` — the LEFT side
526
- * of the two-sided cell (the evidence obligation). A single letter names WHICH check is obligated; the right
527
- * side (after the `/`) carries the OBSERVATION count (the credit). Lint's left MAY carry a count
528
- * suffix (`L2`/`L3`) when a scenario names more than one static check — the evidence-obligation
529
- * count; the runtime letters stay countless (one obligation per tier).
530
- */
531
- const QUALITY_CHECK_LETTER = {
532
- lint: "L",
533
- unit: "U",
534
- integration: "I",
535
- e2e: "E",
536
- performance: "P",
537
- };
538
- /**
539
- * One evidence cell — the two-sided `<obligation>/<observation>` encoding. The LEFT side is the
540
- * EVIDENCE OBLIGATION (a single quality-check letter iff the scenario obligated this check, else `-`); the RIGHT
541
- * side is the OBSERVATION count (the multiplicity of posted credits at this check, else `-`). It
542
- * projects the two-sided ledger row — it reconstructs nothing. It counts BACKING (presence), NOT
543
- * quality: a failing runtime evidenceObligation still shows its credit count (the failing-ness lives
544
- * in the Verdict + Reconciling-items columns), so `U/-` is unambiguously named-but-missing (zero
545
- * evidenceObservations). A `-/N` cell is a NO-EVIDENCE-OBLIGATION observation (a credit at a check the scenario
546
- * did not name, @SCN-LDG-008); a doubly-empty `-/-` renders BLANK (`""`) — no evidence obligation, no credit.
547
- * The lint cell reads the scenario's static evidenceObligation outcomes (lint is not a counted
548
- * evidence observation): its right side is the count of static checks PRESENT — `balanced` or the
549
- * watermelon (the script exists; its inadequacy is quality, in Reconciling-items 🍉), NOT a posted
550
- * credit. The count exposes backing, not quality — never rank by it.
551
- */
552
- function evidenceCell(s, col) {
553
- // A PHANTOM / suspense row has no evidence obligation of its own — its
554
- // whole story is the credit-side reconciling item. Every evidence cell is neither-named-nor-
555
- // matched, so it reads `—`; the citation shows in Reconciling items.
556
- if (!isScenario(s))
557
- return "—";
558
- if (col.kind === "lint") {
559
- // LEFT: a lint obligation is NAMED iff a lint QualityCheckReconciliation exists on the row
560
- // (`L`; `L2`/`L3` when the row names >1 static check — the evidence-obligation count, one
561
- // lint QCR per `@lint=<guard>` obligation). RIGHT: the PRESENT-guard count — the
562
- // static checks `satisfied` or watermelon. Lint's right stays this static-outcome derivation
563
- // (NOT `evidenceObservations`): lint carries no credit multiplicity, so the count is exact.
564
- // BOTH SIDES ASKED (3F-2203) — the named-guard sub-rows and the present-guard count are
565
- // model facts, read off the row. "Present" spans satisfied AND watermelon (a watermelon guard
566
- // EXISTS; its inadequacy is quality, shown in Reconciling items 🍉) — an outcome-vocabulary
567
- // rule, which is why the renderer no longer states it.
568
- const statics = staticCheckReconciliationsOf(s);
569
- const left = statics.length > 0
570
- ? QUALITY_CHECK_LETTER.lint + (statics.length > 1 ? String(statics.length) : "")
571
- : "-";
572
- const present = presentStaticCheckCountOf(s);
573
- const right = present > 0 ? String(present) : "-";
574
- return left === "-" && right === "-" ? "" : `${left}/${right}`;
575
- }
576
- // LEFT — EVIDENCE OBLIGATION is ASKED of the Ledger, not reconstructed. `s.evidenceObligations` is the debit
577
- // side the scenario RAISED (@SCN-LDG-023) — the fact itself — filtered to the ACTIVE ones: a
578
- // `<qc>pending` obligation names NOTHING (it partitions off to a one-sided Pending Item), so it
579
- // is not an evidence obligation. Named → the check's letter; else `-`.
580
- // ASKED, not re-encoded (3F-2203). "A pending obligation names nothing" is the model's rule
581
- // (canon, Pending Item — a `<qc>pending` tag raises NO Evidence Obligation), and it is the SAME
582
- // rule `evidenceObligationPairsOf` applies. The renderer stated the
583
- // `!pending` clause itself before: a third home for one rule, agreeing by copy.
584
- const named = evidenceObligationsOf(s.evidenceObligations).some((o) => o.qualityCheck === col.qualityCheck);
585
- const left = named ? QUALITY_CHECK_LETTER[col.qualityCheck] : "-";
586
- // RIGHT — the OBSERVATION count is the MULTIPLICITY of POSTED CREDITS at this quality-check on THIS
587
- // row (@SCN-LDG-022) — read straight off the Ledger's `evidenceObservations`, NEVER from
588
- // `qualityCheckReconciliations` (which collapse to ONE matched row however many credits cite the
589
- // pair, and would undercount duplicate citations). BOTH the named and no-evidence-obligation arms read this
590
- // one source: a named check's credits are matched-attributed, a no-evidence-obligation check's credits post
591
- // as no-evidence-obligation rows — the same posted credits either way, so `evidenceObservations` is the single
592
- // multiplicity-correct count. (Provably equal to the retired split off `qualityCheckReconciliations`
593
- // for the no-evidence-obligation arm — the reconciler posts one no-evidence-obligation QCR per citing credit — but that
594
- // derivation collapses duplicate citations, so the credits are the correct source.)
595
- const observed = s.evidenceObservations.filter((o) => o.qualityCheck === col.qualityCheck).length;
596
- const right = observed > 0 ? String(observed) : "-";
597
- // A doubly-empty cell (no evidence obligation, no credit) renders BLANK — the column is simply absent for this
598
- // scenario. Otherwise the two-sided `<left>/<right>`.
599
- return left === "-" && right === "-" ? "" : `${left}/${right}`;
600
- }
601
482
  /**
602
483
  * The ledger (moved to the END) — the per-scenario table, with its summary lines on top: the
603
484
  * verdict tally bullet, and (when an evidence-obligation census is threaded) the
@@ -621,12 +502,21 @@ const ledgerSection = (report) => {
621
502
  // (@SCN-RPT-010 / 3F-2103 — the gate that was the retired `evidenceObservations` section's
622
503
  // presence). When it is, the ledger gains the corpus-driven evidence columns.
623
504
  const withObservations = report.summary.testVolume !== undefined;
624
- const totals = report.summary.scenarioReconciliations;
625
- const lines = [
505
+ return [
626
506
  // The reconciled per-scenario OUTPUT table — "Scenario Reconciliations", not "Ledger"
627
507
  // ("Ledger" is the INPUT concept in canon; double-entry-manifesto).
628
508
  "## Scenario Reconciliations",
629
509
  "",
510
+ ...ledgerTallyLines(report.summary.scenarioReconciliations),
511
+ ...(report.evidenceObligations ? censusLines(report.evidenceObligations) : []),
512
+ "",
513
+ ...ledgerTableLines(scenarios, withObservations),
514
+ "",
515
+ ];
516
+ };
517
+ /** The ledger's verdict tally bullet, and the phantom / suspense sub-counts broken out of it. */
518
+ function ledgerTallyLines(totals) {
519
+ const lines = [
630
520
  // The headline verdict tally counts EVERY Scenario Reconciliation — scenario, phantom and
631
521
  // suspense alike (@SCN-RMD-001, 3F-1724; the scenario-only "ruling Q3" is retracted).
632
522
  `- ${totals.total} Scenario Reconciliation(s): ${totals.balanced} balanced, ` +
@@ -644,60 +534,65 @@ const ledgerSection = (report) => {
644
534
  if (totals.suspense > 0) {
645
535
  lines.push(`- ${totals.suspense} suspense-scenario(s): credits citing no scenario, aggregated into "(no scenario)".`);
646
536
  }
647
- if (report.evidenceObligations) {
648
- // The scenario-corpus CENSUS — obligation-side facts on report.evidenceObligations.
649
- // These re-homed here from the dissolved `## Warnings` section: they are census
650
- // facts, NOT warnings and NOT reconciling items, so they ride in the ledger summary co-located
651
- // with the excluded-occurrence bullet (the evidenceObligations census view). Facts only —
652
- // count + locator.
653
- lines.push(`- ${report.evidenceObligations.excludedOccurrences} @SCN ids seen in non-tag context, excluded`);
654
- // Untagged scenarios (@SCN-RPT-014) — Gherkin scenarios with no @SCN tag, located by
655
- // `feature : scenario`. Sorted defensively (a hand-built corpus fixture may be unsorted).
656
- const untagged = report.evidenceObligations.untaggedScenarios;
657
- if (untagged.length > 0) {
658
- const sorted = [...untagged].sort((a, b) => a.feature.localeCompare(b.feature) || a.scenario.localeCompare(b.scenario));
659
- lines.push(`- ${untagged.length} scenario(s) carry no @SCN tag:`);
660
- for (const { feature, scenario } of sorted) {
661
- lines.push(` - ${feature} : ${scenario}`);
662
- }
537
+ return lines;
538
+ }
539
+ /**
540
+ * The scenario-corpus CENSUS — obligation-side facts on report.evidenceObligations.
541
+ * These re-homed here from the dissolved `## Warnings` section: they are census
542
+ * facts, NOT warnings and NOT reconciling items, so they ride in the ledger summary co-located
543
+ * with the excluded-occurrence bullet (the evidenceObligations census view). Facts only —
544
+ * count + locator.
545
+ */
546
+ function censusLines(census) {
547
+ const lines = [`- ${census.excludedOccurrences} @SCN ids seen in non-tag context, excluded`];
548
+ // Untagged scenarios (@SCN-RPT-014) — Gherkin scenarios with no @SCN tag, located by
549
+ // `feature : scenario`. Sorted defensively (a hand-built corpus fixture may be unsorted).
550
+ const untagged = census.untaggedScenarios;
551
+ if (untagged.length > 0) {
552
+ const sorted = [...untagged].sort((a, b) => a.feature.localeCompare(b.feature) || a.scenario.localeCompare(b.scenario));
553
+ lines.push(`- ${untagged.length} scenario(s) carry no @SCN tag:`);
554
+ for (const { feature, scenario } of sorted) {
555
+ lines.push(` - ${feature} : ${scenario}`);
663
556
  }
664
- // Prose-only codes (@SCN-RPT-015) — @SCN ids mentioned in prose but never tagged anywhere
665
- // (the dangling references). The bare code is its own locator. Sorted, after untagged.
666
- const proseOnly = report.evidenceObligations.proseOnlyCodes;
667
- if (proseOnly.length > 0) {
668
- lines.push(`- ${proseOnly.length} @SCN code(s) appear only in prose, never as a tag:`);
669
- for (const code of [...proseOnly].sort()) {
670
- lines.push(` - ${code}`);
671
- }
557
+ }
558
+ // Prose-only codes (@SCN-RPT-015) — @SCN ids mentioned in prose but never tagged anywhere
559
+ // (the dangling references). The bare code is its own locator. Sorted, after untagged.
560
+ const proseOnly = census.proseOnlyCodes;
561
+ if (proseOnly.length > 0) {
562
+ lines.push(`- ${proseOnly.length} @SCN code(s) appear only in prose, never as a tag:`);
563
+ for (const code of [...proseOnly].sort()) {
564
+ lines.push(` - ${code}`);
672
565
  }
673
566
  }
674
- lines.push("");
567
+ return lines;
568
+ }
569
+ /**
570
+ * The per-scenario table: with the corpus-driven evidence columns between Scenario and Verdict when
571
+ * the runtime-observation axis is in play and any column appears, else the original three columns.
572
+ */
573
+ function ledgerTableLines(scenarios, withObservations) {
675
574
  const columns = withObservations ? evidenceColumns(scenarios) : [];
676
- if (withObservations && columns.length > 0) {
677
- const headers = [
678
- "Scenario",
679
- ...columns.map(columnHeader),
680
- "Verdict",
681
- "Reconciling items",
575
+ if (!(withObservations && columns.length > 0)) {
576
+ return [
577
+ "| Scenario | Verdict | Reconciling items |",
578
+ "| --- | --- | --- |",
579
+ ...scenarios.map(renderScenario),
682
580
  ];
683
- lines.push(`| ${headers.join(" | ")} |`);
684
- lines.push(`| ${headers.map(() => "---").join(" | ")} |`);
685
- for (const s of scenarios) {
686
- const cells = columns.map((col) => evidenceCell(s, col));
687
- const items = renderRowExceptions(s);
688
- lines.push(`| ${[s.scnId, ...cells, `${VERDICT_GLYPH[s.verdict]} ${s.verdict}`, items].join(" | ")} |`);
689
- }
690
581
  }
691
- else {
692
- lines.push("| Scenario | Verdict | Reconciling items |");
693
- lines.push("| --- | --- | --- |");
694
- for (const s of scenarios) {
695
- lines.push(renderScenario(s));
696
- }
582
+ const headers = [
583
+ "Scenario",
584
+ ...columns.map(columnHeader),
585
+ "Verdict",
586
+ "Reconciling items",
587
+ ];
588
+ const lines = [`| ${headers.join(" | ")} |`, `| ${headers.map(() => "---").join(" | ")} |`];
589
+ for (const s of scenarios) {
590
+ const cells = columns.map((col) => evidenceCell(s, col));
591
+ const items = renderRowExceptions(s);
592
+ lines.push(`| ${[s.scnId, ...cells, `${VERDICT_GLYPH[s.verdict]} ${s.verdict}`, items].join(" | ")} |`);
697
593
  }
698
- lines.push("");
699
594
  return lines;
700
- };
595
+ }
701
596
  /**
702
597
  * The ordered section list (project design §2; reorganised summary-first / ledger-last,
703
598
  * test volume added): title → provenance → the run-integrity stamp (only when the run is