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.
Files changed (71) hide show
  1. package/.plugin/pins.json +3 -0
  2. package/LICENSE +21 -0
  3. package/agents/sdd-automaton.md +13 -2
  4. package/agents/sdd-scanner.md +85 -0
  5. package/agents/sdd-spec-judge.md +32 -2
  6. package/agents/sdd-warden.md +9 -0
  7. package/package.json +30 -23
  8. package/skills/align-spec/scripts/align-spec.mts +3 -2
  9. package/skills/architect-spec-governance/README.md +1 -0
  10. package/skills/architect-spec-governance/SKILL.md +12 -1
  11. package/skills/blast-estimate/README.md +3 -5
  12. package/skills/blast-estimate/SKILL.md +2 -2
  13. package/skills/blast-estimate/scripts/blast-estimate.mts +7 -4
  14. package/skills/builder-impl-governance/SKILL.md +9 -1
  15. package/skills/builder-spec-governance/README.md +1 -0
  16. package/skills/builder-spec-governance/SKILL.md +31 -3
  17. package/skills/check-partition-quality/scripts/check-partition-quality.mts +3 -1
  18. package/skills/check-plan-safety/scripts/check-plan-safety.mts +5 -2
  19. package/skills/check-project-specs/scripts/check-project-specs.mts +67 -5
  20. package/skills/check-retired-terms/README.md +18 -0
  21. package/skills/check-retired-terms/SKILL.md +81 -0
  22. package/skills/check-retired-terms/scripts/check-retired-terms.mts +293 -0
  23. package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mts +3 -2
  24. package/skills/check-spec-structure/scripts/check-spec-structure.mts +3 -2
  25. package/skills/collision-ladder/README.md +3 -5
  26. package/skills/collision-ladder/scripts/collision-ladder.mts +7 -4
  27. package/skills/combat-log-governance/SKILL.md +43 -4
  28. package/skills/concept-index/scripts/concept-index.mts +3 -2
  29. package/skills/discover-plans/scripts/discover-plans.mts +5 -2
  30. package/skills/discover-specs/scripts/discover-specs.mts +5 -2
  31. package/skills/doctrine-loop/README.md +6 -0
  32. package/skills/doctrine-loop/SKILL.md +136 -2
  33. package/skills/formation-loop/SKILL.md +21 -1
  34. package/skills/gate-validation-governance/SKILL.md +2 -2
  35. package/skills/impl-producer-governance/SKILL.md +10 -1
  36. package/skills/init/scripts/wire-statusline.mts +5 -2
  37. package/skills/lifecycle-governance/SKILL.md +1 -1
  38. package/skills/manage-ignore/scripts/manage-ignore.mts +5 -2
  39. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mts +5 -2
  40. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mts +5 -2
  41. package/skills/mission-graph/README.md +3 -5
  42. package/skills/mission-graph/SKILL.md +72 -5
  43. package/skills/mission-graph/scripts/mission-graph.mts +505 -16
  44. package/skills/oracle-spec-governance/README.md +7 -2
  45. package/skills/oracle-spec-governance/SKILL.md +21 -4
  46. package/skills/place-node/scripts/place-node.mts +3 -2
  47. package/skills/plan-retirement/README.md +5 -2
  48. package/skills/plan-retirement/SKILL.md +5 -1
  49. package/skills/plan-retirement/scripts/retire-plans.mts +5 -2
  50. package/skills/plugin-contract-governance/SKILL.md +7 -1
  51. package/skills/remediation-governance/SKILL.md +36 -1
  52. package/skills/resolve-governances/scripts/resolve-governances.mts +5 -2
  53. package/skills/resolve-tracking/SKILL.md +2 -2
  54. package/skills/resolve-tracking/scripts/resolve-tracking.mts +5 -4
  55. package/skills/sdd/SKILL.md +1 -1
  56. package/skills/spec-format-governance/README.md +1 -1
  57. package/skills/spec-format-governance/SKILL.md +76 -8
  58. package/skills/spec-gate/SKILL.md +18 -2
  59. package/skills/spec-gate/scripts/check-spec-state.mts +47 -11
  60. package/skills/spec-gate/scripts/check-suite.mts +53 -17
  61. package/skills/spec-gate/scripts/classify-edit-class.mts +10 -7
  62. package/skills/spec-producer-governance/README.md +1 -1
  63. package/skills/spec-producer-governance/SKILL.md +7 -3
  64. package/skills/ssa-lowering/README.md +3 -5
  65. package/skills/start-mission/README.md +1 -1
  66. package/skills/start-mission/SKILL.md +9 -5
  67. package/skills/suite-format-governance/SKILL.md +43 -4
  68. package/skills/touch-set-correction/README.md +3 -5
  69. package/skills/touch-set-correction/scripts/touch-set-correction.mts +7 -5
  70. package/skills/verify-scenarios/SKILL.md +10 -3
  71. 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 group = byKey.get(key) ?? []
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 featureKeys = new Set(scenarioKeys.map((s) => s.key))
281
- const extras = [...byKey.keys()].filter((k) => !featureKeys.has(k)).sort()
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.main) process.exit(main(process.argv.slice(2)))
468
+ if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
469
+ process.exit(main(process.argv.slice(2)))
470
+ }