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
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
// scenario ordering/sectioning checks. Pure functions are exported for node:test;
|
|
4
4
|
// running the file directly drives the CLI.
|
|
5
5
|
|
|
6
|
-
import { type Dirent, readdirSync, readFileSync } from 'node:fs'
|
|
6
|
+
import { type Dirent, readdirSync, readFileSync, realpathSync } from 'node:fs'
|
|
7
7
|
import { basename, dirname, join } from 'node:path'
|
|
8
|
+
import { pathToFileURL } from 'node:url'
|
|
8
9
|
import { validateFeatures } from 'gherkin-cli'
|
|
9
10
|
|
|
10
11
|
// ─── types ────────────────────────────────────────────────────────────────────
|
|
@@ -403,46 +404,79 @@ export interface MapRow {
|
|
|
403
404
|
scenario: string
|
|
404
405
|
}
|
|
405
406
|
|
|
406
|
-
export
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
407
|
+
export interface ScenarioMap {
|
|
408
|
+
rows: MapRow[]
|
|
409
|
+
// Trimmed text of each DATA row whose Scenario cell is not backtick-wrapped — a
|
|
410
|
+
// present-but-unparseable row, surfaced as a violation rather than silently dropped.
|
|
411
|
+
unparseable: string[]
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Parse the `## Scenario map` section's tables. The map is grouped by use case, so the section
|
|
415
|
+
// holds one or more markdown tables — a `###` sub-header or a blank line breaks a table block.
|
|
416
|
+
// Within each contiguous `|`-row block the first row is the column header and the second the dashed
|
|
417
|
+
// separator (recognized POSITIONALLY); every row after those is a DATA row whose Scenario cell
|
|
418
|
+
// (column 3) must be backtick-wrapped. A data row that is not is collected in `unparseable`: the
|
|
419
|
+
// backtick match discriminates a data row's cell, it is never the sole signal that a row exists —
|
|
420
|
+
// conflating the two silently dropped a fully-authored-but-un-backticked map (the fail-open closed
|
|
421
|
+
// here). The section is bounded at the next `## ` heading so a following section's table (e.g.
|
|
422
|
+
// `## References`) is not misread as map rows.
|
|
423
|
+
export function parseScenarioMap(specText: string): ScenarioMap | undefined {
|
|
424
|
+
// Anchor to a real `## Scenario map` HEADING line — not a mid-line or backtick-quoted prose
|
|
425
|
+
// mention of the string (a spec that documents the map must not be misread as having one).
|
|
426
|
+
const heading = /^## Scenario map[ \t]*$/m.exec(specText)
|
|
427
|
+
if (heading === null) return undefined
|
|
428
|
+
const after = specText.slice(heading.index + heading[0].length)
|
|
429
|
+
const nextHeading = after.search(/\n## /)
|
|
430
|
+
const body = nextHeading === -1 ? after : after.slice(0, nextHeading)
|
|
431
|
+
|
|
410
432
|
const rows: MapRow[] = []
|
|
433
|
+
const unparseable: string[] = []
|
|
434
|
+
let blockRow = -1 // index within the current contiguous |-row block; -1 = not in a block
|
|
411
435
|
for (const line of body.split('\n')) {
|
|
412
436
|
const t = line.trim()
|
|
413
|
-
if (!t.startsWith('|'))
|
|
437
|
+
if (!t.startsWith('|')) {
|
|
438
|
+
blockRow = -1 // any non-table line ends the current block
|
|
439
|
+
continue
|
|
440
|
+
}
|
|
441
|
+
blockRow++
|
|
442
|
+
if (blockRow < 2) continue // this block's header (0) and dashed separator (1)
|
|
414
443
|
const cells = t
|
|
415
444
|
.split('|')
|
|
416
445
|
.slice(1, -1)
|
|
417
446
|
.map((c) => c.trim())
|
|
418
|
-
|
|
419
|
-
const scenario = cells[2] ?? ''
|
|
420
|
-
// Skip the header row and its separator; a data row names its scenario in backticks.
|
|
447
|
+
const scenario = cells.length === 3 ? (cells[2] ?? '') : ''
|
|
421
448
|
const m = scenario.match(/^`(.+)`$/)
|
|
422
|
-
if (m === null)
|
|
449
|
+
if (m === null) {
|
|
450
|
+
unparseable.push(t)
|
|
451
|
+
continue
|
|
452
|
+
}
|
|
423
453
|
rows.push({ edge: cells[0] ?? '', path: cells[1] ?? '', scenario: m[1] ?? '' })
|
|
424
454
|
}
|
|
425
|
-
return rows
|
|
455
|
+
return { rows, unparseable }
|
|
426
456
|
}
|
|
427
457
|
|
|
428
458
|
export function checkScenarioMap(slug: string, file: string, featureText: string, specText: string): string[] {
|
|
429
|
-
const
|
|
430
|
-
if (
|
|
459
|
+
const map = parseScenarioMap(specText)
|
|
460
|
+
if (map === undefined) return []
|
|
431
461
|
const tag = (msg: string) => `${slug}/${file}: ${msg}`
|
|
432
462
|
const v: string[] = []
|
|
433
463
|
|
|
464
|
+
for (const raw of map.unparseable) {
|
|
465
|
+
v.push(tag(`scenario-map data row has no backtick-wrapped Scenario cell — ${raw}`))
|
|
466
|
+
}
|
|
467
|
+
|
|
434
468
|
const titles = [...featureText.matchAll(/^\s*Scenario(?: Outline)?:\s*(.+?)\s*$/gm)].map((m) => m[1] ?? '')
|
|
435
|
-
const mapped = new Set(rows.map((r) => r.scenario))
|
|
469
|
+
const mapped = new Set(map.rows.map((r) => r.scenario))
|
|
436
470
|
|
|
437
471
|
for (const t of titles) {
|
|
438
472
|
if (!mapped.has(t)) v.push(tag(`scenario is not on the scenario map — "${t}"`))
|
|
439
473
|
}
|
|
440
474
|
const titleSet = new Set(titles)
|
|
441
|
-
for (const r of rows) {
|
|
475
|
+
for (const r of map.rows) {
|
|
442
476
|
if (!titleSet.has(r.scenario)) v.push(tag(`scenario map row names no such scenario — "${r.scenario}"`))
|
|
443
477
|
}
|
|
444
478
|
const seen = new Map<string, string>()
|
|
445
|
-
for (const r of rows) {
|
|
479
|
+
for (const r of map.rows) {
|
|
446
480
|
const key = `${r.edge}\u0000${r.path}`
|
|
447
481
|
const prior = seen.get(key)
|
|
448
482
|
if (prior !== undefined) {
|
|
@@ -498,4 +532,6 @@ export function main(argv: string[]): number {
|
|
|
498
532
|
return 0
|
|
499
533
|
}
|
|
500
534
|
|
|
501
|
-
if (import.meta.
|
|
535
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
|
|
536
|
+
process.exit(main(process.argv.slice(2)))
|
|
537
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// classify-edit-class — the structural edit-class classifier for a touched frozen `.feature`
|
|
3
|
-
// (
|
|
4
|
-
//
|
|
5
|
-
// the gate can route: additive / no-content-change self-clear; narrowing /
|
|
6
|
-
// existing narrowing -> Clearance path. This engine only CLASSIFIES — it fires no
|
|
7
|
-
// adds no new floor.
|
|
3
|
+
// (the "Structural edit-class classification (freeze integrity)" section of the SDD project spec's
|
|
4
|
+
// `authoring/spec-gate` node, repo-only). Classifies each touched file's change against its
|
|
5
|
+
// committed baseline so the gate can route: additive / no-content-change self-clear; narrowing /
|
|
6
|
+
// mixed take the existing narrowing -> Clearance path. This engine only CLASSIFIES — it fires no
|
|
7
|
+
// verdict and adds no new floor.
|
|
8
8
|
//
|
|
9
9
|
// The classification is STRUCTURAL, never a raw git line-diff. A raw line-diff is fooled by a
|
|
10
10
|
// trailing step orphaned off a frozen scenario onto a newly added adjacent scenario: the orphan
|
|
@@ -33,8 +33,9 @@
|
|
|
33
33
|
// Pure functions are exported for node:test; running the file directly drives the CLI.
|
|
34
34
|
|
|
35
35
|
import { execFileSync } from 'node:child_process'
|
|
36
|
-
import { readFileSync } from 'node:fs'
|
|
36
|
+
import { readFileSync, realpathSync } from 'node:fs'
|
|
37
37
|
import { dirname, join, relative, resolve, sep } from 'node:path'
|
|
38
|
+
import { pathToFileURL } from 'node:url'
|
|
38
39
|
import { type DiffReader, diffFeatures, GitError } from 'gherkin-cli'
|
|
39
40
|
|
|
40
41
|
// ─── types ────────────────────────────────────────────────────────────────────
|
|
@@ -408,4 +409,6 @@ export function main(argv: string[]): number {
|
|
|
408
409
|
return 0
|
|
409
410
|
}
|
|
410
411
|
|
|
411
|
-
if (import.meta.
|
|
412
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
|
|
413
|
+
process.exit(main(process.argv.slice(2)))
|
|
414
|
+
}
|
|
@@ -4,4 +4,4 @@ Non-user-invocable SDD skill holding the **default spec-producer procedure**: ho
|
|
|
4
4
|
|
|
5
5
|
Loaded via the harness (`Skill`) by the **conductor** (the main session) when it runs the spec-producer role from the SDD default — the conductor authors **inline** in its own warm context (recorded `produced-by.spec-producer: sdd:automaton`) rather than spawning a producer agent. The grader stays separate: a cold `sdd-spec-judge` reviews the output.
|
|
6
6
|
|
|
7
|
-
References `sdd:spec-governance` (the universal format bar — including the required `## Use Cases` section and the use-case
|
|
7
|
+
References `sdd:spec-format-governance` (the universal format bar — including the required `## Use Cases` section, its actor-first enumeration, and the rule that scenarios derive from the CFG rather than from the stated use-case prose) plus the resolved oracle + builder + architect actor bars (the spec-gate lens set, forward face) as its self-alignment criteria, and `sdd:ownership-governance` for the write-ownership matrix. Bakes in the grilling discipline (breadth-first, depth one-at-a-time, prose before suite) and the reconcile-toward-the-correct-answer rule for contradictions surfaced during grilling.
|
|
@@ -30,9 +30,13 @@ USER_ANSWERS: <answers to previously returned QUESTIONS — or null>
|
|
|
30
30
|
|
|
31
31
|
2. **Reconcile contradictions toward the correct answer, not the popular one.** When grilling surfaces a conflict — between the `spec.md` body and the `.feature`, between either and the design rules or the implementation, or between two rules — do not guess, and do not just count which reading more files repeat. Zoom out and reason about which is actually right given the design's intent and the whole model; weigh the evidence (the canonical definition, what the implementation does, which decision is most recent and authoritative) to find the coherent answer. Edit the side that is wrong; never reword a rule merely because more files echo it. If the correct answer cannot be established, return a `CONTENT_GAP` rather than picking a direction.
|
|
32
32
|
|
|
33
|
-
3. **Write the `spec.md` body per `sdd:spec-format-governance`.** That bar owns the required structure — the `## Use Cases` section (subject, non-goals, and
|
|
33
|
+
3. **Write the `spec.md` body per `sdd:spec-format-governance`.** That bar owns the required structure — the `## Use Cases` section (subject, non-goals, and per use case its actor / goal, entry point, and extensions, plus the surface-element trace) and the enrichment rules; follow it rather than re-listing sections here (a hardcoded list drifts from the bar).
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
**Find the use cases before you name them.** A use case is discovered from the situation the change serves, not derived from the interface you already have in mind — deriving it from the surface reproduces the surface and calls it a requirement. **Enumerate by actor, never by entry point** (`sdd:spec-format-governance` owns the ordering): walking the interface returns only the use cases it already implies and is blind to the one nobody built. **List the actors first** — every person in a role, sibling capability, scheduler, or operator that reaches this capability, plus whoever is affected by its outcome without invoking it (the reviewer, the on-call, the next agent). **Then per actor name the goals** they arrive with — their result, not the call they make; where the answer restates the mechanism ("the caller wants to call it"), you have renamed the function, not found the use case, and the honest move is a `CONTENT_GAP`, never a plausible actor you invented. **Then map goals to entry points**, and report both mismatches: a goal with no entry point is a way in the capability lacks or a goal another node owns, and an entry point serving no listed goal is surface nobody asked for. On `BACKFILL` the source yields only the **served** use cases by construction — recover the unserved ones from the request history, the issue tracker, and recurring workarounds, and record where each came from rather than presenting an inferred set as complete. **Then enumerate the extensions** — walk each use case for **any path from its trigger that does not reach its success outcome**. That criterion decides membership; the recurring kinds (the refusal, the error, the boundary, the partial result, the contended or absent input) are a **prompt to search, not a closed set** (`sdd:spec-format-governance` owns the criterion — a divergence matching none of them still belongs, and a kind that cannot arise is not owed a row). Where nothing can diverge, write `extensions: none — <why>`, so the claim is visible and contestable rather than absent. **Then trace the surface** — take each element the capability exposes (flag, option, parameter, prop, event) and name the use case that needs it and the elements it may not be combined with. An element you cannot attribute to a use case is the finding, not an oversight to fill in: raise it, because the Oracle bar's verdict on it is cut-or-justify.
|
|
36
|
+
|
|
37
|
+
**Then check the extensions against the CFG — the producer-side mirror of the Architect bar.** Where the node carries a `## Control Flow` graph, every extension you just enumerated is a path that graph must actually contain, and every forbidden combination is a decision it must actually refuse. Walk them **both ways**: an edge with no extension is the ordinary uncovered-edge case, and an **extension with no edge** is a divergence the prose claims and the drawn graph cannot take (`sdd:architect-spec-governance` grades exactly this backward). Fix whichever side is wrong — add the missing edge where the extension is real, drop the extension where the graph is right — and never report `STATUS: complete` with a stated extension no edge reaches. Where the graph already reaches every stated extension, **amend neither side**: the check has found nothing, and rewriting a graph or an extension it cleared manufactures churn the Architect lens never asked for. This is a self-alignment duty, not a judge's: settling it here spends no cold round on a contradiction the Architect lens will find every time. On a node with **no** CFG the check is vacuous and fires nothing. Author the body content — What, Why, design decisions, and the command / API surface where one exists — and enrich for human review (headings, tables, short paragraphs, a diagram where it carries the idea). Never leave placeholders (`TBD`, `TODO`, empty sections). **Do not** write the control frontmatter (`status`, `project-path`, `approval`, `produced-by`) — those belong to the conductor and the gate skill. Every referenced engine, skill, or artifact path you name must be real — a reference that resolves to nothing is caught mechanically at step 5 below, but naming a real path the first time spends no round on it. **On `BACKFILL` the four sections are still mandatory** — draw the `## Control Flow` CFG and its `## Scenario map` from the code, never stop at `## Use Cases` (`sdd:spec-format-governance`; `check-spec-structure`'s `incomplete-node` flags a leaf that skips them).
|
|
38
|
+
|
|
39
|
+
4. **Write `<DOMAIN_PATH>/<DOMAIN>.feature`** — pure boolean Gherkin per `sdd:suite-format-governance`. **For a fold (aggregation) node whose rule combines two or more *interacting* sub-conditions, state that rule in closed form — and re-derive its soundness against the real data model — _before_ you derive any scenario** (`sdd:suite-format-governance`). The **order is load-bearing**: scenarios are drawn *from* the rule, so deriving them first is the retrofit-after-the-fact shape that diverged (`github-192`, by example) where stating the rule first converged (`github-224`). A **single-condition** fold may be specified by example, and demanding a closed form of it is **over-firing** — the failure mode of this rule. Closed form is not soundness (`R''` shipped a proof and still deadlocked until re-derived against the real graph, `R'''`), and it buys **convergence, not coverage** — pair the rule with a **mutation sweep** (each interacting condition's mutation breaks a distinct scenario) and a **safety dual** (a liveness scenario passes an over-permissive fold green; assert the case it cannot observe). A **matrix / per-cell** claim is the same rule applied — draw every independent cell as its own scenario, exclude the degenerate cells, and confirm the cells distinct by the sweep. **Cover every use case from the `## Use Cases` section with one-or-more scenarios** (happy path, negative mirror, boundary) — a use case with no scenario is unverified intent; a scenario with no use case is an orphan. **A stated extension earns its scenario by being a path in the CFG**, never by being drawn from the prose — the graph is the single source scenarios derive from, and a suite drawn from a stated list is 1:1 with that list by construction and can no longer surface a hole (`sdd:builder-spec-governance`). So route a divergence covered nowhere back through the graph: add the missing path, and the standing 1:1 edge coverage supplies the scenario. A **forbidden combination** is the same rule in guard form — the CFG carries the decision that refuses it, and the refusal scenario comes from that guard's edge. **On `BACKFILL`, re-derive the scenario set from the CFG's edges** rather than patching the standing suite; the retired corpus is **reference only**, a claim to verify against the current code (`sdd:suite-format-governance`). Each `Then` is an observable boolean — name the artifact a verifier reads to settle it; an act is assertable only when it leaves a trace, and where it records nothing, add the record rather than dropping the act. Never internal state, function names, "sometimes", or how the artifact was authored. Order scenarios by lifecycle stage (the step-down convention). Keep the `.feature` plain; rubric form is legal only inside an `@rubric`-tagged scenario.
|
|
36
40
|
|
|
37
41
|
**A `Given` is a test vector, not specification** (`sdd:suite-format-governance` carries the canonical bar and the swap test). Author each `Given`'s apparatus — its domain, entities, names, framing — from a domain **the artifact does not illustrate**. On a revise CR the apparatus never reuses the artifact's existing worked examples; on `BACKFILL` it never reuses the illustrations you read out of source. Read those examples in full at step 1 — they are evidence of the behavior you are specifying; exclude them only from the apparatus you author into a `Given`.
|
|
38
42
|
|
|
@@ -46,7 +50,7 @@ USER_ANSWERS: <answers to previously returned QUESTIONS — or null>
|
|
|
46
50
|
|
|
47
51
|
**A clean form check does not clear an entangled `Given`.** The engine reads form, not apparatus — it reports no violation on a `Given` whose apparatus reuses the artifact's worked examples. Re-read each authored `Given` against the test-vector bar by hand and rewrite the apparatus before returning `STATUS: complete`.
|
|
48
52
|
|
|
49
|
-
**A clean form check does not clear a scenario that cannot fail.** The engine reads form, not discrimination — a well-formed `Then`, and a well-formed `@rubric` block, are both reported clean when no subject can ever fail them. Apply the **miss test** (`sdd:suite-format-governance`) to every scenario and every `@rubric` dimension you authored: name a **plausible wrong subject** — a memorizer, a copier, a procedure-follower, a single-brancher — and check that it *loses*. Name none and the scenario is inert; rewrite it. The wrong subject must be plausible — an empty artifact fails everything and clears nothing. Rewrite any dimension grading **presence** (a line is emitted), **restatement** (the doctrine's own words), or **procedure** (the steps, not the judgment). Rewrite the **toothless finding** — a `Then` asserting a signal or finding is *raised* but not its **binding consequence** (it withholds the pass, blocks the gate, changes the outcome): name the wrong subject that raises the finding and acts on nothing, then rewrite the `Then` to assert the consequence. Rewrite the **process-`Then`** — a `Then` asserting **how** the artifact was produced ("co-developed with the code", "written test-first", "refactored before completing", authoring order) rather than its **observable behavior or end-state**; nothing in the artifact or a run reveals the authoring sequence, so it can never be a `Then` — rewrite it to assert the observable behavior instead (`sdd:suite-format-governance`). Before writing any criterion as a dimension, run the **substitutability test** (`sdd:suite-format-governance`): a criterion belongs in a `@rubric` **only if** you accept that strength elsewhere may pay for weakness here — otherwise it is not in the sum at all, it is a boolean `Then`. **Write the trade down** for each dimension you author or revise, in the same record that carries the cut's reason, naming it and **what pays for it** — an unrecorded trade is an unowned selection nobody can disagree with. The duty is **yours alone**: no judge reports a missing record, so nothing catches you skipping it. For a `@rubric`, sum what each named wrong subject **banks** (never zero a dimension to make a point): that sum sits **strictly under** the threshold — a tie passes, since the collapsing `Then` passes a score *at least* the threshold. How far under is **not** a constant: it is your judge's noise at the cut (**cSEM**), measured by scoring the subject more than once, never decreed here.
|
|
53
|
+
**A clean form check does not clear a scenario that cannot fail.** The engine reads form, not discrimination — a well-formed `Then`, and a well-formed `@rubric` block, are both reported clean when no subject can ever fail them. Apply the **miss test** (`sdd:suite-format-governance`) to every scenario and every `@rubric` dimension you authored: name a **plausible wrong subject** — a memorizer, a copier, a procedure-follower, a single-brancher — and check that it *loses*. Name none and the scenario is inert; rewrite it. The wrong subject must be plausible — an empty artifact fails everything and clears nothing. Rewrite any dimension grading **presence** (a line is emitted), **restatement** (the doctrine's own words), or **procedure** (the steps, not the judgment). Rewrite the **toothless finding** — a `Then` asserting a signal or finding is *raised* but not its **binding consequence** (it withholds the pass, blocks the gate, changes the outcome): name the wrong subject that raises the finding and acts on nothing, then rewrite the `Then` to assert the consequence. Rewrite the **process-`Then`** — a `Then` asserting **how** the artifact was produced ("co-developed with the code", "written test-first", "refactored before completing", authoring order) rather than its **observable behavior or end-state**; nothing in the artifact or a run reveals the authoring sequence, so it can never be a `Then` — rewrite it to assert the observable behavior instead (`sdd:suite-format-governance`). Before writing any criterion as a dimension, run the **substitutability test** (`sdd:suite-format-governance`): a criterion belongs in a `@rubric` **only if** you accept that strength elsewhere may pay for weakness here — otherwise it is not in the sum at all, it is a boolean `Then`. **Write the trade down** for each dimension you author or revise, in the same record that carries the cut's reason, naming it and **what pays for it** — an unrecorded trade is an unowned selection nobody can disagree with. The duty is **yours alone**: no judge reports a missing record, so nothing catches you skipping it. For a `@rubric`, sum what each named wrong subject **banks** (never zero a dimension to make a point): that sum sits **strictly under** the threshold — a tie passes, since the collapsing `Then` passes a score *at least* the threshold. How far under is **not** a constant: it is your judge's noise at the cut (**cSEM**), measured by scoring the subject more than once, never decreed here. **Ground a dimension or its cut on non-author evidence (cold-instrument doctrine).** When you justify a `@rubric` dimension or its cut with a **measurement** — an ablation Δ, a discrimination count over N runs — it is admissible only if it is **not solely your own**: it meets the non-author evidence standard the doctrine Strategist states canonically (`sdd:doctrine-loop`), never re-listed here. Your own instrument silently assumes the property under test, so a cut grounded on your own measurement alone is **not grounded** — record that it needs non-author or fresh-adversarial evidence. A measurement grounding no dimension or cut is unconstrained; this governs the *evidence*, not the cut *value* (that is the miss-test arithmetic above).
|
|
50
54
|
|
|
51
55
|
**In `revise` mode, run the substitutability test over the standing `@rubric` dimensions your CR touches, not only the ones you author.** A dimension already in the sum that fails the test is a **correction**, and a correction is not a deletion: removing it changes the attainable maximum, so the cut it leaves behind is **un-re-derived whether or not its number still needs to change**. Re-derive that cut as a fresh policy call **in the same edit** and record its reason against the new attainable maximum; raise **one clearance per corrected scenario** and never let one blanket approval stand in for each scenario's own cut decision. Route it on the **removal** — never on whether the diff calls the edit `mixed`, which the in-scenario shape is not. The full procedure is *Correcting a standing rubric* (`sdd:suite-format-governance`). A green form check does **not** clear this: `check-suite.mts` reports only the vacuous `sum(max) < threshold` rubric, and a cut nobody re-derived clears that check every time.
|
|
52
56
|
|
|
@@ -14,11 +14,9 @@ diffs. It **decides** the cut; it does not build, store, classify, or automatica
|
|
|
14
14
|
decision-evidence (SQ-F5 #194, deferred).
|
|
15
15
|
|
|
16
16
|
Built for the Op2 ★ capstone of the cyberfleet-batch change request (GitHub issue #189, the reasoning
|
|
17
|
-
front-end above the shipped deterministic back-end);
|
|
18
|
-
|
|
19
|
-
the
|
|
20
|
-
[`ssa-lowering.feature`](../../../../.agents/specs/sdd/ssa-lowering/ssa-lowering.feature) for the frozen
|
|
21
|
-
behavior suite.
|
|
17
|
+
front-end above the shipped deterministic back-end); the `ssa-lowering` node of the SDD project spec
|
|
18
|
+
(in the cyberplace repository, not shipped in this package) carries the authoritative behavior
|
|
19
|
+
description and the frozen behavior suite.
|
|
22
20
|
|
|
23
21
|
This is a **doctrine, not an engine** — it emits no `.mts`, computes nothing deterministically, and holds
|
|
24
22
|
no state. Working node name only (SQ-name #195).
|
|
@@ -4,4 +4,4 @@ The single user-facing entry for **changing an SDD project** — triggered by a
|
|
|
4
4
|
|
|
5
5
|
The session that runs this skill **is the conductor** — the in-session realization of the conductor role; the headless realization is the `automaton` agent. A third realization is **in-session plan-mode preview**: when Claude Code plan mode is active, explore runs its reasoning (classify, seed-intent grill, draft the spec + scenario list, cold spec-judge) but writes no repo files — it renders the drafted spec + suite into the plan file and ends at **ExitPlanMode**, dropping the build-to-learn spikes. On approval the next real explore adopts the preview as the settled draft. Plan mode is detected **in-body**, never via the trigger `description`, so it never re-fires per turn. It supersedes the retired spec-as-mission entries (`create-spec` / `revise-spec`): adding, revising, or deduping part of the project spec is now an **explore-phase operation inside a CR**, not a top-level mission.
|
|
6
6
|
|
|
7
|
-
Bakes in: step-1 intake (recover the request or fetch an issue URL; scaffold the `.plan.md`); explore as the live grill (classify spec-type + artifact-types, scaffold the node, seed-intent Q&A, the inline spec-producer + cold spec-judge loop with build-to-learn spikes, the iteration cap, the **freeze re-open guard**, observation routing); the internal spec gate (freeze + per-CR gate line to the conductor's own `ledger/` shard + `status: approved`); deliver (spawned impl-producer builder + the internal impl gate); handoff; and the baked autonomy bar (initial strategy, per-gate verdicts, the three hard floors). Pairs with `pause-mission` / `resume-mission`.
|
|
7
|
+
Bakes in: step-1 intake (recover the request or fetch an issue URL; scaffold the `.plan.md`); explore as the live grill (classify spec-type + artifact-types, scaffold the node, actor-first seed-intent Q&A, the inline spec-producer + cold spec-judge loop with build-to-learn spikes, the iteration cap, the **freeze re-open guard**, observation routing); the internal spec gate (freeze + per-CR gate line to the conductor's own `ledger/` shard + `status: approved`); deliver (spawned impl-producer builder + the internal impl gate); handoff; and the baked autonomy bar (initial strategy, per-gate verdicts, the three hard floors). Pairs with `pause-mission` / `resume-mission`.
|
|
@@ -39,9 +39,9 @@ For each unit the CR touches:
|
|
|
39
39
|
- **Locate or place the node — provisionally.** If a `spec.md` / `README.md` already exists at the target → this is a **revise** (no scaffolding). Otherwise **scaffold** a new node and drop it in a *plausible* home **under the layout the project declared** in its root `spec.md` placement map — `capability-first` groups by what the project *does*, `mirror-source` mirrors the source tree. Placement is judged *within* that declaration, never against a preferred one (`sdd:spec-structure-governance`, "strategy is policy, homes are data"); where no strategy is declared, the `capability-first` default applies. A layered / framework-first **top level** stays discouraged under every strategy (it scatters a capability across folders, breaking node↔folder and degrading scheduling). Consult `project-spec/place-node` (`--concept` → candidate homes; `--name` → "belongs near X" duplicate-catch) and the placement-map routing table (root `spec.md`) for contested overlaps, but **do not agonize**: placement is **provisional** and finalized cheaply at **handoff** (step 4), where a scoped Warden pass relocates it to its blessed home *in the same change* (a pure rename — freeze survives, `sdd:lifecycle-governance`). If the user named no capability, propose a capability folder from the CR and confirm.
|
|
40
40
|
- **Classify the node** (declared, never inferred): `spec-type: behavioral` (a testable unit → `## Use Cases` + a `<unit>.feature`), `reference` (a shipped non-testable artifact → `## Subject`, no `.feature`), or **descriptive** (an index → no marker). Tag the node's cross-cutting **`concept:`** (the concern it serves — e.g. `lifecycle` / `resolution`; a string or list, orthogonal to `spec-type`; it feeds `project-spec/concept-index`). Also classify each touched file's **artifact-type** (the squad key — resolved per file, **not stored**): **by convention first** (`skill` under `skills/`, `subagent` under `agents/`, …; the extension never decides). On a genuine **ambiguity or a user-flagged path**, consult and record the tiebreaker map `.agents/sdd/artifact-types.toml` and **confirm — never guess** (`sdd:artifact-type` model).
|
|
41
41
|
- **Scaffold the skeleton** per `sdd:spec-format-governance` (sections per type; `.feature` form per `sdd:suite-format-governance`). Write **no** control frontmatter (`status` / `project-path` / `approval` / `produced-by`) — those live on the root `spec.md` and belong to the conductor and the gate.
|
|
42
|
-
- **Collect seed intent.** For a **new** feature, ask 3–5 targeted questions
|
|
42
|
+
- **Collect seed intent.** For a **new** feature, ask 3–5 targeted questions — **lead with the actors** (who reaches this capability, and who is affected by its outcome without invoking it), then their goals, then the core problem, observable behavior, edge cases / non-goals, and reviewers who must be heard. Ask for the **public interface last, and never first**: an interface offered up front becomes the anchor the use cases get read off, which is the enumeration failure `sdd:spec-format-governance` exists to prevent. For **backfill** (behavior already in code), skip — the producer reads source, tests, history. For a **revise**, collect what changes and why and the parts it touches.
|
|
43
43
|
|
|
44
|
-
**The grill loop (the user loop).** You are the conductor. Run the spec-producer **inline** (load `sdd:spec-producer-governance`, or persona-load a plugin specialist for the `artifact-types`), **dispatch the cold spec-judge** each round — through the dispatch capability's intent seam when one is available (preferring its **warm** unit, context-cleared fresh via `npx cyberlegion
|
|
44
|
+
**The grill loop (the user loop).** You are the conductor. Run the spec-producer **inline** (load `sdd:spec-producer-governance`, or persona-load a plugin specialist for the `artifact-types`), **dispatch the cold spec-judge** each round — through the dispatch capability's intent seam when one is available (preferring its **warm** unit, context-cleared fresh via `npx cyberlegion@0.3.0 unit clear <ref>` before each round's judgment), else a portable cold subagent — and for build-to-learn **dispatch the impl-producer builder** the same way (its warm unit **keeps** its context across spikes; no reset) in `explore` mode against the **non-frozen** suite — spikes are thrown away; their learnings feed the live grill to steer the spec + suite. Set an **iteration cap** (default **3**; honor a user-named cap), then loop:
|
|
45
45
|
|
|
46
46
|
**Governance provenance relay.** When you dispatch the cold spec-judge, forward the inline spec-producer's declared `governances_loaded` (`sdd:spec-producer-governance`) verbatim through the same dispatch channel, keyed **`producer_governances_declared`** — a brief field when the judge is a cold subagent, a mail envelope field when it runs through an agent pool. Forward it **as-is, including an empty set** — you render **no opinion** on which governances were actually required; that check is the spec-judge's own pre-flight (`sdd:sdd-spec-judge`).
|
|
47
47
|
|
|
@@ -79,7 +79,7 @@ Build-to-keep against the **frozen** suite. The deliver **read-set is scoped** (
|
|
|
79
79
|
|
|
80
80
|
**Rebase onto the target — the last deliver act, before the gate.** Before running the impl gate, **rebase the CR branch onto the current tip of the declared target** (for a commit-to-main project, the equivalent `pull --rebase` onto the latest `main`), so the impl gate judges the **merged tree that will actually land** — keeping history linear and leaving handoff a pure consumer that never re-verifies. A **textual conflict** is resolved as **deliver code work** against the frozen `.feature` (never a `.feature` edit); the gate then runs on the resolved tree. A conflict you **cannot resolve confidently is never guess-resolved** — the frozen suite covers *this CR's* behavior, not the incoming change's, so a wrong resolution could still pass the gate and land broken; **stop and escalate** (in-session ask the user; headless return `needs-input` up the relay) and record a `halt`, never land a low-confidence resolution. Rebasing an *unmerged* CR branch is git-reversible (reflog), so it raises **no new hard floor** — but a conflict resolution that would **narrow** a frozen scenario still fires the existing **Clearance** floor, a semver class over the ceiling **Compatibility**, and a genuine contradiction **Conflict** (autonomy bar, below). The rebase-then-gate is **optimistic**: if the target **advances again** between the passing gate and the push (another CR merged in the window), **re-rebase onto the new tip and re-run the impl gate — do not push until the gate passes on the re-rebased tree**, looping until the push wins, so what lands is always a tree the gate saw green. **The loop is bounded, not forced** — if the target keeps advancing past a small cap of attempts, **stop and escalate** (record a `halt`) rather than spinning forever (a liveness stop, same as the unconfident-conflict halt).
|
|
81
81
|
|
|
82
|
-
**The impl gate (Approved → Implemented, internal).** On entering the gate, overwrite the statusline file with `impl gate`. Dispatch the **cold impl-judge** (`sdd:sdd-impl-judge` or the covering plugin's judge) — same seam-when-available wiring, preferring its warm unit context-cleared fresh via `npx cyberlegion
|
|
82
|
+
**The impl gate (Approved → Implemented, internal).** On entering the gate, overwrite the statusline file with `impl gate`. Dispatch the **cold impl-judge** (`sdd:sdd-impl-judge` or the covering plugin's judge) — same seam-when-available wiring, preferring its warm unit context-cleared fresh via `npx cyberlegion@0.3.0 unit clear <ref>` for this judgment, else a portable cold subagent — to run the verification per frozen scenario plus an orthogonal structural/scope read. Advance to **`status: implemented`** **only when every impl-judge passes** (a frozen scenario with no verification blocks the advance — impl-sync is this suite run, not a stored flag). The three actions: **approve** → `implemented`; **change** → fix the **code** (never the frozen `.feature`), under the same evidence-not-a-work-order remediation the spec gate uses (`sdd:remediation-governance`) — including the **provenance** account that stops a regressing loop; **reject** → redo, or a **Oracle-lens revert** (a frozen scenario proved fatal → unfreeze the `.feature`, return to `draft` — the only place a frozen `.feature` reopens).
|
|
83
83
|
|
|
84
84
|
## Step 4 — handoff
|
|
85
85
|
|
|
@@ -95,17 +95,21 @@ Land per the handoff unit. First **finalize placement**: run a Warden placement
|
|
|
95
95
|
|
|
96
96
|
Before you close out, run the **correction-line finalize backstop** (autonomy bar, below): flush any correction whose combat-log line was never written, creating the plan's `*.log.jsonl` if absent.
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
Also run the **plan-brief finalize backstop** (autonomy bar, below): reconcile the plan brief's `todos` and its `## NEXT` anchor to the landed state, **in this same change** — so the delivery never ships a landed mission described as in-progress.
|
|
99
|
+
|
|
100
|
+
Before closing out, **reset the mission's warm units**: `npx cyberlegion@0.3.0 unit clear <ref>` (context-clear, pane stays warm) or tear down every warm unit this mission dispatched — none carries this mission's context into the next.
|
|
99
101
|
|
|
100
102
|
Once landed, **do not spawn** the formation Warden. Surface a **one-line nudge** that a corpus-wide formation pass is due, pointing to `sdd:manage` ("audit the corpus structure" → `formation-loop`). The pass is **on-demand** — run deliberately, not auto-spawned on every landing; `sdd:manage` owns the trigger. Gate nothing on it.
|
|
101
103
|
|
|
102
104
|
## Autonomy, provenance, and the hard floor (baked in)
|
|
103
105
|
|
|
104
|
-
- **Dispatch transport.** Every spawn beyond this session states a **dispatch intent** — role, brief, expected verdict schema — never a pinned command. When a harness-agnostic dispatch capability is available (detected at runtime; the concrete case is the Legate's `dispatch-governance` composing `cyberlegion` primitives — `agent resolve` + `unit spawn` + `mail await` — with **no** `dispatch` CLI verb, the seam named in
|
|
106
|
+
- **Dispatch transport.** Every spawn beyond this session states a **dispatch intent** — role, brief, expected verdict schema — never a pinned command. When a harness-agnostic dispatch capability is available (detected at runtime; the concrete case is the Legate's `dispatch-governance` composing `cyberlegion` primitives — `agent resolve` + `unit spawn` + `mail await` — with **no** `dispatch` CLI verb, the seam named in the SDD project spec's `design/harness-spawning` node, repo-only), route through its intent seam and let it pick `subagent | channel | run-inline`, **preferring a warm unit** over a cold one-shot spawn; with no capability present, fall back to the portable cold subagent (depth-1) default — grader independence intact either way. **Warmth is a property of the unit/process; coldness of the context**: a judge's fresh-context guarantee (ADR-0016) is transport-agnostic — satisfied by a newly spawned cold subagent **or** a warm unit **context-cleared** to a fresh context before **each** judgment (re-deriving its oracle, carrying none of a prior round's context). Clear a warm unit with **`npx cyberlegion@0.3.0 unit clear <ref>`** (`<ref>` = unit id / handle / worktree branch or CR ref) — it injects the harness's own fresh-context command (`/clear` on Claude/Codex/Copilot, `/new-chat` on Cursor; fail-loud on a harness with no honest reset) so the **pane stays warm** while the **context goes cold**; it tears nothing down. The **impl-producer builder** instead stays warm and **keeps** its context across the explore spikes and the deliver build (never cleared between those uses). Warm units stay warm for **one mission** — reused within it, then **`unit clear`**'d or torn down at **handoff**, never carrying this mission's context into the next.
|
|
105
107
|
- **Initial strategy** (run start): assess blast radius + the other dimensions and emit a run-level `kind: leash` block to **your own ledger shard** (`ledger/<cr-ref>.<hash>.jsonl` — mint `<hash>` as 6 random hex **once per session** and reuse it for every line you append; `sdd:combat-log-governance`) — `leash` (`auto-none | auto-spec | auto-all`), `by: derived | user`, `approach[]`. It may be user-specified. This block is `kind: leash`, **not** `strategy` — `strategy` is the doctrine Scanner's alone. Ledger lines carry **no `ts`**.
|
|
106
108
|
- **Per-gate verdict.** At each gate, derive the leash against discovered state and either **self-assert within leash** (write `approval.<gate>: { verdict: approve, by: agent, why }`; the spec lands in the async review queue) or **stop** with a verdict packet for the human. **Never advance** when any judge fails, any open marker remains, or (at the impl gate) any frozen scenario's verification does not pass. Human ratification (`by: <name>`, advance `status`) is reserved to the in-session position holding the user channel — by default you, in-session; a headless `automaton` emits the verdict packet and stops, **even when a coordinator relays "the user approved."**
|
|
107
109
|
- **Combat log.** Append `report` / `correction` lines (and the halt that stopped you) to the plan's `*.log.jsonl` (these carry a UTC `ts`); your run-start `leash` block, self-asserted `gate` lines, and the handoff `followup` records go to **your own shard** in the durable `ledger/` directory sibling to `spec.md` — never another writer's shard, never a shared file (`strategy` there is the Scanner's alone). Free text is commit-message-grade — never code, prompts, secrets, or literal values.
|
|
108
110
|
- **Correction-line durability** (`combat-log-governance` write duty). When a gate you self-assert was reached via a **judge-reject→fix→pass**, append the discrete `correction` line (`correction-kind: judge-iteration`, a matchable `cause`) to the combat log **before** you write the gate `why` — never leave the iteration recorded only in the `why` prose; a gate that passed clean appends none. At **handoff/finalize**, if any correction occurred whose combat-log line was never flushed, write it now — **creating the plan's `*.log.jsonl` if it does not exist** — so no correction is lost to the no-log mission class (a mission with no correction forces nothing). The forced line stays a combat-log `correction`, never a ledger line.
|
|
111
|
+
- **Cause-enum conformance** (`combat-log-governance` write duty). Every time you write a `cause` — a `correction`'s matchable cause **or** a `gate` line's stop cause — **prefer an enum value; if none fits, write the off-enum string into `cause` anyway and flag the line `cause-candidate: true`**, so it stays countable as a proposed enum-growth value instead of silently failing closed. A **visibility nudge, never a write-blocking linter** — the write always succeeds, and forcing an ill-fitting enum value would only relabel the silent drop. An **absent** `cause` still fails closed (the nudge governs only the *no-value-fits* case and licenses no omission). The `gate` stop-cause enum is `dimension | clearance | ceiling`; the `correction` matchable-cause enum is `coverage-gap | design-overreach | spec-feature-contradiction | prose-impl-contradiction` — both closed sets grown only by Council ratification, and a recurring `cause-candidate` value is the growth signal the Council reads.
|
|
112
|
+
- **Plan-brief durability** (the execution-state sibling of the correction-line durability rule above). At **handoff/finalize**, a mission that **lands** reconciles its plan brief to the landed state — every todo set to its true terminal state, and the `## NEXT` anchor rewritten to say what landed, naming no remaining resume action. It is a **backstop**: it does not depend on the loop having kept the brief current, so a brief untouched since intake is reconciled in full in **one pass**, and the reconciled brief lands **in the same change as the work**. **Reconcile means *to the landed state*, not *mark everything done*** — a todo whose work was genuinely **held out of scope** stays un-completed and rides the follow-up machinery instead; marking it completed would make the doctrine Scanner's `todos-all-done ∧ source-closed` cross-check agree wrongly and clear the brief for retirement, deleting the record of work never done. A brief already matching the landed state is left **unmodified** (the backstop writes what diverges and forces no minimum footprint). Scope is the brief and nothing else: **no** `spec.md` `status`/`approval`, and **no terminal value written into the plan-level `status` dispatch flag** — that enum stays `active | approved` and terminal-ness stays **derived**, matching the Scanner's own never-writes-`status` guard. A mission that **halts** instead of landing is **not** reconciled: a halt is a `pause-mission` checkpoint of the true in-progress state, and reconciling it to "landed" would assert a landing that never happened.
|
|
109
113
|
- **Hard floors (mandatory stops):** **Clearance** of a narrowing (weakening/deleting an acceptance scenario; pre-authorizable in the CR), **Compatibility** when the semver class exceeds the change-class ceiling (pre-authorizable), and **Conflict** of a logical contradiction in the suite (not pre-authorizable). An obvious stale-mistake contradiction is a conductor-served minor fix; escalate only when both sides are plausibly intended. **Clear the statusline file** on any abort/halt that ends the mission (a hard floor stop, an unconfident-conflict escalation, or any other terminal halt) — the same exit-path clear as handoff and pause; a mid-mission escalation the user resolves in-session (not a halt) is not an exit and leaves the file as-is.
|
|
110
114
|
|
|
111
115
|
## Suspend and resume
|
|
@@ -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`
|