@3f-consulting/spec-controller-core 0.1.0-alpha.7 → 0.1.0-alpha.70
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.
- package/README.md +3 -3
- package/dist/assemble/evidenceReconciliationFromSources.d.ts +5 -40
- package/dist/assemble/evidenceReconciliationFromSources.d.ts.map +1 -1
- package/dist/assemble/evidenceReconciliationFromSources.js +30 -325
- package/dist/assemble/evidenceReconciliationFromSources.js.map +1 -1
- package/dist/assemble/renderInput.d.ts +7 -73
- package/dist/assemble/renderInput.d.ts.map +1 -1
- package/dist/assemble/renderInput.js +3 -45
- package/dist/assemble/renderInput.js.map +1 -1
- package/dist/census/evidenceObligations.d.ts +11 -46
- package/dist/census/evidenceObligations.d.ts.map +1 -1
- package/dist/census/evidenceObligations.js +19 -25
- package/dist/census/evidenceObligations.js.map +1 -1
- package/dist/domain/evidenceOrder.d.ts +3 -0
- package/dist/domain/evidenceOrder.d.ts.map +1 -0
- package/dist/domain/evidenceOrder.js +15 -0
- package/dist/domain/evidenceOrder.js.map +1 -0
- package/dist/domain/evidenceReconciliation.d.ts +21 -391
- package/dist/domain/evidenceReconciliation.d.ts.map +1 -1
- package/dist/domain/evidenceReconciliation.js +87 -382
- package/dist/domain/evidenceReconciliation.js.map +1 -1
- package/dist/domain/featureCode.d.ts +4 -0
- package/dist/domain/featureCode.d.ts.map +1 -0
- package/dist/domain/featureCode.js +12 -0
- package/dist/domain/featureCode.js.map +1 -0
- package/dist/domain/journal.d.ts +8 -393
- package/dist/domain/journal.d.ts.map +1 -1
- package/dist/domain/journal.js +6 -118
- package/dist/domain/journal.js.map +1 -1
- package/dist/domain/ledger.d.ts +8 -328
- package/dist/domain/ledger.d.ts.map +1 -1
- package/dist/domain/ledger.js +23 -326
- package/dist/domain/ledger.js.map +1 -1
- package/dist/domain/messages.d.ts +213 -0
- package/dist/domain/messages.d.ts.map +1 -0
- package/dist/domain/messages.js +313 -0
- package/dist/domain/messages.js.map +1 -0
- package/dist/domain/outcome.d.ts +3 -0
- package/dist/domain/outcome.d.ts.map +1 -0
- package/dist/domain/outcome.js +2 -0
- package/dist/domain/outcome.js.map +1 -0
- package/dist/domain/posting.d.ts +8 -105
- package/dist/domain/posting.d.ts.map +1 -1
- package/dist/domain/posting.js +4 -56
- package/dist/domain/posting.js.map +1 -1
- package/dist/domain/qualityCheckFault.d.ts +3 -43
- package/dist/domain/qualityCheckFault.d.ts.map +1 -1
- package/dist/domain/qualityCheckFault.js +0 -24
- package/dist/domain/qualityCheckFault.js.map +1 -1
- package/dist/domain/qualityCheckReconciliation.d.ts +7 -280
- package/dist/domain/qualityCheckReconciliation.d.ts.map +1 -1
- package/dist/domain/qualityCheckReconciliation.js +63 -463
- package/dist/domain/qualityCheckReconciliation.js.map +1 -1
- package/dist/domain/qualityCheckReconciliationFault.d.ts +0 -19
- package/dist/domain/qualityCheckReconciliationFault.d.ts.map +1 -1
- package/dist/domain/qualityCheckReconciliationFault.js +1 -24
- package/dist/domain/qualityCheckReconciliationFault.js.map +1 -1
- package/dist/domain/scenarioCodeError.d.ts +16 -0
- package/dist/domain/scenarioCodeError.d.ts.map +1 -0
- package/dist/domain/scenarioCodeError.js +4 -0
- package/dist/domain/scenarioCodeError.js.map +1 -0
- package/dist/domain/scenarioReconciliation.d.ts +2 -207
- package/dist/domain/scenarioReconciliation.d.ts.map +1 -1
- package/dist/domain/scenarioReconciliation.js +4 -101
- package/dist/domain/scenarioReconciliation.js.map +1 -1
- package/dist/domain/scnAllocation.d.ts +27 -0
- package/dist/domain/scnAllocation.d.ts.map +1 -0
- package/dist/domain/scnAllocation.js +33 -0
- package/dist/domain/scnAllocation.js.map +1 -0
- package/dist/domain/verdictCode.d.ts +10 -0
- package/dist/domain/verdictCode.d.ts.map +1 -0
- package/dist/domain/verdictCode.js +16 -0
- package/dist/domain/verdictCode.js.map +1 -0
- package/dist/index.d.ts +14 -40
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -88
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +3 -27
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +3 -94
- package/dist/internal.js.map +1 -1
- package/dist/lib/bareTerm.d.ts +0 -39
- package/dist/lib/bareTerm.d.ts.map +1 -1
- package/dist/lib/bareTerm.js +0 -39
- package/dist/lib/bareTerm.js.map +1 -1
- package/dist/lib/portablePath.d.ts.map +1 -1
- package/dist/lib/portablePath.js +2 -16
- package/dist/lib/portablePath.js.map +1 -1
- package/dist/lib/scnCitation.d.ts +0 -32
- package/dist/lib/scnCitation.d.ts.map +1 -1
- package/dist/lib/scnCitation.js +2 -42
- package/dist/lib/scnCitation.js.map +1 -1
- package/dist/model.d.ts +0 -23
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +0 -23
- package/dist/model.js.map +1 -1
- package/dist/parse/checkScriptsRunInCI.d.ts +5 -168
- package/dist/parse/checkScriptsRunInCI.d.ts.map +1 -1
- package/dist/parse/checkScriptsRunInCI.js +11 -171
- package/dist/parse/checkScriptsRunInCI.js.map +1 -1
- package/dist/parse/ciStepRecord.d.ts +4 -118
- package/dist/parse/ciStepRecord.d.ts.map +1 -1
- package/dist/parse/ciStepRecord.js +18 -225
- package/dist/parse/ciStepRecord.js.map +1 -1
- package/dist/parse/commandRuns.d.ts +0 -39
- package/dist/parse/commandRuns.d.ts.map +1 -1
- package/dist/parse/commandRuns.js +6 -67
- package/dist/parse/commandRuns.js.map +1 -1
- package/dist/parse/cucumber.d.ts +0 -53
- package/dist/parse/cucumber.d.ts.map +1 -1
- package/dist/parse/cucumber.js +42 -146
- package/dist/parse/cucumber.js.map +1 -1
- package/dist/parse/discoveryScope.d.ts +1 -46
- package/dist/parse/discoveryScope.d.ts.map +1 -1
- package/dist/parse/discoveryScope.js +2 -57
- package/dist/parse/discoveryScope.js.map +1 -1
- package/dist/parse/fitnessCitations.d.ts +1 -72
- package/dist/parse/fitnessCitations.d.ts.map +1 -1
- package/dist/parse/fitnessCitations.js +3 -55
- package/dist/parse/fitnessCitations.js.map +1 -1
- package/dist/parse/reconcilerSteps.d.ts +0 -46
- package/dist/parse/reconcilerSteps.d.ts.map +1 -1
- package/dist/parse/reconcilerSteps.js +2 -64
- package/dist/parse/reconcilerSteps.js.map +1 -1
- package/dist/parse/runningFitnessFunctions.d.ts +9 -0
- package/dist/parse/runningFitnessFunctions.d.ts.map +1 -0
- package/dist/parse/runningFitnessFunctions.js +57 -0
- package/dist/parse/runningFitnessFunctions.js.map +1 -0
- package/dist/parse/scenarioCodeErrors.d.ts +11 -0
- package/dist/parse/scenarioCodeErrors.d.ts.map +1 -0
- package/dist/parse/scenarioCodeErrors.js +78 -0
- package/dist/parse/scenarioCodeErrors.js.map +1 -0
- package/dist/parse/scenarios.d.ts +3 -228
- package/dist/parse/scenarios.d.ts.map +1 -1
- package/dist/parse/scenarios.js +20 -457
- package/dist/parse/scenarios.js.map +1 -1
- package/dist/parse/stepAccounting.d.ts +0 -46
- package/dist/parse/stepAccounting.d.ts.map +1 -1
- package/dist/parse/stepAccounting.js +1 -55
- package/dist/parse/stepAccounting.js.map +1 -1
- package/dist/parse/stepExclusions.d.ts +0 -47
- package/dist/parse/stepExclusions.d.ts.map +1 -1
- package/dist/parse/stepExclusions.js +2 -50
- package/dist/parse/stepExclusions.js.map +1 -1
- package/dist/parse/stepNames.d.ts +0 -27
- package/dist/parse/stepNames.d.ts.map +1 -1
- package/dist/parse/stepNames.js +0 -27
- package/dist/parse/stepNames.js.map +1 -1
- package/dist/parse/targetCorpus.d.ts +1 -78
- package/dist/parse/targetCorpus.d.ts.map +1 -1
- package/dist/parse/targetCorpus.js +4 -46
- package/dist/parse/targetCorpus.js.map +1 -1
- package/dist/parse/targetManifest.d.ts +8 -146
- package/dist/parse/targetManifest.d.ts.map +1 -1
- package/dist/parse/targetManifest.js +48 -198
- package/dist/parse/targetManifest.js.map +1 -1
- package/dist/parse/vitest.d.ts +0 -60
- package/dist/parse/vitest.d.ts.map +1 -1
- package/dist/parse/vitest.js +4 -83
- package/dist/parse/vitest.js.map +1 -1
- package/dist/provenance/provenance.d.ts +2 -110
- package/dist/provenance/provenance.d.ts.map +1 -1
- package/dist/provenance/provenance.js +2 -58
- package/dist/provenance/provenance.js.map +1 -1
- package/dist/render/behaviouralRunners.d.ts +0 -29
- package/dist/render/behaviouralRunners.d.ts.map +1 -1
- package/dist/render/behaviouralRunners.js +6 -69
- package/dist/render/behaviouralRunners.js.map +1 -1
- package/dist/render/configurationNotRead.d.ts +3 -0
- package/dist/render/configurationNotRead.d.ts.map +1 -0
- package/dist/render/configurationNotRead.js +17 -0
- package/dist/render/configurationNotRead.js.map +1 -0
- package/dist/render/discoveryScope.d.ts +0 -37
- package/dist/render/discoveryScope.d.ts.map +1 -1
- package/dist/render/discoveryScope.js +3 -39
- package/dist/render/discoveryScope.js.map +1 -1
- package/dist/render/evidenceGrid.d.ts +1 -35
- package/dist/render/evidenceGrid.d.ts.map +1 -1
- package/dist/render/evidenceGrid.js +21 -103
- package/dist/render/evidenceGrid.js.map +1 -1
- package/dist/render/excludedSteps.d.ts +0 -26
- package/dist/render/excludedSteps.d.ts.map +1 -1
- package/dist/render/excludedSteps.js +4 -32
- package/dist/render/excludedSteps.js.map +1 -1
- package/dist/render/fitnessFunctions.d.ts +3 -0
- package/dist/render/fitnessFunctions.d.ts.map +1 -0
- package/dist/render/fitnessFunctions.js +12 -0
- package/dist/render/fitnessFunctions.js.map +1 -0
- package/dist/render/json.d.ts +0 -11
- package/dist/render/json.d.ts.map +1 -1
- package/dist/render/json.js +0 -11
- package/dist/render/json.js.map +1 -1
- package/dist/render/markdown.d.ts +1 -55
- package/dist/render/markdown.d.ts.map +1 -1
- package/dist/render/markdown.js +128 -467
- package/dist/render/markdown.js.map +1 -1
- package/dist/render/provenanceSection.d.ts +0 -22
- package/dist/render/provenanceSection.d.ts.map +1 -1
- package/dist/render/provenanceSection.js +10 -53
- package/dist/render/provenanceSection.js.map +1 -1
- package/dist/render/reconcilerSteps.d.ts +0 -25
- package/dist/render/reconcilerSteps.d.ts.map +1 -1
- package/dist/render/reconcilerSteps.js +5 -30
- package/dist/render/reconcilerSteps.js.map +1 -1
- package/dist/render/runVerdict.d.ts +0 -38
- package/dist/render/runVerdict.d.ts.map +1 -1
- package/dist/render/runVerdict.js +62 -129
- package/dist/render/runVerdict.js.map +1 -1
- package/dist/render/scenarioCodeErrorsSection.d.ts +3 -0
- package/dist/render/scenarioCodeErrorsSection.d.ts.map +1 -0
- package/dist/render/scenarioCodeErrorsSection.js +45 -0
- package/dist/render/scenarioCodeErrorsSection.js.map +1 -0
- package/dist/render/stepAccounting.d.ts +0 -36
- package/dist/render/stepAccounting.d.ts.map +1 -1
- package/dist/render/stepAccounting.js +5 -49
- package/dist/render/stepAccounting.js.map +1 -1
- package/dist/render/testVolume.d.ts +0 -25
- package/dist/render/testVolume.d.ts.map +1 -1
- package/dist/render/testVolume.js +4 -35
- package/dist/render/testVolume.js.map +1 -1
- package/package.json +4 -1
- package/dist/domain/checkCharacteristics.d.ts +0 -88
- package/dist/domain/checkCharacteristics.d.ts.map +0 -1
- package/dist/domain/checkCharacteristics.js +0 -109
- package/dist/domain/checkCharacteristics.js.map +0 -1
- package/dist/domain/fitnessChecks.d.ts +0 -79
- package/dist/domain/fitnessChecks.d.ts.map +0 -1
- package/dist/domain/fitnessChecks.js +0 -81
- package/dist/domain/fitnessChecks.js.map +0 -1
- package/dist/domain/qualityCheckFaultMessages.d.ts +0 -43
- package/dist/domain/qualityCheckFaultMessages.d.ts.map +0 -1
- package/dist/domain/qualityCheckFaultMessages.js +0 -38
- package/dist/domain/qualityCheckFaultMessages.js.map +0 -1
- package/dist/lib/featureCode.d.ts +0 -21
- package/dist/lib/featureCode.d.ts.map +0 -1
- package/dist/lib/featureCode.js +0 -24
- package/dist/lib/featureCode.js.map +0 -1
- package/dist/parse/corpusCodeFaults.d.ts +0 -76
- package/dist/parse/corpusCodeFaults.d.ts.map +0 -1
- package/dist/parse/corpusCodeFaults.js +0 -121
- package/dist/parse/corpusCodeFaults.js.map +0 -1
- package/dist/parse/corpusFaults.d.ts +0 -124
- package/dist/parse/corpusFaults.d.ts.map +0 -1
- package/dist/parse/corpusFaults.js +0 -72
- package/dist/parse/corpusFaults.js.map +0 -1
- package/dist/parse/corpusGrammarFaults.d.ts +0 -71
- package/dist/parse/corpusGrammarFaults.d.ts.map +0 -1
- package/dist/parse/corpusGrammarFaults.js +0 -292
- package/dist/parse/corpusGrammarFaults.js.map +0 -1
- package/dist/parse/runningStaticChecks.d.ts +0 -125
- package/dist/parse/runningStaticChecks.d.ts.map +0 -1
- package/dist/parse/runningStaticChecks.js +0 -316
- package/dist/parse/runningStaticChecks.js.map +0 -1
- package/dist/render/staticChecks.d.ts +0 -23
- package/dist/render/staticChecks.d.ts.map +0 -1
- package/dist/render/staticChecks.js +0 -69
- package/dist/render/staticChecks.js.map +0 -1
package/dist/render/markdown.js
CHANGED
|
@@ -1,589 +1,307 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The markdown renderer — the human-readable spec reconciliation: summary views up
|
|
3
|
-
* top (the derived cuts OF the reconciliation), the per-scenario Scenario Reconciliations last.
|
|
4
|
-
* Each Scenario Reconciliation row carries its binary VERDICT (balanced | out-of-balance) and the
|
|
5
|
-
* LIST of EVERY reconciling item that holds it out of balance (no single
|
|
6
|
-
* picked winner; an out-of-balance scenario lists all its non-balanced items, typed).
|
|
7
|
-
*
|
|
8
|
-
* A PURE PRESENTATION of the shared whole-document `EvidenceReconciliation` model: the JSON
|
|
9
|
-
* renderer serialises that model, and this renderer projects the SAME model to markdown,
|
|
10
|
-
* so the two outputs are one model in two renderings and cannot diverge. `renderMarkdown`
|
|
11
|
-
* keeps its `(balance, ctx, …)` signature — it builds `assembleRenderInput(...)` internally
|
|
12
|
-
* (mirroring `json.ts`) then delegates to `renderReport(report, featureNames)`, the
|
|
13
|
-
* from-model pipeline (exposed for slice 3's render-from-saved-JSON path). Every section
|
|
14
|
-
* emitter reads MATERIALISED fields off the report rather than recomputing them.
|
|
15
|
-
*
|
|
16
|
-
* The report IS the reconciliation; the per-scenario Evidence reconciliation is its core, and the
|
|
17
|
-
* summary views — the Evidence reconciliation aggregate (the balance-check that verifies the accounts
|
|
18
|
-
* agree), lint evidence obligations, out-of-balance by feature — are cuts OF it. Layout
|
|
19
|
-
* reorganised summary-first / ledger-last.
|
|
20
|
-
*
|
|
21
|
-
* Parent: (@SCN-LDG-001).
|
|
22
|
-
*/
|
|
23
|
-
// The single source for the human-readable outcome gloss — rendered in place of the bare
|
|
24
|
-
// handle wherever an outcome surfaces to a reader (3F-1626) — and the DERIVED leaf list the
|
|
25
|
-
// decomposition walks (3F-1806): both live in the model, so the renderer cannot drift from the
|
|
26
|
-
// union. Internal core imports (NOT on the frozen v1 barrel).
|
|
27
1
|
import { OUTCOME_DESCRIPTION, OUT_OF_BALANCE_OUTCOMES, OUTCOME_SIDE_PROFILE } from "../model.js";
|
|
28
|
-
|
|
29
|
-
import { brokenItemsOf,
|
|
2
|
+
import { untaggedScenarioId, } from "../census/evidenceObligations.js";
|
|
3
|
+
import { brokenItemsOf, fitnessReconciliationsOf } from "../domain/ledger.js";
|
|
30
4
|
import { isScenario } from "../domain/scenarioReconciliation.js";
|
|
31
5
|
import { assembleRenderInput } from "../assemble/renderInput.js";
|
|
32
|
-
// The run-integrity predicate (@SCN-LDG-018) — the existential scan for a `broken` reconciling
|
|
33
|
-
// item. The renderer asks the predicate and prints the HEADLINE word: predicate-name ≠
|
|
34
|
-
// headline-word (`hasBrokenEvidence` → "unsound"), exactly as `isBalanced` → "balanced".
|
|
35
|
-
// The three WHOLE-RUN predicates the verdict headline (@SCN-RPT-022) asks — the SAME three the
|
|
36
|
-
// CLI asks to set its exit code, in the SAME dominance order. The renderer never re-scans the
|
|
37
|
-
// reconciliation for a verdict of its own, so the headline cannot drift from the code it names.
|
|
38
6
|
import { hasBrokenEvidence } from "../domain/ledger.js";
|
|
39
7
|
import { runVerdictSection } from "./runVerdict.js";
|
|
8
|
+
import { scenarioCodeErrorsSection } from "./scenarioCodeErrorsSection.js";
|
|
40
9
|
import { provenanceSection } from "./provenanceSection.js";
|
|
10
|
+
import { configurationNotReadSection } from "./configurationNotRead.js";
|
|
41
11
|
import { discoveryScopeSection } from "./discoveryScope.js";
|
|
42
|
-
import {
|
|
12
|
+
import { fitnessFunctionsSection } from "./fitnessFunctions.js";
|
|
43
13
|
import { behaviouralRunnersSection } from "./behaviouralRunners.js";
|
|
44
14
|
import { excludedStepsSection } from "./excludedSteps.js";
|
|
45
15
|
import { reconcilerSection } from "./reconcilerSteps.js";
|
|
46
16
|
import { stepAccountingSection } from "./stepAccounting.js";
|
|
47
17
|
import { testVolumeSection } from "./testVolume.js";
|
|
48
|
-
import { featureCodeOf } from "../
|
|
18
|
+
import { featureCodeOf } from "../domain/featureCode.js";
|
|
49
19
|
import { evidenceColumns, columnHeader, evidenceCell } from "./evidenceGrid.js";
|
|
50
|
-
|
|
20
|
+
import { CORE_MESSAGES, CoreMessage } from "../domain/messages.js";
|
|
51
21
|
const ITEM_GLYPH = {
|
|
52
22
|
satisfied: "✓",
|
|
53
23
|
"failing": "✗",
|
|
54
|
-
// The watermelon: green on the surface (a static check exists), red within (not wired
|
|
55
|
-
// into CI over the real source).
|
|
56
24
|
"watermelon": "🍉",
|
|
57
25
|
"missing": "∅",
|
|
58
|
-
// DEBIT-SIDE: a real, ordinary scenario carrying no @SCN, so its Evidence Obligation has no code
|
|
59
|
-
// for an Evidence Observation to LINK to (@SCN-LDG-021). Not phantom, not suspense — the scenario
|
|
60
|
-
// exists and cucumber runs it. The debit twin of `cites-no-scenario`: same missing link, opposite
|
|
61
|
-
// side. It can never be proven, so its row is always out-of-balance.
|
|
62
26
|
"no-scn-code": "🔗",
|
|
63
|
-
|
|
64
|
-
// reconciling item on a phantom scenario (@SCN-LDG-006).
|
|
27
|
+
"duplicate-scn-code": "⧉",
|
|
65
28
|
"cites-unknown-scenario": "👻",
|
|
66
|
-
// CREDIT-SIDE: a credit citing NO @SCN at all — the reconciling item on the
|
|
67
|
-
// suspense scenario (@SCN-LDG-007).
|
|
68
29
|
"cites-no-scenario": "❓",
|
|
69
|
-
// CREDIT-SIDE: evidence that NEVER RAN — a vitest suite that failed to collect before naming a
|
|
70
|
-
// test (@SCN-LDG-017), or a cucumber step two or more step definitions match (@SCN-LDG-019).
|
|
71
|
-
// Either way it posts to the suspense row: the credit can never discharge an obligation.
|
|
72
30
|
"broken": "💥",
|
|
73
|
-
// CREDIT-SIDE: a credit citing a REAL scenario at a quality-check it never
|
|
74
|
-
// named — the no-evidence-obligation reconciling item on the real scenario's row (@SCN-LDG-008).
|
|
75
31
|
"no-evidence-obligation": "◇",
|
|
76
|
-
// NEITHER-SIDE: a scenario reconciling to zero rows on both sides — the
|
|
77
|
-
// degenerate both-missing reconciling item, blank on both sides (@SCN-LDG-009).
|
|
78
32
|
"both-missing": "⊘",
|
|
79
33
|
};
|
|
80
|
-
/** A short glyph per scenario verdict. */
|
|
81
34
|
const VERDICT_GLYPH = {
|
|
82
35
|
balanced: "✓",
|
|
83
36
|
"out-of-balance": "✗",
|
|
84
37
|
};
|
|
85
|
-
/**
|
|
86
|
-
* Render one reconciling item, labelled by its flat quality-check (the
|
|
87
|
-
* label-=-tag rule): the `qualityCheck` value IS the tag word — a runtime item
|
|
88
|
-
* is its bare level (`unit ✗ Evidence Observation is failing`,
|
|
89
|
-
* `integration ∅ Evidence Observation is missing`, …); a lint item is
|
|
90
|
-
* `lint 🍉 Evidence Observation is passing but fabricated`. The outcome renders as its
|
|
91
|
-
* CANON DESCRIPTION (the single-sourced `OUTCOME_DESCRIPTION`, 3F-1626) — never the bare
|
|
92
|
-
* handle, which is meaningless to a human reading a CI log. The glyph is retained.
|
|
93
|
-
*/
|
|
94
38
|
function renderItem(item) {
|
|
95
|
-
// The NEITHER-side both-missing item is blank on both sides — it names no
|
|
96
|
-
// quality-check (neither a debit evidence obligation nor a credit evidence observation), so it renders with
|
|
97
|
-
// its glyph + description alone, no leading subject.
|
|
98
39
|
if (item.outcome === "both-missing") {
|
|
99
40
|
return `${ITEM_GLYPH["both-missing"]} ${OUTCOME_DESCRIPTION["both-missing"]}`;
|
|
100
41
|
}
|
|
101
|
-
// The `no-scn-code` item (@SCN-LDG-021) is SCENARIO-scope, like both-missing: without an @SCN
|
|
102
|
-
// code no observation can link at ANY tier, so it names no quality-check and renders with its
|
|
103
|
-
// glyph + description alone. (Debit-side, but subject-less — the missing thing is the LINK.)
|
|
104
42
|
if (item.outcome === "no-scn-code") {
|
|
105
43
|
return `${ITEM_GLYPH["no-scn-code"]} ${OUTCOME_DESCRIPTION["no-scn-code"]}`;
|
|
106
44
|
}
|
|
45
|
+
if (item.qualityCheck === undefined) {
|
|
46
|
+
return `${ITEM_GLYPH[item.outcome]} ${OUTCOME_DESCRIPTION[item.outcome]}`;
|
|
47
|
+
}
|
|
107
48
|
const subject = item.qualityCheck;
|
|
108
49
|
const suffix = creditSuffix(item) ?? "";
|
|
109
50
|
return `${subject} ${ITEM_GLYPH[item.outcome]} ${OUTCOME_DESCRIPTION[item.outcome]}${suffix}`;
|
|
110
51
|
}
|
|
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
52
|
function creditSuffix(item) {
|
|
116
|
-
// A CREDIT-SIDE item surfaces its WHERE-TO-FIX in the reconciliation cell (ruling
|
|
117
|
-
// Q5): a lint credit (the uncited static-check folded onto the suspense row, ruling
|
|
118
|
-
// Q4) shows its static check handle; a runtime credit (phantom `cites-unknown-scenario` or
|
|
119
|
-
// suspense `cites-no-scenario`) shows its evidence observation locator. A DEBIT-side evidence obligation
|
|
120
|
-
// (satisfied / missing / failing / watermelon) carries neither and renders bare, as before.
|
|
121
|
-
//
|
|
122
|
-
// READ OFF THE CANON TABLE rather than listed here. A credit-side outcome is exactly one canon
|
|
123
|
-
// calls (Obligation None, Observation Exists) — `cites-no-scenario`, `cites-unknown-scenario`,
|
|
124
|
-
// `no-evidence-obligation` — and a hand-written list of the three would agree with
|
|
125
|
-
// `OUTCOME_SIDE_PROFILE` by copy rather than by construction, free to drift the day a fourth
|
|
126
|
-
// joins. (`broken` and `both-missing` are (None, None), so both terms are read.)
|
|
127
53
|
const profile = OUTCOME_SIDE_PROFILE[item.outcome];
|
|
128
54
|
const isCredit = profile.obligation === "None" && profile.observation === "Exists";
|
|
129
|
-
// The `broken` credit (@SCN-RPT-021) carries BOTH halves of its where-to-fix: the locator of the
|
|
130
|
-
// evidence that never ran (a test file, or a feature file) AND the message it carries. The
|
|
131
|
-
// message's HEADLINE LINE only — a vitest collection message is multi-line (message + stack) and
|
|
132
|
-
// a stack in a table cell would destroy the table; the full message is printed in the
|
|
133
|
-
// `## Run integrity` section above, which is where a reader diagnoses. Here the row stays
|
|
134
|
-
// legible without leaving the table.
|
|
135
55
|
if (item.outcome === "broken") {
|
|
136
56
|
const where = [item.locator, item.message === undefined ? undefined : headlineOf(item.message)]
|
|
137
57
|
.filter((part) => part !== undefined && part.length > 0)
|
|
138
58
|
.join(": ");
|
|
139
59
|
return where.length > 0 ? ` (${cellSafe(where)})` : undefined;
|
|
140
60
|
}
|
|
141
|
-
if (isCredit && item.
|
|
142
|
-
return ` (${item.
|
|
61
|
+
if (isCredit && item.fitnessFunction)
|
|
62
|
+
return ` (${item.fitnessFunction})`;
|
|
143
63
|
if (isCredit && item.locator !== undefined)
|
|
144
64
|
return ` (${item.locator})`;
|
|
145
65
|
return undefined;
|
|
146
66
|
}
|
|
147
|
-
/**
|
|
148
|
-
* The first non-empty line of a runner message — its HEADLINE (`Error: Cannot find package
|
|
149
|
-
* '@cucumber/tag-expressions' imported from …`), the line that names what broke. The rest is
|
|
150
|
-
* the stack, which belongs in the fenced verbatim print, not in a table cell.
|
|
151
|
-
*/
|
|
152
67
|
function headlineOf(message) {
|
|
153
68
|
return message.split("\n").map((line) => line.trim()).find((line) => line.length > 0) ?? "";
|
|
154
69
|
}
|
|
155
|
-
/**
|
|
156
|
-
* Make an arbitrary runner-supplied string safe to sit INSIDE a markdown table cell: collapse
|
|
157
|
-
* every run of whitespace to a single space, and escape the pipe — an unescaped `|` in a cell
|
|
158
|
-
* silently splits it into two columns and corrupts the table from that row on. Defensive: the
|
|
159
|
-
* text comes from the runner, not from us.
|
|
160
|
-
*/
|
|
161
70
|
function cellSafe(text) {
|
|
162
71
|
return text.replace(/\s+/g, " ").replace(/\|/g, "\\|").trim();
|
|
163
72
|
}
|
|
164
|
-
/**
|
|
165
|
-
* Render a Pending Item (@SCN-PND-010) — the one-sided IOU a deliberately-deferred
|
|
166
|
-
* `<quality-check>pending` obligation raises (evidence-manifesto §11B). Labelled DISTINCT from
|
|
167
|
-
* a reconciling item: the ⏳ marker + the explicit "pending" word read as neither
|
|
168
|
-
* balanced-clean (the `—` placeholder) nor an error (a reconciling item's ✗/∅/🍉 outcome). It
|
|
169
|
-
* is NEVER run through `renderItem` and never enters the reconciling-item list — a Pending
|
|
170
|
-
* Item is a structurally distinct category, never one of the out-of-balance leaf outcomes.
|
|
171
|
-
*/
|
|
172
73
|
function renderPendingItem(item) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
return item.qualityCheck === undefined ? "⏳ pending" : `⏳ pending: ${item.qualityCheck}`;
|
|
74
|
+
return item.qualityCheck === undefined
|
|
75
|
+
? CORE_MESSAGES[CoreMessage.PendingItem]
|
|
76
|
+
: CORE_MESSAGES[CoreMessage.PendingItemFor](item.qualityCheck);
|
|
177
77
|
}
|
|
178
|
-
/**
|
|
179
|
-
* The exceptions cell for a Scenario Reconciliation row (@SCN-PND-010) — its reconciling items
|
|
180
|
-
* (the errors an out-of-balance verdict carries) and its Pending Items (the deferred IOUs),
|
|
181
|
-
* each rendered as its OWN distinctly-labelled group and then joined. The two groups are built
|
|
182
|
-
* separately, so a Pending Item is never folded into the reconciling-item list nor mistaken for
|
|
183
|
-
* an error. A row with neither renders the em-dash placeholder; a balanced-with-pending row
|
|
184
|
-
* shows its ⏳ pending group alone (distinct from the clean-balanced `—`). A row with no
|
|
185
|
-
* `pendingItems` renders byte-identically to before (reconciling items only, or `—`).
|
|
186
|
-
*/
|
|
187
78
|
function renderRowExceptions(s) {
|
|
188
79
|
const reconciling = s.reconcilingItems.map(renderItem).join("; ");
|
|
189
|
-
const pending =
|
|
80
|
+
const pending = s.pendingItems.map(renderPendingItem).join("; ");
|
|
190
81
|
const groups = [reconciling, pending].filter((g) => g.length > 0);
|
|
191
82
|
return groups.length === 0 ? "—" : groups.join("; ");
|
|
192
83
|
}
|
|
193
|
-
|
|
194
|
-
* Render one scenario row: its id, its binary verdict, and its exceptions cell — EVERY
|
|
195
|
-
* reconciling item followed by any Pending Item (labelled distinct). A balanced scenario with
|
|
196
|
-
* neither is rendered as an em-dash placeholder.
|
|
197
|
-
*/
|
|
198
|
-
function renderScenario(s) {
|
|
199
|
-
return `| ${s.scnId} | ${VERDICT_GLYPH[s.verdict]} ${s.verdict} | ${renderRowExceptions(s)} |`;
|
|
200
|
-
}
|
|
201
|
-
/** The report title — the report IS the reconciliation (the Evidence reconciliation is its core). */
|
|
202
|
-
// The report IS the reconciliation. "Ledger" names only the INPUT in canon (evidence
|
|
203
|
-
// entries by scenario); the rendered document is the reconciled OUTPUT, never "Ledger"
|
|
204
|
-
// (double-entry-manifesto). Renamed from "# Ledger" → "# Spec reconciliation" → here.
|
|
205
|
-
const titleSection = () => ["# Evidence Reconciliation", ""];
|
|
206
|
-
/**
|
|
207
|
-
* The RUN-INTEGRITY stamp (@SCN-RPT-021) — the customer-visible payoff of the `broken`
|
|
208
|
-
* reconciling item (3F-1642), and the ONLY place the word **"unsound"** appears. The renderer
|
|
209
|
-
* asks `hasBrokenEvidence` and prints the headline word: predicate-name ≠ headline-word, the
|
|
210
|
-
* `isBalanced` / "balanced" precedent.
|
|
211
|
-
*
|
|
212
|
-
* WHY IT SITS HERE — after provenance, BEFORE the summary views. The stamp is not one more cut
|
|
213
|
-
* of the reconciliation; it is a caveat ON all of them. A behaviour that was never exercised
|
|
214
|
-
* reconciles `missing` whether its proof is ABSENT or merely BROKEN, so a `missing` row in an
|
|
215
|
-
* unsound run cannot be read as "write the test" — the reader must know that before reading a
|
|
216
|
-
* single count. Every other section keeps its @SCN-RPT-009 summary-first / ledger-last position.
|
|
217
|
-
*
|
|
218
|
-
* WHAT IT PRINTS. The broken evidence's locator (the WHERE) and its message in a fenced block
|
|
219
|
-
* (the WHAT) — multi-line, stack included where there is one, because that is what the reader
|
|
220
|
-
* diagnoses from and a table cell cannot hold it.
|
|
221
|
-
*
|
|
222
|
-
* TWO PRODUCERS, ONE SHAPE, NO BRANCH (3F-1654). `broken` now has two instances — a vitest suite
|
|
223
|
-
* that failed to COLLECT, and a cucumber step two or more step definitions match (@SCN-LDG-019) —
|
|
224
|
-
* so the copy here is GENERIC: an ambiguous step is neither a "suite" nor a "collection error",
|
|
225
|
-
* and a renderer that called it one would be the tool lying about what broke, in the one place the
|
|
226
|
-
* reader will ever see it. The renderer needs no per-reader branch: it prints what the ITEM
|
|
227
|
-
* CARRIES. Where the runner gives its own words (vitest's collection error) the message IS those
|
|
228
|
-
* words, passed through VERBATIM; where the runner gives none (cucumber emits no `error_message`
|
|
229
|
-
* for an ambiguous step) the READER synthesised the diagnosis upstream. The difference is resolved
|
|
230
|
-
* before the renderer sees it.
|
|
231
|
-
*
|
|
232
|
-
* THE BOUNDED SOFTENING of 3F-1642's "print verbatim — the tool does not interpret" stance
|
|
233
|
-
* (operator-mandated; exactly three clauses, no further): pass the runner's words through where it
|
|
234
|
-
* gives them; STATE the diagnosis — WHAT BROKE — where it does not; PRESCRIBE NO CURE, either way.
|
|
235
|
-
* The renderer may name WHAT failed and WHERE. It may never say how to fix it. The gloss stays
|
|
236
|
-
* REMEDY-NEUTRAL: what broke is equally a missing dependency, a syntax error, a bad import, a
|
|
237
|
-
* top-level throw, or two colliding step definitions, so the report never guesses the cure (never
|
|
238
|
-
* `pnpm install`, and never "delete one of the step definitions"). The one remedy that always
|
|
239
|
-
* holds — fix the harness and regenerate — is the one it gives, carrying the parent's fuller
|
|
240
|
-
* `OUTCOME_DESCRIPTION` gloss through to the reader; it POINTS, it does not diagnose a remedy.
|
|
241
|
-
*
|
|
242
|
-
* OMIT-WHEN-EMPTY (the Pending Items tally precedent): a run carrying no broken evidence
|
|
243
|
-
* returns `[]`, so a SOUND report is byte-identical to the pre-slice output — no stray empty
|
|
244
|
-
* section. Markdown-only: a machine consumer reads soundness as `summary.byOutcome.broken > 0`,
|
|
245
|
-
* already type-forced into the JSON, so no new JSON field is owed.
|
|
246
|
-
*/
|
|
84
|
+
const titleSection = () => [CORE_MESSAGES[CoreMessage.ReportTitle], ""];
|
|
247
85
|
const runIntegritySection = (report) => {
|
|
248
|
-
// The predicate is the single source of the verdict — the renderer never re-derives
|
|
249
|
-
// "is this run sound?" with its own scan, so the stamp cannot drift from the CLI's exit 4
|
|
250
|
-
// (@SCN-CLI-012), which asks the same predicate.
|
|
251
86
|
if (!hasBrokenEvidence({ scenarioReconciliations: report.ledger.scenarioReconciliations }))
|
|
252
87
|
return [];
|
|
253
|
-
// ASKED, not derived (3F-2203) — which items are broken is a model query, and it is the same
|
|
254
|
-
// scan `hasBrokenEvidence` above is now derived from, so the stamp and the list cannot disagree.
|
|
255
|
-
// Taking `.length` of a list you were handed is projection, not derivation.
|
|
256
88
|
const brokenItems = brokenItemsOf({
|
|
257
89
|
scenarioReconciliations: report.ledger.scenarioReconciliations,
|
|
258
90
|
});
|
|
259
91
|
const count = brokenItems.length;
|
|
260
|
-
// GENERIC over both producers — the count-phrase names what a collection-failed vitest suite and
|
|
261
|
-
// an ambiguous cucumber step have in common (the proof did not run), never one kind's idiom.
|
|
262
92
|
const proofs = count === 1
|
|
263
|
-
?
|
|
264
|
-
:
|
|
93
|
+
? CORE_MESSAGES[CoreMessage.OneProofNeverRan]
|
|
94
|
+
: CORE_MESSAGES[CoreMessage.ProofsNeverRan](count);
|
|
265
95
|
const lines = [
|
|
266
|
-
|
|
96
|
+
CORE_MESSAGES[CoreMessage.RunIntegrityHeading],
|
|
267
97
|
"",
|
|
268
|
-
|
|
269
|
-
"below may therefore be a BROKEN proof rather than an absent one — it cannot be read as " +
|
|
270
|
-
`"write the test", because the test may already be there. Fix the harness and regenerate.`,
|
|
98
|
+
CORE_MESSAGES[CoreMessage.RunUnsound](proofs),
|
|
271
99
|
"",
|
|
272
100
|
];
|
|
273
101
|
for (const item of brokenItems) {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
// section honest if one ever does not.
|
|
277
|
-
lines.push(`- \`${item.locator ?? "(evidence not named by the runner)"}\` — what broke:`);
|
|
102
|
+
const locator = item.locator ?? CORE_MESSAGES[CoreMessage.BrokenEvidenceNotNamed];
|
|
103
|
+
lines.push(`- ${CORE_MESSAGES[CoreMessage.BrokenEvidenceWhatBroke](`\`${locator}\``)}`);
|
|
278
104
|
lines.push("");
|
|
279
|
-
// WHAT broke, exactly as the item carries it: the runner's own words VERBATIM where the runner
|
|
280
|
-
// gave them (vitest — every line, stack included, unedited: the tool does not interpret the
|
|
281
|
-
// error), and the reader's synthesised diagnosis where it gave none (cucumber's ambiguous step
|
|
282
|
-
// has no `error_message` to quote). No branch — one shape, resolved upstream. Either way the
|
|
283
|
-
// line names what failed; it never prescribes the cure.
|
|
284
105
|
lines.push("```text");
|
|
285
|
-
lines.push(...(item.message ??
|
|
106
|
+
lines.push(...(item.message ?? CORE_MESSAGES[CoreMessage.BrokenEvidenceNoMessage]).split("\n"));
|
|
286
107
|
lines.push("```");
|
|
287
108
|
lines.push("");
|
|
288
109
|
}
|
|
289
110
|
return lines;
|
|
290
111
|
};
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
* Evidence reconciliation (@SCN-RPT-004 Update — supersedes the `Axis |
|
|
300
|
-
* Count` shape) — a SUBJECTS-AS-ROWS table: each subject (Scenario Reconciliation, Evidence
|
|
301
|
-
* obligation) on its own row with its own Total, and balanced / out-of-balance as COLUMNS (both
|
|
302
|
-
* subjects report into the same binary, so the "Verdict"/"outcome"/"Axis" words retire).
|
|
303
|
-
*
|
|
304
|
-
* The first subject is the SCENARIO RECONCILIATION — the manifesto's collection, ALL three row
|
|
305
|
-
* kinds (scenario, phantom, suspense) — not `Scenario`, which names only the real thing
|
|
306
|
-
* (3F-1726). Because the row is a pure presentation of `summary.scenarioReconciliations`, whose
|
|
307
|
-
* totals count every row (@SCN-RMD-001, 3F-1724 — a phantom / suspense reconciliation is always
|
|
308
|
-
* out-of-balance), a run whose only out-of-balance row is the suspense reconciliation renders
|
|
309
|
-
* `N+1 | N | 1`, never `N | N | 0`. The subject's out-of-balance column therefore agrees with
|
|
310
|
-
* `isBalanced` and with the exit code — the three views that disagreed in the AWTY 17-36-50Z run
|
|
311
|
-
* (3F-1703), where a failing run's tally read `141 | 141 | 0` and looked like a clean board.
|
|
312
|
-
* The evidence-obligation out-of-balance is decomposed IN PLACE into its reconciling-item
|
|
313
|
-
* causes — three nested (`↳`) rows in canonical order (missing,
|
|
314
|
-
* failing, the watermelon),
|
|
315
|
-
* populating the out-of-balance column only and ALWAYS shown (a zero cause is itself a
|
|
316
|
-
* fact). The missing row carries the bare-level split as Detail — missing
|
|
317
|
-
* evidenceObligations grouped by subject, counting evidence obligations not scenarios.
|
|
318
|
-
*
|
|
319
|
-
* The scenario-verdict and evidence-obligation out-of-balance counts diverge ON PURPOSE:
|
|
320
|
-
* one out-of-balance scenario can carry several missing evidenceObligations (SCN-PST-035's double
|
|
321
|
-
* arm — the live 43 scenarios vs 44 evidenceObligations). The reshape presents that as a
|
|
322
|
-
* granularity fact, not a contradiction. The nested causes reconcile to the evidenceObligation
|
|
323
|
-
* out-of-balance. No bold. A pure presentation of the report's materialised
|
|
324
|
-
* `summary.scenarioReconciliations` / `summary.evidenceObligations` totals; markdown-only.
|
|
325
|
-
*/
|
|
112
|
+
const summaryViewsSection = () => [CORE_MESSAGES[CoreMessage.SummaryViewsHeading], ""];
|
|
113
|
+
const EVIDENCE_RECONCILIATION_COLUMNS = [
|
|
114
|
+
CORE_MESSAGES[CoreMessage.SubjectColumn],
|
|
115
|
+
CORE_MESSAGES[CoreMessage.TotalColumn],
|
|
116
|
+
CORE_MESSAGES[CoreMessage.BalancedColumn],
|
|
117
|
+
CORE_MESSAGES[CoreMessage.OutOfBalanceColumn],
|
|
118
|
+
CORE_MESSAGES[CoreMessage.DetailColumn],
|
|
119
|
+
];
|
|
326
120
|
const evidenceReconciliationSection = (report) => {
|
|
327
121
|
const scenarioReconciliations = report.summary.scenarioReconciliations;
|
|
328
122
|
const evidenceObligations = report.summary.evidenceObligations;
|
|
329
123
|
const totalEvidenceObligations = evidenceObligations.total;
|
|
330
|
-
// The evidenceObligation out-of-balance is the sum of the three non-balanced outcomes — the
|
|
331
|
-
// total the nested causes reconcile to.
|
|
332
124
|
const evidenceObligationsOutOfBalance = evidenceObligations.outOfBalance;
|
|
333
|
-
// A five-cell row; an empty string renders as an empty cell (the Total/Balanced cells
|
|
334
|
-
// of a nested cause row, and the Detail cell of a subject row).
|
|
335
125
|
const row = (cells) => `| ${cells.join(" | ")} |`;
|
|
336
|
-
// The bare-level split of the missing evidenceObligations, materialised on
|
|
337
|
-
// the report as `{ subject, count }[]` in canonical order — rendered `43 unit + 1 integration`.
|
|
338
126
|
const missingSplit = evidenceObligations.missingByQualityCheck
|
|
339
127
|
.map(({ subject, count }) => `${count} ${subject}`)
|
|
340
128
|
.join(" + ");
|
|
341
129
|
const lines = [
|
|
342
|
-
|
|
130
|
+
CORE_MESSAGES[CoreMessage.EvidenceReconciliationHeading],
|
|
343
131
|
"",
|
|
344
|
-
row(
|
|
132
|
+
row(EVIDENCE_RECONCILIATION_COLUMNS),
|
|
345
133
|
row(["---", "---", "---", "---", "---"]),
|
|
346
134
|
row([
|
|
347
|
-
|
|
348
|
-
// phantom and suspense rows alike), not `Scenario`, which names only the real thing
|
|
349
|
-
// (@SCN-RPT-004, 3F-1726; `scenarios` is in the manifesto's retired column).
|
|
350
|
-
"Scenario Reconciliation",
|
|
135
|
+
CORE_MESSAGES[CoreMessage.ScenarioReconciliationSubject],
|
|
351
136
|
String(scenarioReconciliations.total),
|
|
352
137
|
String(scenarioReconciliations.balanced),
|
|
353
138
|
String(scenarioReconciliations.outOfBalance),
|
|
354
139
|
"",
|
|
355
140
|
]),
|
|
356
141
|
row([
|
|
357
|
-
|
|
142
|
+
CORE_MESSAGES[CoreMessage.EvidenceObligationSubject],
|
|
358
143
|
String(totalEvidenceObligations),
|
|
359
144
|
String(evidenceObligations.byOutcome.satisfied),
|
|
360
145
|
String(evidenceObligationsOutOfBalance),
|
|
361
146
|
"",
|
|
362
147
|
]),
|
|
363
148
|
];
|
|
364
|
-
// Decompose the evidence-obligation out-of-balance into its causes — a FLAT list of the
|
|
365
|
-
// nine out-of-balance leaf outcomes in canonical order (@SCN-RPT-019, 3F-1453; the ninth,
|
|
366
|
-
// `no-scn-code`, added by @SCN-LDG-021, 3F-1774). The list is DERIVED from the type-forced
|
|
367
|
-
// OUTCOME_DESCRIPTION (3F-1806), so a new Outcome renders here with no edit to this file;
|
|
368
|
-
// its ROW ORDER is that map's key order, which is therefore load-bearing. Each a
|
|
369
|
-
// counted `↳ <outcome>` row (only the out-of-balance column populated) reconciling to the
|
|
370
|
-
// total exactly once. No debit/credit/neither grouping: the grouping emitted a subtotal
|
|
371
|
-
// `↳ <group>-side` row AND its leaf rows at the same `↳` indent, so a naive column-sum
|
|
372
|
-
// double-counted (the live 170 vs 85). `missing` carries the bare-level split as Detail.
|
|
373
|
-
// Every outcome is ALWAYS rendered, including a zero count (a zero cause is itself a fact).
|
|
374
149
|
for (const outcome of OUT_OF_BALANCE_OUTCOMES) {
|
|
375
150
|
const detail = outcome === "missing" ? missingSplit : "";
|
|
376
|
-
// The Subject cell shows the outcome's CANON DESCRIPTION, not the bare handle (3F-1626)
|
|
377
|
-
// — single-sourced from OUTCOME_DESCRIPTION so it cannot drift from the outcome it names.
|
|
378
151
|
lines.push(row([`↳ ${OUTCOME_DESCRIPTION[outcome]}`, "", "", String(evidenceObligations.byOutcome[outcome]), detail]));
|
|
379
152
|
}
|
|
380
|
-
// The Pending Items tally (@SCN-PND-011) — a SEPARATE line, NEVER an out-of-balance leaf of
|
|
381
|
-
// its own, at any leaf count (3F-1453 line, endorsed; re-endorsed at eight leaves, 3F-1649;
|
|
382
|
-
// the count is now nine, 3F-1774 — the constraint is categorical, not arithmetic): a Pending
|
|
383
|
-
// Item is deferred, not out-of-balance, so it is
|
|
384
|
-
// tallied apart from BOTH the balanced and out-of-balance counts and the `↳` leaf
|
|
385
|
-
// decomposition above — its own line, never a `↳` row under the out-of-balance table. Read
|
|
386
|
-
// off the materialised `summary.pending`; rendered only when the run genuinely carries Pending
|
|
387
|
-
// Items (the omit-when-empty precedent), so a zero-pending report stays byte-identical.
|
|
388
153
|
lines.push(...pendingTally(report.summary.pending));
|
|
389
154
|
lines.push("");
|
|
390
155
|
return lines;
|
|
391
156
|
};
|
|
392
|
-
/** The Pending Items tally line, preceded by its blank — or nothing at all when none is pending. */
|
|
393
157
|
function pendingTally(pending) {
|
|
394
158
|
return pending > 0
|
|
395
|
-
? ["",
|
|
159
|
+
? ["", CORE_MESSAGES[CoreMessage.PendingTally](pending)]
|
|
396
160
|
: [];
|
|
397
161
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
* by the @lint TAG) — lifts every `@lint` evidence obligation out of the per-scenario trial
|
|
401
|
-
* balance into its OWN `### Lint evidence obligations` table `| Scenario | Static check | Outcome |`,
|
|
402
|
-
* one row per obligation in document order, each `outcome` carrying its `ITEM_GLYPH` glyph (so a
|
|
403
|
-
* 🍉 watermelon reads at a glance). The total line STATES how many of them reconcile balanced —
|
|
404
|
-
* read off the report's materialised `summary.lint` tally. Omitted entirely (returns `[]`) when no
|
|
405
|
-
* scenario names a lint obligation. A pure derive over the ledger rows; markdown-only.
|
|
406
|
-
*/
|
|
407
|
-
const lintSection = (report) => {
|
|
408
|
-
// Scenario rows only — a lint evidence obligation is an obligation-side thing. The suspense row's
|
|
409
|
-
// lint credit (an uncited static-check, cites-no-scenario) is NOT an evidence obligation, so
|
|
410
|
-
// it stays out of this table + tally; it surfaces on the suspense row's reconciling items.
|
|
411
|
-
// ASKED, not derived (3F-2203) — the obligation-side lint sub-rows are a model list, and it is the SAME
|
|
412
|
-
// list `summary.lint` is tallied from, so the table and its total line cannot disagree. The
|
|
413
|
-
// scenario-rows-only cut (a suspense lint CREDIT is an observation, not an obligation) lives with
|
|
414
|
-
// the rule; it stood here and in `lintTally`, character-identical, agreeing by copy.
|
|
415
|
-
const rows = lintReconciliationsOf({
|
|
162
|
+
const fitnessSection = (report) => {
|
|
163
|
+
const rows = fitnessReconciliationsOf({
|
|
416
164
|
scenarioReconciliations: report.ledger.scenarioReconciliations,
|
|
417
|
-
}).map(
|
|
418
|
-
// The Outcome cell shows the canon DESCRIPTION, not the bare handle (3F-1626,
|
|
419
|
-
// single-sourced from OUTCOME_DESCRIPTION); the ITEM_GLYPH glyph is retained.
|
|
420
|
-
({ scnId, reconciliation: r }) => `| ${scnId} | ${r.staticCheck} | ${ITEM_GLYPH[r.outcome]} ${OUTCOME_DESCRIPTION[r.outcome]} |`);
|
|
165
|
+
}).map(({ scnId, reconciliation: r }) => `| ${scnId} | ${ITEM_GLYPH[r.outcome]} ${OUTCOME_DESCRIPTION[r.outcome]} |`);
|
|
421
166
|
if (rows.length === 0)
|
|
422
167
|
return [];
|
|
423
|
-
const { satisfied, total } = report.summary.
|
|
168
|
+
const { satisfied, total } = report.summary.fitness;
|
|
424
169
|
return [
|
|
425
|
-
|
|
170
|
+
CORE_MESSAGES[CoreMessage.FitnessHeading],
|
|
426
171
|
"",
|
|
427
|
-
|
|
172
|
+
CORE_MESSAGES[CoreMessage.FitnessSatisfiedOf](satisfied, total),
|
|
428
173
|
"",
|
|
429
|
-
|
|
430
|
-
"| --- | --- |
|
|
174
|
+
CORE_MESSAGES[CoreMessage.FitnessTableHeader],
|
|
175
|
+
"| --- | --- |",
|
|
431
176
|
...rows,
|
|
432
177
|
"",
|
|
433
178
|
];
|
|
434
179
|
};
|
|
435
|
-
/**
|
|
436
|
-
* Out-of-balance by feature (@SCN-RPT-006; re-cut under Summary views with a
|
|
437
|
-
* total line, rendered as a TABLE) — re-cuts the `verdict === "out-of-balance"`
|
|
438
|
-
* scenarios by their feature code (the `<FFF>` parsed from each `scnId` via the shared
|
|
439
|
-
* `featureCodeOf` helper), so a reader sees the discrepancy CONCENTRATION by feature.
|
|
440
|
-
* One row per feature in first-appearance (document) order, with a per-feature count and
|
|
441
|
-
* the out-of-balance ids. The `FFF` prefix only — the full feature names are added separately.
|
|
442
|
-
* A malformed/unparseable scnId (`featureCodeOf` → null) falls back to its raw id as the
|
|
443
|
-
* group key, so no discrepancy is dropped. The Feature cell reads `<slug> (<FFF>)` when a
|
|
444
|
-
* `featureNames` map provides the slug for the code, else the bare `FFF` — the
|
|
445
|
-
* fallback for an uncoded id (no file registers the code) or the no-map back-compat path.
|
|
446
|
-
* Omitted entirely when no scenario is out of balance. A pure derive over
|
|
447
|
-
* `report.ledger.scenarioReconciliations` (+ the model's `metadata.featureNames` title lookup);
|
|
448
|
-
* markdown-only.
|
|
449
|
-
*/
|
|
450
180
|
const byFeatureSection = (report) => {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
const
|
|
181
|
+
const rows = report.ledger.scenarioReconciliations;
|
|
182
|
+
const scenarios = rows.filter(isScenario);
|
|
183
|
+
const groups = outOfBalanceByFeature(scenarios, report.evidenceObligations?.untaggedScenarios ?? [], report.metadata?.featureNames ?? {});
|
|
184
|
+
const ungrouped = ungroupedOutOfBalance(report.summary.scenarioReconciliations);
|
|
185
|
+
if (groups.size === 0 && ungrouped.length === 0)
|
|
186
|
+
return [];
|
|
187
|
+
const outOfBalance = scenarios.filter((s) => s.verdict === "out-of-balance").length;
|
|
188
|
+
const head = CORE_MESSAGES[CoreMessage.ScenarioRowsOutOfBalance](outOfBalance, scenarios.length);
|
|
189
|
+
return [
|
|
190
|
+
CORE_MESSAGES[CoreMessage.OutOfBalanceByFeatureHeading],
|
|
191
|
+
"",
|
|
192
|
+
`${head}${groups.size > 0 ? CORE_MESSAGES[CoreMessage.GroupedByFeature] : ""}${ungroupedTail(ungrouped)}.`,
|
|
193
|
+
"",
|
|
194
|
+
...byFeatureTableLines(groups, report.metadata?.featureNames),
|
|
195
|
+
];
|
|
196
|
+
};
|
|
197
|
+
function outOfBalanceByFeature(scenarios, untagged, featureNames) {
|
|
198
|
+
const codeOfFile = new Map(Object.entries(featureNames).map(([code, slug]) => [`${slug}.feature`, code]));
|
|
199
|
+
const featureOf = new Map(untagged.map((u) => [untaggedScenarioId(u), codeOfFile.get(u.feature) ?? u.feature]));
|
|
454
200
|
const groups = new Map();
|
|
455
201
|
for (const s of scenarios) {
|
|
456
202
|
if (s.verdict !== "out-of-balance")
|
|
457
203
|
continue;
|
|
458
|
-
const
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
|
|
204
|
+
const noScnCode = carriesNoScnCode(s);
|
|
205
|
+
const key = (noScnCode ? featureOf.get(s.scnId) : featureCodeOf(s.scnId)) ?? s.scnId;
|
|
206
|
+
const group = groups.get(key) ?? { ids: [], noScnCode: 0 };
|
|
207
|
+
if (noScnCode)
|
|
208
|
+
group.noScnCode += 1;
|
|
209
|
+
else
|
|
210
|
+
group.ids.push(s.scnId);
|
|
211
|
+
groups.set(key, group);
|
|
462
212
|
}
|
|
213
|
+
return groups;
|
|
214
|
+
}
|
|
215
|
+
function carriesNoScnCode(row) {
|
|
216
|
+
return row.reconcilingItems.some((item) => item.outcome === "no-scn-code");
|
|
217
|
+
}
|
|
218
|
+
function byFeatureTableLines(groups, featureNames) {
|
|
463
219
|
if (groups.size === 0)
|
|
464
220
|
return [];
|
|
465
|
-
const
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
"",
|
|
471
|
-
"| Feature | Count | Out-of-balance `@SCN` ids |",
|
|
472
|
-
"| --- | --- | --- |",
|
|
473
|
-
];
|
|
474
|
-
for (const [code, ids] of groups) {
|
|
475
|
-
const slug = report.metadata?.featureNames?.[code];
|
|
476
|
-
const label = slug ? `${slug} (${code})` : code;
|
|
477
|
-
lines.push(`| ${label} | ${ids.length} | ${ids.join(", ")} |`);
|
|
221
|
+
const lines = [CORE_MESSAGES[CoreMessage.ByFeatureTableHeader], "| --- | --- | --- |"];
|
|
222
|
+
for (const [key, group] of groups) {
|
|
223
|
+
const slug = featureNames?.[key];
|
|
224
|
+
const label = slug ? `${slug} (${key})` : key;
|
|
225
|
+
lines.push(`| ${label} | ${group.ids.length + group.noScnCode} | ${idsCell(group)} |`);
|
|
478
226
|
}
|
|
479
227
|
lines.push("");
|
|
480
228
|
return lines;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
229
|
+
}
|
|
230
|
+
function idsCell(group) {
|
|
231
|
+
const ids = group.ids.join(", ");
|
|
232
|
+
if (group.noScnCode === 0)
|
|
233
|
+
return ids;
|
|
234
|
+
const noScnCode = CORE_MESSAGES[CoreMessage.NoScnCodeScenarios](group.noScnCode);
|
|
235
|
+
return ids === "" ? noScnCode : `${ids}${CORE_MESSAGES[CoreMessage.AndJoiner]}${noScnCode}`;
|
|
236
|
+
}
|
|
237
|
+
function ungroupedOutOfBalance(totals) {
|
|
238
|
+
return [
|
|
239
|
+
...(totals.phantom > 0 ? [CORE_MESSAGES[CoreMessage.PhantomScenarioRows](totals.phantom)] : []),
|
|
240
|
+
...(totals.suspense > 0 ? [CORE_MESSAGES[CoreMessage.SuspenseScenarioRows](totals.suspense)] : []),
|
|
241
|
+
];
|
|
242
|
+
}
|
|
243
|
+
function ungroupedTail(ungrouped) {
|
|
244
|
+
return ungrouped.length > 0
|
|
245
|
+
? `; ${CORE_MESSAGES[CoreMessage.NotGroupedByFeature](ungrouped.join(CORE_MESSAGES[CoreMessage.AndJoiner]))}`
|
|
246
|
+
: "";
|
|
247
|
+
}
|
|
493
248
|
const ledgerSection = (report) => {
|
|
494
|
-
// Group the scenario rows first, then the PHANTOM rows, then the SUSPENSE row as
|
|
495
|
-
// distinct blocks at the END (ruling Q3) — a phantom/suspense is not a
|
|
496
|
-
// scenario, so it never mingles with the scenario verdict rows.
|
|
497
249
|
const scenarioRows = report.ledger.scenarioReconciliations.filter(isScenario);
|
|
498
250
|
const phantomRows = report.ledger.scenarioReconciliations.filter((s) => s.kind === "phantom-scenario");
|
|
499
251
|
const suspenseRows = report.ledger.scenarioReconciliations.filter((s) => s.kind === "suspense-scenario");
|
|
500
252
|
const scenarios = [...scenarioRows, ...phantomRows, ...suspenseRows];
|
|
501
|
-
// The runtime-observation axis is IN PLAY iff the summary carries the test-volume fold
|
|
502
|
-
// (@SCN-RPT-010 / 3F-2103 — the gate that was the retired `evidenceObservations` section's
|
|
503
|
-
// presence). When it is, the ledger gains the corpus-driven evidence columns.
|
|
504
253
|
const withObservations = report.summary.testVolume !== undefined;
|
|
505
254
|
return [
|
|
506
|
-
|
|
507
|
-
// ("Ledger" is the INPUT concept in canon; double-entry-manifesto).
|
|
508
|
-
"## Scenario Reconciliations",
|
|
255
|
+
CORE_MESSAGES[CoreMessage.ScenarioReconciliationsHeading],
|
|
509
256
|
"",
|
|
510
257
|
...ledgerTallyLines(report.summary.scenarioReconciliations),
|
|
511
|
-
...(report.evidenceObligations ? censusLines(report.evidenceObligations) : []),
|
|
258
|
+
...(report.evidenceObligations ? censusLines(report.evidenceObligations, scenarioRows.length) : []),
|
|
512
259
|
"",
|
|
513
260
|
...ledgerTableLines(scenarios, withObservations),
|
|
514
261
|
"",
|
|
515
262
|
];
|
|
516
263
|
};
|
|
517
|
-
/** The ledger's verdict tally bullet, and the phantom / suspense sub-counts broken out of it. */
|
|
518
264
|
function ledgerTallyLines(totals) {
|
|
519
265
|
const lines = [
|
|
520
|
-
|
|
521
|
-
// suspense alike (@SCN-RMD-001, 3F-1724; the scenario-only "ruling Q3" is retracted).
|
|
522
|
-
`- ${totals.total} Scenario Reconciliation(s): ${totals.balanced} balanced, ` +
|
|
523
|
-
`${totals.outOfBalance} out-of-balance.`,
|
|
266
|
+
`- ${CORE_MESSAGES[CoreMessage.ScenarioReconciliationTally](totals.total, totals.balanced, totals.outOfBalance)}`,
|
|
524
267
|
];
|
|
525
|
-
// The phantom (P) count is broken out as a SUB-COUNT of the tally above — it is counted
|
|
526
|
-
// INSIDE total / out-of-balance (a phantom reconciliation is always out-of-balance), and
|
|
527
|
-
// named here so a reader sees what makes up the total.
|
|
528
268
|
if (totals.phantom > 0) {
|
|
529
|
-
lines.push(`- ${totals.phantom}
|
|
269
|
+
lines.push(`- ${CORE_MESSAGES[CoreMessage.PhantomScenarioTally](totals.phantom)}`);
|
|
530
270
|
}
|
|
531
|
-
// The suspense (S) count is broken out the same way — credits citing no scenario at all
|
|
532
|
-
// (uncited evidence observations + uncited static-checks), aggregated into one row,
|
|
533
|
-
// counted INSIDE the tally above.
|
|
534
271
|
if (totals.suspense > 0) {
|
|
535
|
-
lines.push(`- ${totals.suspense}
|
|
272
|
+
lines.push(`- ${CORE_MESSAGES[CoreMessage.SuspenseScenarioTally](totals.suspense)}`);
|
|
536
273
|
}
|
|
537
274
|
return lines;
|
|
538
275
|
}
|
|
539
|
-
|
|
540
|
-
|
|
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).
|
|
276
|
+
function censusLines(census, accounts) {
|
|
277
|
+
const lines = [`- ${CORE_MESSAGES[CoreMessage.ExcludedScnOccurrences](census.excludedOccurrences)}`];
|
|
550
278
|
const untagged = census.untaggedScenarios;
|
|
551
279
|
if (untagged.length > 0) {
|
|
552
280
|
const sorted = [...untagged].sort((a, b) => a.feature.localeCompare(b.feature) || a.scenario.localeCompare(b.scenario));
|
|
553
|
-
lines.push(`- ${untagged.length}
|
|
281
|
+
lines.push(`- ${CORE_MESSAGES[CoreMessage.UntaggedScenarios](untagged.length)}`);
|
|
554
282
|
for (const { feature, scenario } of sorted) {
|
|
555
283
|
lines.push(` - ${feature} : ${scenario}`);
|
|
556
284
|
}
|
|
557
285
|
}
|
|
558
|
-
|
|
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}`);
|
|
565
|
-
}
|
|
566
|
-
}
|
|
286
|
+
lines.push(...sharedScnCodeLines(census.sharedScnCodes ?? [], accounts));
|
|
567
287
|
return lines;
|
|
568
288
|
}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
289
|
+
function sharedScnCodeLines(shared, accounts) {
|
|
290
|
+
if (shared.length === 0)
|
|
291
|
+
return [];
|
|
292
|
+
const scenarios = accounts + shared.reduce((extra, code) => extra + code.scenarios.length - 1, 0);
|
|
293
|
+
return [
|
|
294
|
+
`- ${CORE_MESSAGES[CoreMessage.SharedScnCodes](scenarios, accounts, shared.length)}`,
|
|
295
|
+
...shared.map((code) => ` - ${code.scnId}: ${code.scenarios.map((s) => `${s.feature} : ${s.scenario}`).join("; ")}`),
|
|
296
|
+
];
|
|
297
|
+
}
|
|
573
298
|
function ledgerTableLines(scenarios, withObservations) {
|
|
574
299
|
const columns = withObservations ? evidenceColumns(scenarios) : [];
|
|
575
|
-
if (!(withObservations && columns.length > 0)) {
|
|
576
|
-
return [
|
|
577
|
-
"| Scenario | Verdict | Reconciling items |",
|
|
578
|
-
"| --- | --- | --- |",
|
|
579
|
-
...scenarios.map(renderScenario),
|
|
580
|
-
];
|
|
581
|
-
}
|
|
582
300
|
const headers = [
|
|
583
|
-
|
|
301
|
+
CORE_MESSAGES[CoreMessage.ScenarioColumn],
|
|
584
302
|
...columns.map(columnHeader),
|
|
585
|
-
|
|
586
|
-
|
|
303
|
+
CORE_MESSAGES[CoreMessage.VerdictColumn],
|
|
304
|
+
CORE_MESSAGES[CoreMessage.ReconcilingItemsColumn],
|
|
587
305
|
];
|
|
588
306
|
const lines = [`| ${headers.join(" | ")} |`, `| ${headers.map(() => "---").join(" | ")} |`];
|
|
589
307
|
for (const s of scenarios) {
|
|
@@ -593,89 +311,32 @@ function ledgerTableLines(scenarios, withObservations) {
|
|
|
593
311
|
}
|
|
594
312
|
return lines;
|
|
595
313
|
}
|
|
596
|
-
/**
|
|
597
|
-
* The ordered section list (project design §2; reorganised summary-first / ledger-last,
|
|
598
|
-
* test volume added): title → provenance → the run-integrity stamp (only when the run is
|
|
599
|
-
* unsound) → Summary views
|
|
600
|
-
* (Evidence reconciliation, test volume, lint evidence obligations, out-of-balance by feature)
|
|
601
|
-
* → the ledger. The `## Warnings` section was RETIRED entirely: the
|
|
602
|
-
* no-evidence scenario (the earlier zero-evidence warning) is now a `both-missing` ledger
|
|
603
|
-
* row, and the scenario-corpus
|
|
604
|
-
* census facts (untagged scenarios / prose-only codes) re-homed into the ledger summary.
|
|
605
|
-
*
|
|
606
|
-
* The run-integrity stamp (@SCN-RPT-021) precedes the summary views because it is a caveat ON
|
|
607
|
-
* them, not another cut OF them — an unsound run's `missing` counts cannot be taken at face
|
|
608
|
-
* value. It omits itself entirely on a sound run, so the ordering above is unchanged for
|
|
609
|
-
* every report that carries no broken evidence.
|
|
610
|
-
*/
|
|
611
314
|
const SECTIONS = [
|
|
612
315
|
titleSection,
|
|
613
316
|
provenanceSection,
|
|
317
|
+
configurationNotReadSection,
|
|
614
318
|
runIntegritySection,
|
|
319
|
+
scenarioCodeErrorsSection,
|
|
615
320
|
summaryViewsSection,
|
|
616
|
-
// The verdict OPENS the summary (@SCN-RPT-022) — before any cut of the reconciliation, so a
|
|
617
|
-
// reader meets the bottom line first and a failing run can never LOOK like a passing one.
|
|
618
321
|
runVerdictSection,
|
|
619
322
|
evidenceReconciliationSection,
|
|
620
323
|
testVolumeSection,
|
|
621
|
-
// The scope discovery ranged over (@SCN-USG-020), ahead of the static-check census rather than
|
|
622
|
-
// among the dispositions below it: it is not one of the things a step became, it is the pipeline
|
|
623
|
-
// every figure under it was read from. A reader meets what was read before what was found.
|
|
624
324
|
discoveryScopeSection,
|
|
625
|
-
|
|
626
|
-
// The steps recognised as behavioural runners (@SCN-USG-011), beside the static-check counts
|
|
627
|
-
// rather than among them: a runner is what a step was recognised as INSTEAD of reaching that
|
|
628
|
-
// census, so it belongs next to it and never inside it.
|
|
325
|
+
fitnessFunctionsSection,
|
|
629
326
|
behaviouralRunnersSection,
|
|
630
|
-
// The steps that are the reconciler (@SCN-USG-014), between the two: it is the disposition
|
|
631
|
-
// nobody configured, so it reads after what the target named its runners and before the list the
|
|
632
|
-
// consumer wrote — the tool accounting for itself before it accounts for their instructions.
|
|
633
327
|
reconcilerSection,
|
|
634
|
-
// The steps the target named non-checks (@SCN-USG-015), after the runners for the same reason
|
|
635
|
-
// those follow the census: all three are dispositions a step got INSTEAD of reaching the books,
|
|
636
|
-
// and they read in the order the reader meets them — what ran the suites, what this tool took
|
|
637
|
-
// out of its own books, then what the consumer took out.
|
|
638
328
|
excludedStepsSection,
|
|
639
|
-
// What became of every step the run read (@SCN-USG-018), last of the pipeline cuts because it
|
|
640
|
-
// is the one that counts what the four above it named. A reader meets the dispositions and then
|
|
641
|
-
// the figure saying they are all of them — which is the axis's first non-negotiable, stated as
|
|
642
|
-
// something checkable rather than as a sentence taken on trust.
|
|
643
329
|
stepAccountingSection,
|
|
644
|
-
|
|
330
|
+
fitnessSection,
|
|
645
331
|
byFeatureSection,
|
|
646
332
|
ledgerSection,
|
|
647
333
|
];
|
|
648
|
-
/**
|
|
649
|
-
* Render the whole-document report model to markdown — the from-model pipeline,
|
|
650
|
-
* a PURE PRESENTATION over the same `EvidenceReconciliation` the JSON serialises. Exposed for slice 3's
|
|
651
|
-
* render-from-saved-JSON path: a consumer rebuilds a `EvidenceReconciliation` from banked JSON and
|
|
652
|
-
* renders the human view off it, with no `RunContext` in hand. Needs NO external map — the
|
|
653
|
-
* feature titles are read off `report.metadata.featureNames` (@SCN-RMD-006; nested under
|
|
654
|
-
* `metadata`), so the by-feature cut names each feature in full from the model alone.
|
|
655
|
-
*/
|
|
656
334
|
export function renderReport(report) {
|
|
657
335
|
return SECTIONS.flatMap((section) => section(report)).join("\n");
|
|
658
336
|
}
|
|
659
|
-
/**
|
|
660
|
-
* Regenerate the markdown from a banked `spec-reconciliation.json` ALONE (@SCN-RMD-007)
|
|
661
|
-
* — the render-from-saved-JSON seam. `JSON.parse` the stored whole-document report back into a
|
|
662
|
-
* `EvidenceReconciliation` and render it via `renderReport`: no `RunContext`, no live inputs, the JSON is the
|
|
663
|
-
* whole document. Drift-equality is the acceptance — the markdown from the saved JSON equals the
|
|
664
|
-
* markdown rendered directly from the same run (the by-feature titles included, which is why the
|
|
665
|
-
* feature-title model-completion @SCN-RMD-006 is the enabling dependency). The load-bearing proof
|
|
666
|
-
* that the JSON is the model, not a lossy projection.
|
|
667
|
-
*/
|
|
668
337
|
export function renderReportFromJson(json) {
|
|
669
338
|
return renderReport(JSON.parse(json));
|
|
670
339
|
}
|
|
671
|
-
/**
|
|
672
|
-
* Render the reconciliation to markdown. Keeps its `(balance, ctx, …)` signature (mirroring
|
|
673
|
-
* `json.ts`): it assembles the shared whole-document report from the reconcile pieces —
|
|
674
|
-
* threading the live `featureNames` title source INTO `assembleRenderInput`, so the titles
|
|
675
|
-
* land on the model — then delegates to `renderReport`. So `writer.ts` and the unit tests are
|
|
676
|
-
* untouched, the markdown stays byte-identical (same slugs, now model-sourced), and it is
|
|
677
|
-
* provably a presentation of the same model the JSON serialises.
|
|
678
|
-
*/
|
|
679
340
|
export function renderMarkdown(evidenceReconciliation, ctx, evidenceObligations, featureNames) {
|
|
680
341
|
return renderReport(assembleRenderInput(evidenceReconciliation, ctx, evidenceObligations, featureNames));
|
|
681
342
|
}
|