cyber-sdd 0.0.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/.claude-plugin/plugin.json +17 -0
- package/.codex-plugin/plugin.json +17 -0
- package/.plugin/plugin.json +17 -0
- package/README.md +159 -0
- package/agents/sdd-automaton.md +97 -0
- package/agents/sdd-impl-judge.md +214 -0
- package/agents/sdd-scanner.md +120 -0
- package/agents/sdd-spec-judge.md +224 -0
- package/agents/sdd-warden.md +101 -0
- package/package.json +24 -0
- package/skills/align-spec/README.md +20 -0
- package/skills/align-spec/SKILL.md +111 -0
- package/skills/align-spec/scripts/align-spec.mts +187 -0
- package/skills/architect-impl-governance/README.md +46 -0
- package/skills/architect-impl-governance/SKILL.md +45 -0
- package/skills/architect-spec-governance/README.md +48 -0
- package/skills/architect-spec-governance/SKILL.md +59 -0
- package/skills/blast-estimate/README.md +47 -0
- package/skills/blast-estimate/SKILL.md +133 -0
- package/skills/blast-estimate/scripts/blast-estimate.mts +583 -0
- package/skills/builder-impl-governance/README.md +47 -0
- package/skills/builder-impl-governance/SKILL.md +47 -0
- package/skills/builder-spec-governance/README.md +49 -0
- package/skills/builder-spec-governance/SKILL.md +36 -0
- package/skills/check-partition-quality/README.md +22 -0
- package/skills/check-partition-quality/SKILL.md +51 -0
- package/skills/check-partition-quality/scripts/check-partition-quality.mts +336 -0
- package/skills/check-plan-safety/README.md +17 -0
- package/skills/check-plan-safety/SKILL.md +60 -0
- package/skills/check-plan-safety/scripts/check-plan-safety.mts +145 -0
- package/skills/check-project-specs/README.md +19 -0
- package/skills/check-project-specs/SKILL.md +69 -0
- package/skills/check-project-specs/scripts/check-project-specs.mts +217 -0
- package/skills/check-scenario-overlap/README.md +19 -0
- package/skills/check-scenario-overlap/SKILL.md +74 -0
- package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mts +249 -0
- package/skills/check-spec-structure/README.md +17 -0
- package/skills/check-spec-structure/SKILL.md +66 -0
- package/skills/check-spec-structure/scripts/check-spec-structure.mts +346 -0
- package/skills/collision-ladder/README.md +18 -0
- package/skills/collision-ladder/SKILL.md +83 -0
- package/skills/collision-ladder/scripts/collision-ladder.mts +657 -0
- package/skills/combat-log-governance/README.md +13 -0
- package/skills/combat-log-governance/SKILL.md +257 -0
- package/skills/concept-index/README.md +13 -0
- package/skills/concept-index/SKILL.md +38 -0
- package/skills/concept-index/scripts/concept-index.mts +245 -0
- package/skills/discover-plans/README.md +16 -0
- package/skills/discover-plans/SKILL.md +74 -0
- package/skills/discover-plans/scripts/discover-plans.mts +212 -0
- package/skills/discover-specs/README.md +15 -0
- package/skills/discover-specs/SKILL.md +76 -0
- package/skills/discover-specs/scripts/discover-specs.mts +396 -0
- package/skills/doctrine-loop/README.md +15 -0
- package/skills/doctrine-loop/SKILL.md +97 -0
- package/skills/formation-loop/README.md +17 -0
- package/skills/formation-loop/SKILL.md +140 -0
- package/skills/gate-validation-governance/README.md +12 -0
- package/skills/gate-validation-governance/SKILL.md +87 -0
- package/skills/impl-producer-governance/README.md +48 -0
- package/skills/impl-producer-governance/SKILL.md +85 -0
- package/skills/init/README.md +27 -0
- package/skills/init/SKILL.md +68 -0
- package/skills/init/scripts/wire-statusline.mts +276 -0
- package/skills/lifecycle-governance/README.md +11 -0
- package/skills/lifecycle-governance/SKILL.md +168 -0
- package/skills/manage/README.md +9 -0
- package/skills/manage/SKILL.md +62 -0
- package/skills/manage-ignore/README.md +19 -0
- package/skills/manage-ignore/SKILL.md +52 -0
- package/skills/manage-ignore/scripts/manage-ignore.mts +294 -0
- package/skills/manage-scenario-bridge/README.md +20 -0
- package/skills/manage-scenario-bridge/SKILL.md +60 -0
- package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mts +156 -0
- package/skills/manage-spec-anchors/README.md +18 -0
- package/skills/manage-spec-anchors/SKILL.md +56 -0
- package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mts +328 -0
- package/skills/mission-graph/README.md +15 -0
- package/skills/mission-graph/SKILL.md +67 -0
- package/skills/mission-graph/scripts/mission-graph.mts +844 -0
- package/skills/oracle-spec-governance/README.md +45 -0
- package/skills/oracle-spec-governance/SKILL.md +45 -0
- package/skills/ownership-governance/README.md +65 -0
- package/skills/ownership-governance/SKILL.md +104 -0
- package/skills/pause-mission/README.md +18 -0
- package/skills/pause-mission/SKILL.md +112 -0
- package/skills/place-node/README.md +12 -0
- package/skills/place-node/SKILL.md +47 -0
- package/skills/place-node/scripts/place-node.mts +157 -0
- package/skills/plan-retirement/README.md +32 -0
- package/skills/plan-retirement/SKILL.md +90 -0
- package/skills/plan-retirement/scripts/retire-plans.mts +196 -0
- package/skills/plugin-contract-governance/README.md +12 -0
- package/skills/plugin-contract-governance/SKILL.md +112 -0
- package/skills/remediation-governance/README.md +46 -0
- package/skills/remediation-governance/SKILL.md +78 -0
- package/skills/resolve-governances/README.md +18 -0
- package/skills/resolve-governances/SKILL.md +50 -0
- package/skills/resolve-governances/scripts/resolve-governances.mts +515 -0
- package/skills/resolve-tracking/SKILL.md +64 -0
- package/skills/resolve-tracking/scripts/resolve-tracking.mts +213 -0
- package/skills/resume-mission/README.md +12 -0
- package/skills/resume-mission/SKILL.md +53 -0
- package/skills/scaffold-project-spec/README.md +7 -0
- package/skills/scaffold-project-spec/SKILL.md +192 -0
- package/skills/sdd/README.md +7 -0
- package/skills/sdd/SKILL.md +92 -0
- package/skills/solution-producer-governance/README.md +9 -0
- package/skills/solution-producer-governance/SKILL.md +44 -0
- package/skills/spec-format-governance/README.md +73 -0
- package/skills/spec-format-governance/SKILL.md +114 -0
- package/skills/spec-gate/README.md +26 -0
- package/skills/spec-gate/SKILL.md +201 -0
- package/skills/spec-gate/scripts/check-spec-state.mts +601 -0
- package/skills/spec-gate/scripts/check-suite.mts +501 -0
- package/skills/spec-gate/scripts/classify-edit-class.mts +411 -0
- package/skills/spec-producer-governance/README.md +7 -0
- package/skills/spec-producer-governance/SKILL.md +86 -0
- package/skills/spec-structure-governance/README.md +40 -0
- package/skills/spec-structure-governance/SKILL.md +169 -0
- package/skills/ssa-lowering/README.md +26 -0
- package/skills/ssa-lowering/SKILL.md +181 -0
- package/skills/start-mission/README.md +7 -0
- package/skills/start-mission/SKILL.md +115 -0
- package/skills/suite-format-governance/README.md +75 -0
- package/skills/suite-format-governance/SKILL.md +299 -0
- package/skills/suite-format-governance/references/rubric.md +313 -0
- package/skills/touch-set-correction/README.md +16 -0
- package/skills/touch-set-correction/SKILL.md +67 -0
- package/skills/touch-set-correction/scripts/touch-set-correction.mts +418 -0
- package/skills/verify-scenarios/README.md +17 -0
- package/skills/verify-scenarios/SKILL.md +109 -0
- package/skills/verify-scenarios/scripts/verify-scenarios.mts +386 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// verify-scenarios — the Gherkin-scenario -> test-report bridge verifier. For a frozen `.feature`
|
|
3
|
+
// and a spec-node path, reports per-scenario PASS / FAIL / UNBOUND by reading the reports of one or
|
|
4
|
+
// more configured test-result SOURCES, so an impl-judge reasons by hand only over the UNBOUND set
|
|
5
|
+
// instead of every scenario.
|
|
6
|
+
//
|
|
7
|
+
// Binding convention: KEY = a scenario's `@id:<slug>` tag if present, else its verbatim name. A test
|
|
8
|
+
// declares its node via a `describe('spec:<node>', …)` wrapper (or equivalent) — the bridge reads
|
|
9
|
+
// that segment out of the test-report's `" > "`-joined test name. A test's leaf title is either the
|
|
10
|
+
// exact scenario name or `@id:<slug>`; a Scenario Outline is ONE key (its outline name), not one per
|
|
11
|
+
// Examples row.
|
|
12
|
+
//
|
|
13
|
+
// Operations:
|
|
14
|
+
// --feature <path> frozen .feature to verify (required)
|
|
15
|
+
// --node <path> the spec node to bind against, e.g. cyberlegion/identity (required)
|
|
16
|
+
// --config <toml> .agents/sdd/scenario-bridge.toml (default, resolved under --root)
|
|
17
|
+
// --root <dir> bridge/report root: --config's default, --report, and every source's
|
|
18
|
+
// reportPath resolve here (default cwd)
|
|
19
|
+
// --feature-root <dir> where --feature resolves; defaults to --root when omitted (monorepo:
|
|
20
|
+
// specs at repo root, bridge/report under the project's own root)
|
|
21
|
+
// --report <xml> bypass --config: a single ad-hoc junit report (resolved under --root)
|
|
22
|
+
// --run execute each source's `command` before reading its report
|
|
23
|
+
// --format toon|json machine output (default: a readable text map)
|
|
24
|
+
//
|
|
25
|
+
// Config `.agents/sdd/scenario-bridge.toml` is an array-of-tables of sources:
|
|
26
|
+
// [[source]]
|
|
27
|
+
// adapter = "junit"
|
|
28
|
+
// command = "pnpm build && vitest run src --reporter=junit --outputFile=.agents/.scenario-report.xml"
|
|
29
|
+
// reportPath = ".agents/.scenario-report.xml"
|
|
30
|
+
//
|
|
31
|
+
// Adapter interface: (source, root, run) => Array<{node, key, outcome: 'pass'|'fail'|'skip'}>. Only
|
|
32
|
+
// `junit` is implemented; `tap`/`aced` slot into the same `switch(adapter)` seam later.
|
|
33
|
+
//
|
|
34
|
+
// Fold: union every source's results, keep only those whose `node` equals --node, group by KEY. Per
|
|
35
|
+
// feature scenario KEY: UNBOUND if no result; PASS if ≥1 result and none fail; FAIL if any fails.
|
|
36
|
+
// EXTRA = bound result keys matching no feature scenario key (diagnostic, not a failure).
|
|
37
|
+
//
|
|
38
|
+
// Pure functions are exported for node:test; running the file directly drives the CLI.
|
|
39
|
+
|
|
40
|
+
import { execSync } from 'node:child_process'
|
|
41
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
42
|
+
import { isAbsolute, join } from 'node:path'
|
|
43
|
+
import { parseFeatures } from 'gherkin-cli'
|
|
44
|
+
|
|
45
|
+
// Resolves a path argument against `--root`: relative paths join beneath root (which defaults to the
|
|
46
|
+
// current directory); an absolute path is used verbatim, never double-prefixed under root.
|
|
47
|
+
export function underRoot(root: string, p: string): string {
|
|
48
|
+
return isAbsolute(p) ? p : join(root, p)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── Config (a minimal TOML: `[[source]]` array-of-tables) ──
|
|
52
|
+
|
|
53
|
+
export interface SourceConfig {
|
|
54
|
+
adapter: string
|
|
55
|
+
command?: string
|
|
56
|
+
reportPath: string
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Parses `[[source]]` blocks; each block's `key = "value"` lines become fields on one source. Blocks
|
|
60
|
+
// are delimited by the next `[[source]]` or `[...]` header (or EOF). Malformed/incomplete blocks
|
|
61
|
+
// (no reportPath) are dropped rather than throwing — mirrors spec-anchors' lenient-read posture.
|
|
62
|
+
export function parseSourcesToml(text: string): SourceConfig[] {
|
|
63
|
+
const out: SourceConfig[] = []
|
|
64
|
+
const blocks = text.split(/(?=^\s*\[\[source\]\]\s*$)/m).filter((b) => /^\s*\[\[source\]\]\s*$/m.test(b))
|
|
65
|
+
for (const block of blocks) {
|
|
66
|
+
const body = block.replace(/^\s*\[\[source\]\]\s*$/m, '')
|
|
67
|
+
const fields: Record<string, string> = {}
|
|
68
|
+
for (const m of body.matchAll(/^\s*([A-Za-z_][\w-]*)\s*=\s*(?:"([^"]*)"|'([^']*)')\s*$/gm)) {
|
|
69
|
+
fields[m[1]] = m[2] ?? m[3] ?? ''
|
|
70
|
+
}
|
|
71
|
+
if (!fields.adapter || !fields.reportPath) continue
|
|
72
|
+
out.push({ adapter: fields.adapter, command: fields.command, reportPath: fields.reportPath })
|
|
73
|
+
}
|
|
74
|
+
return out
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function readSourcesConfig(configPath: string): SourceConfig[] {
|
|
78
|
+
if (!existsSync(configPath)) return []
|
|
79
|
+
try {
|
|
80
|
+
return parseSourcesToml(readFileSync(configPath, 'utf8'))
|
|
81
|
+
} catch {
|
|
82
|
+
return []
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── Scenario keys from the frozen .feature (via gherkin-cli) ──
|
|
87
|
+
|
|
88
|
+
export interface GherkinScenario {
|
|
89
|
+
name: string
|
|
90
|
+
keyword: string
|
|
91
|
+
tags: string[]
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Narrower than gherkin-cli's real `ParseResult` — this function reads only `files[].scenarios`,
|
|
95
|
+
// so its signature stays structural rather than demanding fields (summary, per-file counts) it
|
|
96
|
+
// never touches; a real `ParseResult` value satisfies this shape and passes straight through.
|
|
97
|
+
interface GherkinParseOutput {
|
|
98
|
+
files: { scenarios: GherkinScenario[] }[]
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface ScenarioKey {
|
|
102
|
+
name: string
|
|
103
|
+
key: string
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const ID_TAG_RE = /^@id:(.+)$/
|
|
107
|
+
|
|
108
|
+
// A scenario's KEY is its `@id:<slug>` tag if one is present, else its verbatim name. A Scenario
|
|
109
|
+
// Outline is one key (its outline name) — gherkin-cli already emits one entry per outline, not per
|
|
110
|
+
// Examples row, so no special-casing is needed here.
|
|
111
|
+
export function scenarioKeysFromParse(parsed: GherkinParseOutput): ScenarioKey[] {
|
|
112
|
+
const out: ScenarioKey[] = []
|
|
113
|
+
for (const file of parsed.files) {
|
|
114
|
+
for (const s of file.scenarios) {
|
|
115
|
+
const idTag = s.tags.map((t) => ID_TAG_RE.exec(t)).find((m): m is RegExpExecArray => m !== null)
|
|
116
|
+
out.push({ name: s.name, key: idTag ? idTag[1] : s.name })
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return out
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// `featureRoot` resolves `featurePath` (defaults to `root` at the call site when the CLI omits
|
|
123
|
+
// `--feature-root` — see main()).
|
|
124
|
+
export function getScenarioKeys(root: string, featurePath: string, featureRoot: string = root): ScenarioKey[] {
|
|
125
|
+
const abs = underRoot(featureRoot, featurePath)
|
|
126
|
+
return scenarioKeysFromParse(parseFeatures([abs]))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ── JUnit parsing (hand-rolled, no xml dep) ──
|
|
130
|
+
|
|
131
|
+
export type Outcome = 'pass' | 'fail' | 'skip'
|
|
132
|
+
|
|
133
|
+
export interface JUnitTestcase {
|
|
134
|
+
classname: string
|
|
135
|
+
name: string
|
|
136
|
+
outcome: Outcome
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const XML_ENTITIES: [RegExp, string][] = [
|
|
140
|
+
[/</g, '<'],
|
|
141
|
+
[/>/g, '>'],
|
|
142
|
+
[/"/g, '"'],
|
|
143
|
+
[/'/g, "'"],
|
|
144
|
+
[/&/g, '&'], // last, so a literal "&lt;" does not get double-unescaped
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
export function unescapeXml(s: string): string {
|
|
148
|
+
let out = s
|
|
149
|
+
for (const [re, rep] of XML_ENTITIES) out = out.replace(re, rep)
|
|
150
|
+
return out
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function attr(attrs: string, name: string): string {
|
|
154
|
+
const m = new RegExp(`\\b${name}\\s*=\\s*"([^"]*)"`).exec(attrs)
|
|
155
|
+
return m ? unescapeXml(m[1]) : ''
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Finds every `<testcase …>…</testcase>` (or self-closed `<testcase … />`), extracting `classname`
|
|
159
|
+
// and `name` BY ATTRIBUTE NAME (not position), unescaping both. Outcome: a child `<failure` -> fail;
|
|
160
|
+
// a child `<skipped` -> skip; else pass.
|
|
161
|
+
//
|
|
162
|
+
// The opening-tag regex walks attribute-by-attribute (quoted strings or a non-quote/non-`>` run) so
|
|
163
|
+
// an attribute VALUE containing a literal `>` — e.g. a scenario name built from `" > "`-joined
|
|
164
|
+
// segments — never truncates the match, which a naive `[^>]*` class would do.
|
|
165
|
+
const OPEN_TAG_RE = /<testcase\b((?:"[^"]*"|'[^']*'|[^"'>/])*)(\/)?>/g
|
|
166
|
+
|
|
167
|
+
export function parseJUnitTestcases(xml: string): JUnitTestcase[] {
|
|
168
|
+
const out: JUnitTestcase[] = []
|
|
169
|
+
for (const m of xml.matchAll(OPEN_TAG_RE)) {
|
|
170
|
+
const attrs = m[1]
|
|
171
|
+
const selfClosed = m[2] === '/'
|
|
172
|
+
let body = ''
|
|
173
|
+
if (!selfClosed) {
|
|
174
|
+
const closeIdx = xml.indexOf('</testcase>', m.index + m[0].length)
|
|
175
|
+
if (closeIdx !== -1) body = xml.slice(m.index + m[0].length, closeIdx)
|
|
176
|
+
}
|
|
177
|
+
const outcome: Outcome = /<failure\b/.test(body) ? 'fail' : /<skipped\b/.test(body) ? 'skip' : 'pass'
|
|
178
|
+
out.push({ classname: attr(attrs, 'classname'), name: attr(attrs, 'name'), outcome })
|
|
179
|
+
}
|
|
180
|
+
return out
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── junit adapter: testcase -> {node, key, outcome} ──
|
|
184
|
+
|
|
185
|
+
export interface BoundResult {
|
|
186
|
+
node: string
|
|
187
|
+
key: string
|
|
188
|
+
outcome: Outcome
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const NODE_SEGMENT_RE = /^spec:(.+)$/
|
|
192
|
+
|
|
193
|
+
// Splits an (already-unescaped) testcase `name` on `" > "`. NODE = the capture of whichever segment
|
|
194
|
+
// matches `/^spec:(.+)$/` (found at any depth); testcases with no such segment are not bound to a
|
|
195
|
+
// spec node and are dropped. LEAF = the last segment; KEY = its `@id:<slug>` capture if it matches,
|
|
196
|
+
// else the leaf verbatim.
|
|
197
|
+
export function junitTestcaseToResult(tc: JUnitTestcase): BoundResult | undefined {
|
|
198
|
+
const segments = tc.name.split(' > ')
|
|
199
|
+
let node: string | undefined
|
|
200
|
+
for (const seg of segments) {
|
|
201
|
+
const m = NODE_SEGMENT_RE.exec(seg)
|
|
202
|
+
if (m) {
|
|
203
|
+
node = m[1]
|
|
204
|
+
break
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (node === undefined) return undefined
|
|
208
|
+
const leaf = segments[segments.length - 1]
|
|
209
|
+
const idMatch = ID_TAG_RE.exec(leaf)
|
|
210
|
+
const key = idMatch ? idMatch[1] : leaf
|
|
211
|
+
return { node, key, outcome: tc.outcome }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function junitResultsFromXml(xml: string): BoundResult[] {
|
|
215
|
+
const out: BoundResult[] = []
|
|
216
|
+
for (const tc of parseJUnitTestcases(xml)) {
|
|
217
|
+
const r = junitTestcaseToResult(tc)
|
|
218
|
+
if (r) out.push(r)
|
|
219
|
+
}
|
|
220
|
+
return out
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function runJunitSource(source: SourceConfig, root: string, run: boolean): BoundResult[] {
|
|
224
|
+
if (run && source.command) {
|
|
225
|
+
execSync(source.command, { cwd: root, stdio: ['ignore', 'pipe', 'inherit'] })
|
|
226
|
+
}
|
|
227
|
+
const reportPath = underRoot(root, source.reportPath)
|
|
228
|
+
if (!existsSync(reportPath)) return []
|
|
229
|
+
return junitResultsFromXml(readFileSync(reportPath, 'utf8'))
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Adapter dispatch seam — `tap` / `aced` / others implement the same signature and slot in here.
|
|
233
|
+
export function runSource(source: SourceConfig, root: string, run: boolean): BoundResult[] {
|
|
234
|
+
switch (source.adapter) {
|
|
235
|
+
case 'junit':
|
|
236
|
+
return runJunitSource(source, root, run)
|
|
237
|
+
default:
|
|
238
|
+
return []
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ── Fold: union all sources, group by key, compare against the feature's scenario set ──
|
|
243
|
+
|
|
244
|
+
export type ScenarioState = 'pass' | 'fail' | 'unbound'
|
|
245
|
+
|
|
246
|
+
export interface ScenarioReport {
|
|
247
|
+
name: string
|
|
248
|
+
key: string
|
|
249
|
+
state: ScenarioState
|
|
250
|
+
resultCount: number
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export interface FoldReport {
|
|
254
|
+
node: string
|
|
255
|
+
total: number
|
|
256
|
+
bound: number
|
|
257
|
+
pass: number
|
|
258
|
+
fail: number
|
|
259
|
+
unbound: number
|
|
260
|
+
scenarios: ScenarioReport[]
|
|
261
|
+
extras: string[]
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function foldResults(scenarioKeys: ScenarioKey[], results: BoundResult[], node: string): FoldReport {
|
|
265
|
+
const nodeResults = results.filter((r) => r.node === node)
|
|
266
|
+
const byKey = new Map<string, BoundResult[]>()
|
|
267
|
+
for (const r of nodeResults) {
|
|
268
|
+
const list = byKey.get(r.key)
|
|
269
|
+
if (list) list.push(r)
|
|
270
|
+
else byKey.set(r.key, [r])
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const scenarios: ScenarioReport[] = scenarioKeys.map(({ name, key }) => {
|
|
274
|
+
const group = byKey.get(key) ?? []
|
|
275
|
+
const state: ScenarioState =
|
|
276
|
+
group.length === 0 ? 'unbound' : group.some((r) => r.outcome === 'fail') ? 'fail' : 'pass'
|
|
277
|
+
return { name, key, state, resultCount: group.length }
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
const featureKeys = new Set(scenarioKeys.map((s) => s.key))
|
|
281
|
+
const extras = [...byKey.keys()].filter((k) => !featureKeys.has(k)).sort()
|
|
282
|
+
|
|
283
|
+
const pass = scenarios.filter((s) => s.state === 'pass').length
|
|
284
|
+
const fail = scenarios.filter((s) => s.state === 'fail').length
|
|
285
|
+
const unbound = scenarios.filter((s) => s.state === 'unbound').length
|
|
286
|
+
|
|
287
|
+
return { node, total: scenarios.length, bound: pass + fail, pass, fail, unbound, scenarios, extras }
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ── Output ──
|
|
291
|
+
|
|
292
|
+
function toonField(v: string): string {
|
|
293
|
+
if (v === '' || /[",]/.test(v) || v !== v.trim()) return `"${v.replace(/"/g, '""')}"`
|
|
294
|
+
return v
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function formatToon(report: FoldReport): string {
|
|
298
|
+
const header = `scenarios[${report.scenarios.length}]{name,key,state,resultCount}:`
|
|
299
|
+
const rows = report.scenarios.map(
|
|
300
|
+
(s) => ` ${[toonField(s.name), toonField(s.key), s.state, String(s.resultCount)].join(',')}`,
|
|
301
|
+
)
|
|
302
|
+
const extras = [`extras[${report.extras.length}]{key}:`, ...report.extras.map((e) => ` ${toonField(e)}`)]
|
|
303
|
+
const summary = `summary{node,total,bound,pass,fail,unbound}:\n ${[
|
|
304
|
+
toonField(report.node),
|
|
305
|
+
report.total,
|
|
306
|
+
report.bound,
|
|
307
|
+
report.pass,
|
|
308
|
+
report.fail,
|
|
309
|
+
report.unbound,
|
|
310
|
+
].join(',')}`
|
|
311
|
+
return [header, ...rows, ...extras, summary].join('\n')
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function formatText(report: FoldReport): string {
|
|
315
|
+
const lines: string[] = []
|
|
316
|
+
for (const s of report.scenarios) lines.push(`${s.state.toUpperCase().padEnd(8)} ${s.name}`)
|
|
317
|
+
lines.push('')
|
|
318
|
+
lines.push(
|
|
319
|
+
`${report.bound}/${report.total} BOUND, ${report.pass} pass, ${report.fail} fail, ${report.unbound} unbound`,
|
|
320
|
+
)
|
|
321
|
+
if (report.extras.length > 0) {
|
|
322
|
+
lines.push('')
|
|
323
|
+
lines.push(`EXTRA (bound results matching no scenario): ${report.extras.join(', ')}`)
|
|
324
|
+
}
|
|
325
|
+
return lines.join('\n')
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ── CLI ──
|
|
329
|
+
|
|
330
|
+
function flag(argv: string[], name: string): string | undefined {
|
|
331
|
+
const i = argv.indexOf(name)
|
|
332
|
+
return i === -1 ? undefined : argv[i + 1]
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Resolves the source set for a run: `--report` bypasses the configured sources entirely with a
|
|
336
|
+
// single ad-hoc junit source; otherwise the sources come from the (resolved) config file.
|
|
337
|
+
export function resolveSources(argv: string[], root: string): SourceConfig[] {
|
|
338
|
+
const reportFlag = flag(argv, '--report')
|
|
339
|
+
if (reportFlag) {
|
|
340
|
+
return [{ adapter: 'junit', reportPath: reportFlag }]
|
|
341
|
+
}
|
|
342
|
+
const configPath = underRoot(root, flag(argv, '--config') ?? '.agents/sdd/scenario-bridge.toml')
|
|
343
|
+
return readSourcesConfig(configPath)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Renders a fold report in the requested format, defaulting to text.
|
|
347
|
+
export function renderReport(report: FoldReport, format: string): string {
|
|
348
|
+
if (format === 'json') return JSON.stringify(report, null, 2)
|
|
349
|
+
if (format === 'toon') return formatToon(report)
|
|
350
|
+
return formatText(report)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Non-zero exactly when any scenario is UNBOUND or FAIL; zero only when every scenario is bound
|
|
354
|
+
// and passing.
|
|
355
|
+
export function exitCode(report: FoldReport): number {
|
|
356
|
+
return report.unbound > 0 || report.fail > 0 ? 1 : 0
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export function main(argv: string[]): number {
|
|
360
|
+
const root = flag(argv, '--root') ?? '.'
|
|
361
|
+
const featureRoot = flag(argv, '--feature-root') ?? root
|
|
362
|
+
const feature = flag(argv, '--feature')
|
|
363
|
+
const node = flag(argv, '--node')
|
|
364
|
+
const format = flag(argv, '--format') ?? 'text'
|
|
365
|
+
const run = argv.includes('--run')
|
|
366
|
+
const w = (s: string) => process.stdout.write(`${s}\n`)
|
|
367
|
+
|
|
368
|
+
if (!feature || !node) {
|
|
369
|
+
w(
|
|
370
|
+
'usage: verify-scenarios --feature <path> --node <path> [--config <toml>] [--root <dir>] [--feature-root <dir>] [--report <xml>] [--run] [--format toon|json]',
|
|
371
|
+
)
|
|
372
|
+
return 1
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const sources = resolveSources(argv, root)
|
|
376
|
+
|
|
377
|
+
const results = sources.flatMap((s) => runSource(s, root, run))
|
|
378
|
+
const scenarioKeys = getScenarioKeys(root, feature, featureRoot)
|
|
379
|
+
const report = foldResults(scenarioKeys, results, node)
|
|
380
|
+
|
|
381
|
+
w(renderReport(report, format))
|
|
382
|
+
|
|
383
|
+
return exitCode(report)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (import.meta.main) process.exit(main(process.argv.slice(2)))
|