cyber-sdd 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.plugin/pins.json +3 -0
- package/LICENSE +21 -0
- package/agents/sdd-automaton.md +13 -2
- package/agents/sdd-scanner.md +85 -0
- package/agents/sdd-spec-judge.md +32 -2
- package/agents/sdd-warden.md +9 -0
- package/package.json +30 -23
- package/skills/align-spec/scripts/align-spec.mts +3 -2
- package/skills/blast-estimate/README.md +3 -5
- package/skills/blast-estimate/SKILL.md +2 -2
- package/skills/blast-estimate/scripts/blast-estimate.mts +7 -4
- package/skills/builder-impl-governance/SKILL.md +9 -1
- package/skills/builder-spec-governance/SKILL.md +13 -1
- package/skills/check-partition-quality/scripts/check-partition-quality.mts +3 -1
- package/skills/check-plan-safety/scripts/check-plan-safety.mts +5 -2
- package/skills/check-project-specs/scripts/check-project-specs.mts +67 -5
- package/skills/check-retired-terms/README.md +18 -0
- package/skills/check-retired-terms/SKILL.md +81 -0
- package/skills/check-retired-terms/scripts/check-retired-terms.mts +293 -0
- package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mts +3 -2
- package/skills/check-spec-structure/scripts/check-spec-structure.mts +3 -2
- package/skills/collision-ladder/README.md +3 -5
- package/skills/collision-ladder/scripts/collision-ladder.mts +7 -4
- package/skills/combat-log-governance/SKILL.md +43 -4
- package/skills/concept-index/scripts/concept-index.mts +3 -2
- package/skills/discover-plans/scripts/discover-plans.mts +5 -2
- package/skills/discover-specs/scripts/discover-specs.mts +5 -2
- package/skills/doctrine-loop/README.md +6 -0
- package/skills/doctrine-loop/SKILL.md +136 -2
- package/skills/formation-loop/SKILL.md +21 -1
- package/skills/gate-validation-governance/SKILL.md +2 -2
- package/skills/impl-producer-governance/SKILL.md +10 -1
- package/skills/init/scripts/wire-statusline.mts +5 -2
- package/skills/lifecycle-governance/SKILL.md +1 -1
- package/skills/manage-ignore/scripts/manage-ignore.mts +5 -2
- package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mts +5 -2
- package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mts +5 -2
- package/skills/mission-graph/README.md +3 -5
- package/skills/mission-graph/SKILL.md +72 -5
- package/skills/mission-graph/scripts/mission-graph.mts +505 -16
- package/skills/place-node/scripts/place-node.mts +3 -2
- package/skills/plan-retirement/README.md +5 -2
- package/skills/plan-retirement/SKILL.md +5 -1
- package/skills/plan-retirement/scripts/retire-plans.mts +5 -2
- package/skills/plugin-contract-governance/SKILL.md +7 -1
- package/skills/remediation-governance/SKILL.md +36 -1
- package/skills/resolve-governances/scripts/resolve-governances.mts +5 -2
- package/skills/resolve-tracking/SKILL.md +2 -2
- package/skills/resolve-tracking/scripts/resolve-tracking.mts +5 -4
- package/skills/sdd/SKILL.md +1 -1
- package/skills/spec-format-governance/SKILL.md +5 -0
- package/skills/spec-gate/SKILL.md +18 -2
- package/skills/spec-gate/scripts/check-spec-state.mts +47 -11
- package/skills/spec-gate/scripts/check-suite.mts +53 -17
- package/skills/spec-gate/scripts/classify-edit-class.mts +10 -7
- package/skills/spec-producer-governance/SKILL.md +2 -2
- package/skills/ssa-lowering/README.md +3 -5
- package/skills/start-mission/SKILL.md +8 -4
- package/skills/suite-format-governance/SKILL.md +43 -4
- package/skills/touch-set-correction/README.md +3 -5
- package/skills/touch-set-correction/scripts/touch-set-correction.mts +7 -5
- package/skills/verify-scenarios/SKILL.md +10 -3
- package/skills/verify-scenarios/scripts/verify-scenarios.mts +92 -8
|
@@ -70,6 +70,45 @@ the current code**, never the baseline to patch. Reading the standing suite and
|
|
|
70
70
|
a diff notices is not this procedure — it leaves stale scenarios in place and misses edges the CFG
|
|
71
71
|
mandates (ADR-0029).
|
|
72
72
|
|
|
73
|
+
## A fold node states its rule in closed form before its scenarios
|
|
74
|
+
|
|
75
|
+
A **fold** (aggregation) node folds several sub-conditions into one verdict — a ready-frontier folding
|
|
76
|
+
reachability against a mutex, a gate-legality aggregate, a per-cell matrix claim. **When the fold
|
|
77
|
+
combines two or more *interacting* sub-conditions, state its rule in closed form — and re-derive that
|
|
78
|
+
rule's soundness against the real data model — before you draw the CFG.** A **single-condition** fold
|
|
79
|
+
may be specified by example; demanding a closed form of it is the failure mode of this rule, not its
|
|
80
|
+
point. Scenarios drawn off a rule never written down are drawn off a rule never *agreed*, and each
|
|
81
|
+
producer-judge round then rewrites a different corner of it: the corpus ran the A/B — the `github-192`
|
|
82
|
+
fence, specified by example, **diverged** (`1 → 1 → 3` contradictions, each manufactured by the prior
|
|
83
|
+
fix, reverted at the cap); `github-224` stated the rule first and **converged** (zero). Closed form
|
|
84
|
+
buys **iteration convergence** — insurance a single-condition fold does not need and a
|
|
85
|
+
multi-condition one rarely survives without. Three qualifications, each a way it is misapplied:
|
|
86
|
+
|
|
87
|
+
- **Fire only on ≥2 *interacting* sub-conditions — over-firing is the failure mode.** A
|
|
88
|
+
single-condition fold by example converges fine (the WAW-mutex touch-set-intersection scenarios
|
|
89
|
+
settled in a handful, no stated rule). Mere aggregation is not the trigger; genuine interaction is.
|
|
90
|
+
- **Closed form is not soundness — re-derive against the real data model.** A rule in closed form,
|
|
91
|
+
even one carrying a proof, can be unsound against the data it folds: `R''` shipped a termination
|
|
92
|
+
proof and still deadlocked, its project-scoped exemption violated by graph-global RAW closure, fixed
|
|
93
|
+
to `R'''` only once the assumption was re-derived against the real graph. A proof over an assumed
|
|
94
|
+
model proves nothing about the real one.
|
|
95
|
+
- **Convergence is not coverage — pair the rule with a mutation sweep and a safety dual.** Stating
|
|
96
|
+
the rule kills the divergence, not the gaps. A **mutation sweep** mutates each interacting condition
|
|
97
|
+
and confirms each break lands on a *distinct* scenario — two mutations breaking one scenario mean
|
|
98
|
+
the conditions collapsed into one and the CFG has fewer real branches than it claims. A **safety
|
|
99
|
+
dual** guards the blind spot a convergence check cannot see: a liveness rule (*some grant path
|
|
100
|
+
exists*, *the frontier advances*) passes an **over-permissive** fold green, because over-permission
|
|
101
|
+
adds paths rather than removing them — pair every liveness scenario with the safety scenario
|
|
102
|
+
asserting the case it structurally cannot observe.
|
|
103
|
+
|
|
104
|
+
**The matrix corollary — a per-cell claim is this rule applied.** An outcome stated per cell of a grid
|
|
105
|
+
of interacting conditions is this rule with the closed form written as the **cell function**. Draw
|
|
106
|
+
**every independent cell as its own CFG branch** and exclude the **degenerate** ones (a cell whose
|
|
107
|
+
outcome reconverges with a sibling collapses under the reconvergence rule above, exactly as a
|
|
108
|
+
**universal** "every cell behaves the same" claim is one convergence scenario, not a grid — the
|
|
109
|
+
`github-278` round-4 draft asserted such a universal one row too wide and dropped it). Confirm the
|
|
110
|
+
cells genuinely independent by the mutation sweep. This is not a separate bar; it is the fold rule.
|
|
111
|
+
|
|
73
112
|
## Sections mirror the spec's use-case groups; every scenario binds to a map edge
|
|
74
113
|
|
|
75
114
|
`spec.md` sections the node by **use-case group**, each carrying a drawn **CFG** and an
|
|
@@ -119,10 +158,10 @@ only *where the node genuinely owns the routing decision*, and two different dec
|
|
|
119
158
|
node's own content, so **the node owns it outright**.
|
|
120
159
|
|
|
121
160
|
The two look alike in shape and differ only in who decides, so **step form does not classify them**
|
|
122
|
-
and no mechanical check should try (see
|
|
123
|
-
a deletion that read the second case as the first was blocked at the gate and
|
|
124
|
-
deterministic, fully-owned decision table that selects *what an already-invoked subject
|
|
125
|
-
conduct, not engagement — it wants `@behavior`.
|
|
161
|
+
and no mechanical check should try (see the frozen suite of the SDD project spec's `ssa-lowering`
|
|
162
|
+
node, repo-only, where a deletion that read the second case as the first was blocked at the gate and
|
|
163
|
+
reverted). A deterministic, fully-owned decision table that selects *what an already-invoked subject
|
|
164
|
+
does* is conduct, not engagement — it wants `@behavior`.
|
|
126
165
|
|
|
127
166
|
**`@frozen` is the only file-level tag** — it sits on the `Feature`, not a scenario.
|
|
128
167
|
|
|
@@ -4,11 +4,9 @@ The concrete engine for **touch-set-correction** — a read-only, post-hoc recon
|
|
|
4
4
|
Mission's declared touch-set against what its `git diff` actually changed, composing `git diff`,
|
|
5
5
|
[`resolve-governances`](../resolve-governances/SKILL.md), and `gherkin-cli diff` into the corrected
|
|
6
6
|
touch-set the mission-graph's single writer records at retirement. Built for the Op2 deferral of the
|
|
7
|
-
cyberfleet-batch change request;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
[`touch-set-correction.feature`](../../../../.agents/specs/sdd/touch-set-correction/touch-set-correction.feature)
|
|
11
|
-
for the frozen 21-scenario contract.
|
|
7
|
+
cyberfleet-batch change request; the `touch-set-correction` node of the SDD project spec (in the
|
|
8
|
+
cyberplace repository, not shipped in this package) carries the authoritative behavior description
|
|
9
|
+
and the frozen 21-scenario contract.
|
|
12
10
|
|
|
13
11
|
- **Skill contract:** [`SKILL.md`](./SKILL.md)
|
|
14
12
|
- **Script:** [`scripts/touch-set-correction.mts`](./scripts/touch-set-correction.mts)
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
// touch-set (recovered from `git diff base..head`). It composes three tools — `git diff` (changed
|
|
5
5
|
// files), `resolve-governances` (each file's artifact-type, best-effort), and `gherkin-cli diff`
|
|
6
6
|
// (a touched .feature's changed scenarios) — into one three-way split (confirmed / missed /
|
|
7
|
-
// over-declared) plus the corrected touch-set (= the actual touched set).
|
|
8
|
-
//
|
|
7
|
+
// over-declared) plus the corrected touch-set (= the actual touched set). The touch-set-correction
|
|
8
|
+
// node of the SDD project spec (repo-only) carries the full contract.
|
|
9
9
|
//
|
|
10
10
|
// Architecture — pure derivation kept apart from IO, on purpose (mission-graph.mts's convention):
|
|
11
11
|
// - isFeature / fileToNode / reconcile / assembleCorrection are PURE: they take and return plain
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
// Pure functions are exported for node:test; running the file directly drives the CLI.
|
|
28
28
|
|
|
29
29
|
import { execFileSync } from 'node:child_process'
|
|
30
|
-
import { readFileSync } from 'node:fs'
|
|
30
|
+
import { readFileSync, realpathSync } from 'node:fs'
|
|
31
31
|
import { dirname, join, relative, resolve, sep } from 'node:path'
|
|
32
|
-
import { fileURLToPath } from 'node:url'
|
|
32
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
33
33
|
import { type DiffReader, diffFeatures } from 'gherkin-cli'
|
|
34
34
|
|
|
35
35
|
// ── Types ──
|
|
@@ -415,4 +415,6 @@ export function main(argv: string[]): number {
|
|
|
415
415
|
return 0
|
|
416
416
|
}
|
|
417
417
|
|
|
418
|
-
if (import.meta.
|
|
418
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
|
|
419
|
+
process.exit(main(process.argv.slice(2)))
|
|
420
|
+
}
|
|
@@ -29,6 +29,13 @@ artifact-type already falls through to SDD defaults).
|
|
|
29
29
|
- **Many-to-one is fine.** Two tests can bind the same key; the fold is PASS only if none of them
|
|
30
30
|
fail. A test that maps to an already-covered key and isn't the canonical rename shows up as an
|
|
31
31
|
EXTRA (diagnostic, not a failure) — leave it.
|
|
32
|
+
- **A punctuation-only near-miss still binds.** When no result matches a key exactly, the fold
|
|
33
|
+
retries on a comparison key that folds curly quotes/apostrophes, dashes, and the ellipsis to their
|
|
34
|
+
ASCII forms and collapses whitespace — so a pasted `’` for a `'` does not land the same scenario in
|
|
35
|
+
BOTH the UNBOUND list and the EXTRA list with nothing linking them. The bind is reported as a
|
|
36
|
+
**PROBABLE TITLE MISMATCH** naming both verbatim titles, so the typo still gets fixed. Nothing
|
|
37
|
+
rewrites a title, an **exact** match always wins, case is **not** folded, and an **ambiguous** fold
|
|
38
|
+
(two candidates folding alike) stays UNBOUND rather than binding the wrong one.
|
|
32
39
|
|
|
33
40
|
## Config schema
|
|
34
41
|
|
|
@@ -76,9 +83,9 @@ node "<skill>/scripts/verify-scenarios.mts" \
|
|
|
76
83
|
- `--report <xml>` bypasses `--config` entirely — a single ad-hoc junit source, no command.
|
|
77
84
|
- `--run` executes each source's `command` first; without it, existing reports are read as-is.
|
|
78
85
|
- Default output is a readable per-scenario table + a `N/M BOUND, P pass, F fail, U unbound`
|
|
79
|
-
summary line + any EXTRA keys. `--format json` emits
|
|
80
|
-
`{node,total,bound,pass,fail,unbound,scenarios[],extras[]}`. `--format toon` emits the
|
|
81
|
-
TOON tabular form.
|
|
86
|
+
summary line + any EXTRA keys + any PROBABLE TITLE MISMATCH pairs. `--format json` emits
|
|
87
|
+
`{node,total,bound,pass,fail,unbound,scenarios[],extras[],mismatches[]}`. `--format toon` emits the
|
|
88
|
+
repo's TOON tabular form.
|
|
82
89
|
- Exit code is non-zero when any scenario is UNBOUND or FAIL; zero only at full BOUND+PASS.
|
|
83
90
|
|
|
84
91
|
## Monorepo rooting — `--feature-root` vs. `--root`
|
|
@@ -35,11 +35,20 @@
|
|
|
35
35
|
// feature scenario KEY: UNBOUND if no result; PASS if ≥1 result and none fail; FAIL if any fails.
|
|
36
36
|
// EXTRA = bound result keys matching no feature scenario key (diagnostic, not a failure).
|
|
37
37
|
//
|
|
38
|
+
// Near-miss binding: a key that matches no result EXACTLY gets a second pass on a punctuation- and
|
|
39
|
+
// whitespace-folded COMPARISON key (a pasted curly apostrophe, an em dash, a non-breaking space),
|
|
40
|
+
// so a real binding is not split across the UNBOUND list and the EXTRA list with nothing saying the
|
|
41
|
+
// two are the same scenario. Only unclaimed results are eligible, and only an unambiguous 1:1 fold
|
|
42
|
+
// binds — an ambiguous fold stays UNBOUND rather than manufacturing a false bind. Nothing rewrites
|
|
43
|
+
// a title: the fold is the only place the folded form exists, and every reported field carries the
|
|
44
|
+
// verbatim key, with the matched test title reported alongside it as a probable title mismatch.
|
|
45
|
+
//
|
|
38
46
|
// Pure functions are exported for node:test; running the file directly drives the CLI.
|
|
39
47
|
|
|
40
48
|
import { execSync } from 'node:child_process'
|
|
41
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
49
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs'
|
|
42
50
|
import { isAbsolute, join } from 'node:path'
|
|
51
|
+
import { pathToFileURL } from 'node:url'
|
|
43
52
|
import { parseFeatures } from 'gherkin-cli'
|
|
44
53
|
|
|
45
54
|
// Resolves a path argument against `--root`: relative paths join beneath root (which defaults to the
|
|
@@ -248,6 +257,15 @@ export interface ScenarioReport {
|
|
|
248
257
|
key: string
|
|
249
258
|
state: ScenarioState
|
|
250
259
|
resultCount: number
|
|
260
|
+
/** The result key this scenario bound to when it bound on the folded comparison key, not exactly. */
|
|
261
|
+
matchedKey?: string
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export interface TitleMismatch {
|
|
265
|
+
/** The frozen scenario's verbatim key. */
|
|
266
|
+
key: string
|
|
267
|
+
/** The test's verbatim leaf title it bound to — differing only by punctuation or whitespace. */
|
|
268
|
+
matchedKey: string
|
|
251
269
|
}
|
|
252
270
|
|
|
253
271
|
export interface FoldReport {
|
|
@@ -259,6 +277,43 @@ export interface FoldReport {
|
|
|
259
277
|
unbound: number
|
|
260
278
|
scenarios: ScenarioReport[]
|
|
261
279
|
extras: string[]
|
|
280
|
+
/** Bindings made on the folded comparison key — a probable one-character typo, reported so it gets fixed. */
|
|
281
|
+
mismatches: TitleMismatch[]
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Punctuation and whitespace variants that carry no meaning of their own: the smart-quote and dash
|
|
285
|
+
// substitutions an editor, a copy-paste, or a Markdown renderer makes to a title on its way into a
|
|
286
|
+
// test file. Case is deliberately NOT folded — a differing case is not a punctuation variant, and
|
|
287
|
+
// folding it would bind two titles the author wrote differently on purpose.
|
|
288
|
+
const PUNCTUATION_FOLDS: [RegExp, string][] = [
|
|
289
|
+
[/[‘’‚‛′´]/g, "'"], // curly/prime apostrophes -> straight
|
|
290
|
+
[/[“”„‟″]/g, '"'], // curly double quotes -> straight
|
|
291
|
+
[/[‐‑‒–—―−]/g, '-'], // hyphens/dashes/minus -> hyphen
|
|
292
|
+
[/…/g, '...'], // ellipsis -> three dots
|
|
293
|
+
]
|
|
294
|
+
// A no-break/thin/figure space needs no entry of its own — the trailing `\s+` collapse below
|
|
295
|
+
// already covers every Unicode space separator.
|
|
296
|
+
|
|
297
|
+
// The MATCH-ONLY form of a key: Unicode-composed, punctuation-folded, whitespace-collapsed. Never
|
|
298
|
+
// written back anywhere — reports carry the verbatim key.
|
|
299
|
+
export function comparisonKey(key: string): string {
|
|
300
|
+
let out = key.normalize('NFC')
|
|
301
|
+
for (const [re, rep] of PUNCTUATION_FOLDS) out = out.replace(re, rep)
|
|
302
|
+
return out.replace(/\s+/g, ' ').trim()
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Indexes values by their comparison key, dropping every comparison key claimed by more than one
|
|
306
|
+
// value — an ambiguous fold must not bind, so only the 1:1 entries survive.
|
|
307
|
+
function unambiguousByComparisonKey(keys: string[]): Map<string, string> {
|
|
308
|
+
const index = new Map<string, string>()
|
|
309
|
+
const ambiguous = new Set<string>()
|
|
310
|
+
for (const k of keys) {
|
|
311
|
+
const c = comparisonKey(k)
|
|
312
|
+
if (index.has(c)) ambiguous.add(c)
|
|
313
|
+
else index.set(c, k)
|
|
314
|
+
}
|
|
315
|
+
for (const c of ambiguous) index.delete(c)
|
|
316
|
+
return index
|
|
262
317
|
}
|
|
263
318
|
|
|
264
319
|
export function foldResults(scenarioKeys: ScenarioKey[], results: BoundResult[], node: string): FoldReport {
|
|
@@ -270,21 +325,39 @@ export function foldResults(scenarioKeys: ScenarioKey[], results: BoundResult[],
|
|
|
270
325
|
else byKey.set(r.key, [r])
|
|
271
326
|
}
|
|
272
327
|
|
|
328
|
+
const featureKeys = new Set(scenarioKeys.map((s) => s.key))
|
|
329
|
+
// Only a result no scenario claims exactly is eligible for a near-miss bind, and only a scenario
|
|
330
|
+
// that found no exact result looks for one — an exact binding always wins.
|
|
331
|
+
const unclaimed = [...byKey.keys()].filter((k) => !featureKeys.has(k))
|
|
332
|
+
const unmatchedScenarioKeys = scenarioKeys.map((s) => s.key).filter((k) => !byKey.has(k))
|
|
333
|
+
const unclaimedByComparison = unambiguousByComparisonKey(unclaimed)
|
|
334
|
+
const unmatchedByComparison = unambiguousByComparisonKey(unmatchedScenarioKeys)
|
|
335
|
+
|
|
336
|
+
const nearMisses = new Map<string, string>() // scenario key -> result key
|
|
337
|
+
for (const [comparison, scenarioKey] of unmatchedByComparison) {
|
|
338
|
+
const resultKey = unclaimedByComparison.get(comparison)
|
|
339
|
+
if (resultKey !== undefined) nearMisses.set(scenarioKey, resultKey)
|
|
340
|
+
}
|
|
341
|
+
|
|
273
342
|
const scenarios: ScenarioReport[] = scenarioKeys.map(({ name, key }) => {
|
|
274
|
-
const
|
|
343
|
+
const matchedKey = byKey.has(key) ? undefined : nearMisses.get(key)
|
|
344
|
+
const group = byKey.get(matchedKey ?? key) ?? []
|
|
275
345
|
const state: ScenarioState =
|
|
276
346
|
group.length === 0 ? 'unbound' : group.some((r) => r.outcome === 'fail') ? 'fail' : 'pass'
|
|
277
|
-
return { name, key, state, resultCount: group.length }
|
|
347
|
+
return { name, key, state, resultCount: group.length, ...(matchedKey === undefined ? {} : { matchedKey }) }
|
|
278
348
|
})
|
|
279
349
|
|
|
280
|
-
const
|
|
281
|
-
const extras =
|
|
350
|
+
const nearMissKeys = new Set(nearMisses.values())
|
|
351
|
+
const extras = unclaimed.filter((k) => !nearMissKeys.has(k)).sort()
|
|
352
|
+
const mismatches: TitleMismatch[] = scenarios
|
|
353
|
+
.filter((s): s is ScenarioReport & { matchedKey: string } => s.matchedKey !== undefined)
|
|
354
|
+
.map((s) => ({ key: s.key, matchedKey: s.matchedKey }))
|
|
282
355
|
|
|
283
356
|
const pass = scenarios.filter((s) => s.state === 'pass').length
|
|
284
357
|
const fail = scenarios.filter((s) => s.state === 'fail').length
|
|
285
358
|
const unbound = scenarios.filter((s) => s.state === 'unbound').length
|
|
286
359
|
|
|
287
|
-
return { node, total: scenarios.length, bound: pass + fail, pass, fail, unbound, scenarios, extras }
|
|
360
|
+
return { node, total: scenarios.length, bound: pass + fail, pass, fail, unbound, scenarios, extras, mismatches }
|
|
288
361
|
}
|
|
289
362
|
|
|
290
363
|
// ── Output ──
|
|
@@ -300,6 +373,10 @@ export function formatToon(report: FoldReport): string {
|
|
|
300
373
|
(s) => ` ${[toonField(s.name), toonField(s.key), s.state, String(s.resultCount)].join(',')}`,
|
|
301
374
|
)
|
|
302
375
|
const extras = [`extras[${report.extras.length}]{key}:`, ...report.extras.map((e) => ` ${toonField(e)}`)]
|
|
376
|
+
const mismatches = [
|
|
377
|
+
`mismatches[${report.mismatches.length}]{key,matchedKey}:`,
|
|
378
|
+
...report.mismatches.map((m) => ` ${[toonField(m.key), toonField(m.matchedKey)].join(',')}`),
|
|
379
|
+
]
|
|
303
380
|
const summary = `summary{node,total,bound,pass,fail,unbound}:\n ${[
|
|
304
381
|
toonField(report.node),
|
|
305
382
|
report.total,
|
|
@@ -308,7 +385,7 @@ export function formatToon(report: FoldReport): string {
|
|
|
308
385
|
report.fail,
|
|
309
386
|
report.unbound,
|
|
310
387
|
].join(',')}`
|
|
311
|
-
return [header, ...rows, ...extras, summary].join('\n')
|
|
388
|
+
return [header, ...rows, ...extras, ...mismatches, summary].join('\n')
|
|
312
389
|
}
|
|
313
390
|
|
|
314
391
|
export function formatText(report: FoldReport): string {
|
|
@@ -322,6 +399,11 @@ export function formatText(report: FoldReport): string {
|
|
|
322
399
|
lines.push('')
|
|
323
400
|
lines.push(`EXTRA (bound results matching no scenario): ${report.extras.join(', ')}`)
|
|
324
401
|
}
|
|
402
|
+
if (report.mismatches.length > 0) {
|
|
403
|
+
lines.push('')
|
|
404
|
+
lines.push('PROBABLE TITLE MISMATCH (bound on punctuation/whitespace only — fix the test title):')
|
|
405
|
+
for (const m of report.mismatches) lines.push(` scenario "${m.key}" <- test "${m.matchedKey}"`)
|
|
406
|
+
}
|
|
325
407
|
return lines.join('\n')
|
|
326
408
|
}
|
|
327
409
|
|
|
@@ -383,4 +465,6 @@ export function main(argv: string[]): number {
|
|
|
383
465
|
return exitCode(report)
|
|
384
466
|
}
|
|
385
467
|
|
|
386
|
-
if (import.meta.
|
|
468
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
|
|
469
|
+
process.exit(main(process.argv.slice(2)))
|
|
470
|
+
}
|