cyber-sdd 0.0.0 → 0.2.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/architect-spec-governance/README.md +1 -0
- package/skills/architect-spec-governance/SKILL.md +12 -1
- 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/README.md +1 -0
- package/skills/builder-spec-governance/SKILL.md +31 -3
- 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/oracle-spec-governance/README.md +7 -2
- package/skills/oracle-spec-governance/SKILL.md +21 -4
- 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/README.md +1 -1
- package/skills/spec-format-governance/SKILL.md +76 -8
- 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/README.md +1 -1
- package/skills/spec-producer-governance/SKILL.md +7 -3
- package/skills/ssa-lowering/README.md +3 -5
- package/skills/start-mission/README.md +1 -1
- package/skills/start-mission/SKILL.md +9 -5
- 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
|
@@ -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
|
+
}
|