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,583 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// blast-estimate — the concrete engine for blast-estimate's derivation: compute a Mission's BLAST
|
|
3
|
+
// (low/medium/high) from its touch-set + the project corpus instead of trusting the hand-typed guess,
|
|
4
|
+
// then line the computed level up against the declared one (agrees / under-called / over-called). See
|
|
5
|
+
// .agents/specs/sdd/blast-estimate/README.md for the full contract this mirrors.
|
|
6
|
+
//
|
|
7
|
+
// Three inputs, each measured — never inferred — from the corpus:
|
|
8
|
+
// - count — how many of the touch-set's areas resolve to a known work area
|
|
9
|
+
// - centrality — dependency fan-in: how many OTHER work areas' files reference a touched one
|
|
10
|
+
// - sensitivity — whether a touched area is MARKED in the opt-in `.agents/sdd/sensitive-paths.toml`
|
|
11
|
+
// (absent file = no area sensitive, not an error; a file that fails to parse fails LOUD — the
|
|
12
|
+
// estimate computes no level rather than silently reading it as "nothing marked")
|
|
13
|
+
//
|
|
14
|
+
// Two exclusions are structural, not just documented: this engine takes no "breaking"/compatibility
|
|
15
|
+
// input at all (that dimension cannot leak in), and centrality is measured fan-in only — a work area's
|
|
16
|
+
// NAME (e.g. "public") never enters the score.
|
|
17
|
+
//
|
|
18
|
+
// A work area not found in the corpus is SURFACED (`unresolved`), never silently dropped. An empty
|
|
19
|
+
// touch-set — or one that resolves to zero known areas — computes `unknown`, never `low` ("nothing
|
|
20
|
+
// touched is not evidence of low reach").
|
|
21
|
+
//
|
|
22
|
+
// Work-area recovery is NOT this engine's to invent: it REUSES the sibling touch-set-correction's
|
|
23
|
+
// pure `fileToNode(path, layouts)` over `discoverLayouts`' declared `ProjectLayout[]` — the same
|
|
24
|
+
// cross-skill reuse collision-ladder does (which imports `fileToNode` + `collectChangedFiles` from
|
|
25
|
+
// the same module). This matters and is not cosmetic: a work area spans MULTIPLE declared roots — a
|
|
26
|
+
// spec root AND an impl root (`sdd/mission-graph` lives at BOTH `.agents/specs/sdd/mission-graph/`
|
|
27
|
+
// and `plugins/sdd/skills/mission-graph/`) — so a node's identity comes from the declared layout,
|
|
28
|
+
// never from a path's shape. Any local walk that keyed on "first two segments" would split one node
|
|
29
|
+
// in two, miss it entirely from the repo root, and measure fan-in over spec prose alone.
|
|
30
|
+
//
|
|
31
|
+
// Read-only: only readdirSync/readFileSync/statSync ever run here. It RETURNS the estimate; the
|
|
32
|
+
// mission-graph's single writer records it. Pure functions are exported for node:test; running the
|
|
33
|
+
// file directly drives the CLI.
|
|
34
|
+
|
|
35
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs'
|
|
36
|
+
import { join, relative } from 'node:path'
|
|
37
|
+
import {
|
|
38
|
+
discoverLayouts,
|
|
39
|
+
fileToNode,
|
|
40
|
+
type ProjectLayout,
|
|
41
|
+
} from '../../touch-set-correction/scripts/touch-set-correction.mts'
|
|
42
|
+
|
|
43
|
+
export type { ProjectLayout }
|
|
44
|
+
|
|
45
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', '.turbo', '.next', 'coverage'])
|
|
46
|
+
const SENSITIVE_PATHS_FILE = '.agents/sdd/sensitive-paths.toml'
|
|
47
|
+
|
|
48
|
+
export type BlastLevel = 'low' | 'medium' | 'high'
|
|
49
|
+
export type DeclaredBlast = BlastLevel | 'unknown'
|
|
50
|
+
|
|
51
|
+
// ── Corpus scan — work-area discovery over the DECLARED layouts (never a path-shape guess) ──
|
|
52
|
+
|
|
53
|
+
/** Every repo-relative file path under `dir` (recursively), skipping build/vendor noise. Paths are
|
|
54
|
+
* made relative with `path.relative`, never string-sliced: `join('.', x)` normalizes the `./` away,
|
|
55
|
+
* so slicing by `root.length` silently corrupts every path under a RELATIVE root (`--root .`, the
|
|
56
|
+
* CLI's default) while working fine under an absolute one. */
|
|
57
|
+
function walkFiles(dir: string, root: string, out: string[]): void {
|
|
58
|
+
let entries: import('node:fs').Dirent[]
|
|
59
|
+
try {
|
|
60
|
+
entries = readdirSync(dir, { withFileTypes: true })
|
|
61
|
+
} catch {
|
|
62
|
+
return // an undeclared or absent root contributes nothing
|
|
63
|
+
}
|
|
64
|
+
for (const entry of entries) {
|
|
65
|
+
if (SKIP_DIRS.has(entry.name)) continue
|
|
66
|
+
const full = join(dir, entry.name)
|
|
67
|
+
if (entry.isDirectory()) walkFiles(full, root, out)
|
|
68
|
+
else out.push(relative(root, full).replace(/\\/g, '/'))
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* discoverWorkAreas — the corpus's work areas, grouped by node id, over the DECLARED layouts. Walks
|
|
74
|
+
* every root of every project, maps each file through touch-set-correction's pure `fileToNode`, and
|
|
75
|
+
* groups by the node it names. Because a node's roots include BOTH its spec root and its impl root,
|
|
76
|
+
* one node's file set spans both trees — which is exactly what makes fan-in measure real dependency
|
|
77
|
+
* rather than spec-prose cross-reference. A file that maps to no node (`fileToNode` returns null —
|
|
78
|
+
* the existing "unmapped" concept) is dropped from the area map, never invented into an atom.
|
|
79
|
+
*
|
|
80
|
+
* Values are repo-relative paths; `root` is the repo root they are resolved against.
|
|
81
|
+
*/
|
|
82
|
+
export function discoverWorkAreas(layouts: ProjectLayout[], root: string): Map<string, string[]> {
|
|
83
|
+
const byNode = new Map<string, string[]>()
|
|
84
|
+
const seen = new Set<string>()
|
|
85
|
+
for (const layout of layouts) {
|
|
86
|
+
for (const rawRoot of layout.roots) {
|
|
87
|
+
const rel = rawRoot.replace(/\/+$/, '')
|
|
88
|
+
const files: string[] = []
|
|
89
|
+
walkFiles(join(root, rel), root, files)
|
|
90
|
+
for (const path of files) {
|
|
91
|
+
if (seen.has(path)) continue // a path under two declared roots is counted once
|
|
92
|
+
seen.add(path)
|
|
93
|
+
const node = fileToNode(path, layouts)
|
|
94
|
+
if (node === null) continue // unmapped — surfaced by touch-set-correction, not an atom here
|
|
95
|
+
const bucket = byNode.get(node)
|
|
96
|
+
if (bucket) bucket.push(path)
|
|
97
|
+
else byNode.set(node, [path])
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return byNode
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function escapeRegExp(s: string): string {
|
|
105
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── The reference matcher — the forms the corpus ACTUALLY uses to name a work area ──
|
|
109
|
+
//
|
|
110
|
+
// A bare `project/capability` id is what a touch-set and the ledger write, but it is NOT how prose
|
|
111
|
+
// references an area. Real references are links and paths; matching the bare id alone scored 56 of
|
|
112
|
+
// this repo's 62 `sdd` areas at fan-in 0 — including `sdd/spec-gate`, referenced by a dozen files —
|
|
113
|
+
// while the few non-zero scores came from glossary rows using an id as a typographic EXAMPLE. That
|
|
114
|
+
// measures "how often a name got used as documentation filler", not "how much of the project leans
|
|
115
|
+
// on this area", which is the opposite of what the rubric asks for and worse than no signal at all.
|
|
116
|
+
//
|
|
117
|
+
// Every path form is derived from the project's DECLARED layout roots, never hardcoded, so a
|
|
118
|
+
// re-rooted project keeps working. The `(?:[\w.-]+/)*` segment matters: a node's spec can sit NESTED
|
|
119
|
+
// under its root (`.agents/specs/sdd/authoring/spec-gate/`), in which case `fileToNode`'s
|
|
120
|
+
// capability-first rule maps the spec side to `sdd/authoring` and only the impl side to
|
|
121
|
+
// `sdd/spec-gate` — so a flat `<root>/<capability>/` derivation would still miss the spec path.
|
|
122
|
+
//
|
|
123
|
+
// This stays MENTION-based and cheap by design. Real produced/consumed symbol dependency is
|
|
124
|
+
// ssa-lowering / collision-ladder territory; the boundary was never the problem, only the recall.
|
|
125
|
+
|
|
126
|
+
/** Left boundary: a reference must not start mid-token. */
|
|
127
|
+
const LB = '(?:^|[^\\w/:.-])'
|
|
128
|
+
|
|
129
|
+
function stripTrailingSlash(s: string): string {
|
|
130
|
+
return s.replace(/\/+$/, '')
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** The reference forms that name `nodeId` unambiguously from ANYWHERE (each is project-qualified):
|
|
134
|
+
* the bare id (`sdd/spec-gate`), the skill-style ref (`sdd:spec-gate`), and a path under any of the
|
|
135
|
+
* project's declared roots, at any nesting depth (`plugins/sdd/skills/spec-gate/`,
|
|
136
|
+
* `.agents/specs/sdd/authoring/spec-gate/`). */
|
|
137
|
+
function globalReferencePattern(project: string, capability: string, roots: string[]): RegExp {
|
|
138
|
+
const alts = [
|
|
139
|
+
`${LB}${escapeRegExp(`${project}/${capability}`)}(?![\\w-])`,
|
|
140
|
+
`${LB}${escapeRegExp(`${project}:${capability}`)}(?![\\w-])`,
|
|
141
|
+
]
|
|
142
|
+
for (const root of roots) {
|
|
143
|
+
alts.push(`${escapeRegExp(stripTrailingSlash(root))}/(?:[\\w.-]+/)*${escapeRegExp(capability)}/`)
|
|
144
|
+
}
|
|
145
|
+
return new RegExp(alts.join('|'))
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** The relative-link form (`../spec-gate/`, `../../authoring/spec-gate/`) — a sibling-node link. It
|
|
149
|
+
* carries no project, so it is only counted from a file in the SAME project; otherwise two projects
|
|
150
|
+
* sharing a capability name (`manage`, `design`) would cross-credit each other's fan-in. */
|
|
151
|
+
function relativeReferencePattern(capability: string): RegExp {
|
|
152
|
+
return new RegExp(`(?:\\.\\./)+(?:[\\w.-]+/)*${escapeRegExp(capability)}/`)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function projectRoots(project: string, layouts: ProjectLayout[]): string[] {
|
|
156
|
+
return layouts.filter((l) => l.project === project).flatMap((l) => l.roots)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* computeFanInMap — centrality for the requested areas: area A's fan-in is the number of OTHER areas
|
|
161
|
+
* holding at least one file that REFERENCES A in any of the forms the corpus actually uses (see
|
|
162
|
+
* `globalReferencePattern` / `relativeReferencePattern`). Because a node's file set spans its spec
|
|
163
|
+
* AND impl roots, a reference from an implementation file counts exactly as spec prose does — fan-in
|
|
164
|
+
* measures the project's real lean on an area. An area's own files never count toward its own fan-in.
|
|
165
|
+
*
|
|
166
|
+
* `targets` defaults to every known area; passing only the areas actually being scored keeps the CLI
|
|
167
|
+
* cheap (a one-area touch-set costs 1 pattern × N files instead of 229 × N). Each file is read at
|
|
168
|
+
* most once, and an owner already credited for a target is skipped.
|
|
169
|
+
*/
|
|
170
|
+
export function computeFanInMap(
|
|
171
|
+
byNode: Map<string, string[]>,
|
|
172
|
+
root: string,
|
|
173
|
+
layouts: ProjectLayout[],
|
|
174
|
+
targets?: string[],
|
|
175
|
+
): Map<string, number> {
|
|
176
|
+
const ids = targets ?? [...byNode.keys()]
|
|
177
|
+
const specs = ids.map((id) => {
|
|
178
|
+
const slash = id.indexOf('/')
|
|
179
|
+
const project = id.slice(0, slash)
|
|
180
|
+
const capability = id.slice(slash + 1)
|
|
181
|
+
return {
|
|
182
|
+
id,
|
|
183
|
+
project,
|
|
184
|
+
global: globalReferencePattern(project, capability, projectRoots(project, layouts)),
|
|
185
|
+
relative: relativeReferencePattern(capability),
|
|
186
|
+
}
|
|
187
|
+
})
|
|
188
|
+
const referencers = new Map<string, Set<string>>(ids.map((id) => [id, new Set<string>()]))
|
|
189
|
+
for (const [owner, files] of byNode) {
|
|
190
|
+
const ownerProject = projectOf(owner)
|
|
191
|
+
for (const rel of files) {
|
|
192
|
+
let text: string
|
|
193
|
+
try {
|
|
194
|
+
text = readFileSync(join(root, rel), 'utf8')
|
|
195
|
+
} catch {
|
|
196
|
+
continue
|
|
197
|
+
}
|
|
198
|
+
for (const spec of specs) {
|
|
199
|
+
if (spec.id === owner) continue
|
|
200
|
+
const seen = referencers.get(spec.id)
|
|
201
|
+
if (seen?.has(owner)) continue // this owner already counted for `spec.id`
|
|
202
|
+
// A relative link carries no project, so it only counts within the same project.
|
|
203
|
+
if (spec.global.test(text) || (ownerProject === spec.project && spec.relative.test(text))) {
|
|
204
|
+
seen?.add(owner)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return new Map(ids.map((id) => [id, referencers.get(id)?.size ?? 0]))
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ── Sensitivity — declared, never inferred ──
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Parses the opt-in `.agents/sdd/sensitive-paths.toml`: a `sensitive = [ "id", ... ]` string array.
|
|
216
|
+
*
|
|
217
|
+
* LINE-anchored and lenient, matching manage-spec-anchors' `anchors = [ … ]` parser byte for byte in
|
|
218
|
+
* shape — a leading comment, a trailing comment, or a neighbouring key are ordinary TOML and must
|
|
219
|
+
* parse. An earlier whole-file-anchored regex rejected all three, so a perfectly valid file that
|
|
220
|
+
* marked an area computed no level at all.
|
|
221
|
+
*
|
|
222
|
+
* An EMPTY file is `[]` (a real "nothing marked" declaration). A file with no `sensitive` array at
|
|
223
|
+
* all throws — that is genuinely malformed, and an unreadable marking is not evidence of no
|
|
224
|
+
* markings.
|
|
225
|
+
*/
|
|
226
|
+
export function parseSensitivePaths(text: string): string[] {
|
|
227
|
+
if (text.trim() === '') return []
|
|
228
|
+
const m = /(^|\n)\s*sensitive\s*=\s*\[([\s\S]*?)\]/.exec(text)
|
|
229
|
+
if (!m) throw new Error('expected a `sensitive = [...]` array')
|
|
230
|
+
const out: string[] = []
|
|
231
|
+
for (const q of m[2].matchAll(/"([^"]*)"|'([^']*)'/g)) out.push((q[1] ?? q[2]).trim())
|
|
232
|
+
return out.filter((s) => s !== '')
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export type SensitiveResult = { ok: true; marked: string[] } | { ok: false; error: string }
|
|
236
|
+
|
|
237
|
+
/** Reads the opt-in sensitive-paths file. **Absent** (ENOENT) = `{ ok: true, marked: [] }` — no area
|
|
238
|
+
* is sensitive, and that is NOT an error. Anything else — present-but-unparseable, unreadable
|
|
239
|
+
* (permissions), or not a regular file — = `{ ok: false, error }`, and the caller computes no level.
|
|
240
|
+
*
|
|
241
|
+
* ONLY ENOENT is benign. A bare `catch` here would swallow EACCES and a directory-at-the-path into
|
|
242
|
+
* "no markings", which fails in the DANGEROUS direction: the estimate silently UNDER-calls blast on
|
|
243
|
+
* exactly the areas a project took the trouble to mark. A read that cannot classify must fail loud,
|
|
244
|
+
* never default to the safe-looking answer — the same duty the absent-vs-unparseable scenarios draw:
|
|
245
|
+
* an unreadable marking is not evidence of no markings. */
|
|
246
|
+
export function readSensitivePaths(corpusRoot: string): SensitiveResult {
|
|
247
|
+
const file = join(corpusRoot, SENSITIVE_PATHS_FILE)
|
|
248
|
+
let text: string
|
|
249
|
+
try {
|
|
250
|
+
if (!statSync(file).isFile()) {
|
|
251
|
+
return { ok: false, error: `${SENSITIVE_PATHS_FILE} is not a regular file` }
|
|
252
|
+
}
|
|
253
|
+
text = readFileSync(file, 'utf8')
|
|
254
|
+
} catch (err) {
|
|
255
|
+
if ((err as NodeJS.ErrnoException).code === 'ENOENT') return { ok: true, marked: [] }
|
|
256
|
+
return { ok: false, error: `${SENSITIVE_PATHS_FILE} is unreadable: ${(err as Error).message}` }
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
return { ok: true, marked: parseSensitivePaths(text) }
|
|
260
|
+
} catch (err) {
|
|
261
|
+
return { ok: false, error: `${SENSITIVE_PATHS_FILE} is unreadable: ${(err as Error).message}` }
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ── The computation — count × centrality × sensitivity ──
|
|
266
|
+
//
|
|
267
|
+
// The rubric fixes the three inputs and their ordering properties; the exact arithmetic is this
|
|
268
|
+
// engine's choice (deliberately unpinned — see the node README). Buckets, not a smooth curve, keep
|
|
269
|
+
// the mapping easy to explain and to hold constant under review:
|
|
270
|
+
// - breadth max(countScore, coverageScore) — how much of the project is disturbed, measured
|
|
271
|
+
// two ways, whichever says MORE:
|
|
272
|
+
// * countScore 1 → 0; 2-3 → 1; 4+ → 3 (absolute reach: touching many areas is broad even
|
|
273
|
+
// in a large project the touch-set nowhere near covers)
|
|
274
|
+
// * coverageScore 3 iff the touch-set covers EVERY work area of a touched project that has
|
|
275
|
+
// >= 2 work areas, else 0 (relative reach: a 3-area project touched entirely IS
|
|
276
|
+
// project-wide — the barrier agreement holds at every project size >= 2, not just 4+)
|
|
277
|
+
// - centrality 0 → 0; 1-2 → 1; 3+ → 2
|
|
278
|
+
// - sensitivity any touched area marked → +2 (a single marking is enough to move the level)
|
|
279
|
+
// score >= 3 → high; score >= 1 → medium; score 0 → low.
|
|
280
|
+
//
|
|
281
|
+
// The `>= 2 work areas` guard on coverage is load-bearing, not a nicety, and since #238 it is the
|
|
282
|
+
// CONTRACT rather than this engine's private tiebreaker. It is keyed per PROJECT, not per corpus: a
|
|
283
|
+
// 1-area project inside a larger multi-project corpus is still never project-wide.
|
|
284
|
+
//
|
|
285
|
+
// Before #238 the frozen suite was self-contradictory here — on a 1-area project "a single
|
|
286
|
+
// peripheral work area" (→ low) and "a touch-set reaching across every work area of its project"
|
|
287
|
+
// (→ high) described the SAME input with opposite Thens, and this guard silently picked the winner.
|
|
288
|
+
// The suite now states the precondition itself: the project-wide scenario requires a project holding
|
|
289
|
+
// more than one work area, and "a lone work area is its whole project but is not project-wide reach"
|
|
290
|
+
// pins the 1-area answer to low FOR A LONE AREA WITH NO FAN-IN AND NO MARKING. Coverage is reach
|
|
291
|
+
// RELATIVE to a project, and a project of one has none to cover, so coverage never fires there and
|
|
292
|
+
// BREADTH rests on absolute count alone — centrality and sensitivity still score, so a lone area that
|
|
293
|
+
// is central or marked computes medium or high.
|
|
294
|
+
|
|
295
|
+
function countScore(n: number): number {
|
|
296
|
+
if (n <= 1) return 0
|
|
297
|
+
if (n <= 3) return 1
|
|
298
|
+
return 3
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const COVERAGE_SCORE = 3
|
|
302
|
+
|
|
303
|
+
// Centrality thresholds are calibrated against REAL fan-in, which spans roughly 0..17 on a corpus of
|
|
304
|
+
// this size. They were originally 0/1-2/3+ — tuned while the reference matcher only saw bare ids, so
|
|
305
|
+
// the observed corpus max was 4 and "3+" read as the top of the scale. With the matcher fixed, "3+"
|
|
306
|
+
// covered most of the distribution and no single area could ever reach `high` on reach alone, which
|
|
307
|
+
// under-called every hub (`sdd/spec-gate`, referenced by 10 other areas, computed `medium`). The top
|
|
308
|
+
// tier now marks a genuine hub: touching an area that a large share of the project leans on IS
|
|
309
|
+
// project-scale reach, even at count 1.
|
|
310
|
+
function centralityScore(fanIn: number): number {
|
|
311
|
+
if (fanIn <= 0) return 0
|
|
312
|
+
if (fanIn <= 2) return 1
|
|
313
|
+
if (fanIn <= 6) return 2
|
|
314
|
+
return 3
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const SENSITIVITY_SCORE = 2
|
|
318
|
+
|
|
319
|
+
export function levelFromScore(score: number): BlastLevel {
|
|
320
|
+
if (score >= 3) return 'high'
|
|
321
|
+
if (score >= 1) return 'medium'
|
|
322
|
+
return 'low'
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export interface Reasons {
|
|
326
|
+
count: number
|
|
327
|
+
maxFanIn: number
|
|
328
|
+
sensitiveAreas: string[]
|
|
329
|
+
/** Every touched project the touch-set covers ENTIRELY (and which has >= 2 work areas) — the
|
|
330
|
+
* coverage half of breadth, named so a `high` on reach alone is explainable. */
|
|
331
|
+
projectWide: string[]
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** The project a work area belongs to — the first segment of its `project/capability` id. */
|
|
335
|
+
function projectOf(nodeId: string): string {
|
|
336
|
+
return nodeId.split('/')[0]
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* projectsCoveredEntirely — the touched projects whose EVERY work area is in the touch-set, among
|
|
341
|
+
* projects holding >= 2 work areas. Coverage is relative reach: it asks what fraction of a project
|
|
342
|
+
* is disturbed, where `count` only asks how many areas in absolute terms. A project with a single
|
|
343
|
+
* work area never qualifies (see the guard note above).
|
|
344
|
+
*
|
|
345
|
+
* "Every work area of its project" means every area the LAYOUTS declare — `byNode` is the discovered
|
|
346
|
+
* area map keyed by node id, so a project's area set is its nodes across all of its declared roots
|
|
347
|
+
* (spec + impl), not whatever a directory walk happened to find.
|
|
348
|
+
*/
|
|
349
|
+
export function projectsCoveredEntirely(resolved: string[], byNode: Map<string, string[]>): string[] {
|
|
350
|
+
const areasByProject = new Map<string, string[]>()
|
|
351
|
+
for (const node of byNode.keys()) {
|
|
352
|
+
const p = projectOf(node)
|
|
353
|
+
const areas = areasByProject.get(p)
|
|
354
|
+
if (areas) areas.push(node)
|
|
355
|
+
else areasByProject.set(p, [node])
|
|
356
|
+
}
|
|
357
|
+
const resolvedSet = new Set(resolved)
|
|
358
|
+
const covered: string[] = []
|
|
359
|
+
for (const project of new Set(resolved.map(projectOf))) {
|
|
360
|
+
const areas = areasByProject.get(project) ?? []
|
|
361
|
+
if (areas.length < 2) continue // a 1-area project is never "project-wide" — see the guard note
|
|
362
|
+
if (areas.every((a) => resolvedSet.has(a))) covered.push(project)
|
|
363
|
+
}
|
|
364
|
+
return covered.sort()
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** The pure scoring step, once `count`/`maxFanIn`/`sensitiveAreas`/`projectWide` are known. Exported
|
|
368
|
+
* so the ordering-property scenarios (hub-vs-leaf, marked-vs-unmarked) can be driven directly. */
|
|
369
|
+
export function scoreBlast(
|
|
370
|
+
count: number,
|
|
371
|
+
maxFanIn: number,
|
|
372
|
+
sensitiveAreas: string[],
|
|
373
|
+
projectWide: string[] = [],
|
|
374
|
+
): { level: BlastLevel; reasons: Reasons } {
|
|
375
|
+
const breadth = Math.max(countScore(count), projectWide.length > 0 ? COVERAGE_SCORE : 0)
|
|
376
|
+
const score = breadth + centralityScore(maxFanIn) + (sensitiveAreas.length > 0 ? SENSITIVITY_SCORE : 0)
|
|
377
|
+
return { level: levelFromScore(score), reasons: { count, maxFanIn, sensitiveAreas, projectWide } }
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ── The line-up — declared against computed ──
|
|
381
|
+
|
|
382
|
+
export type LineUpOutcome = 'agrees' | 'under-called' | 'over-called' | 'no-declared'
|
|
383
|
+
|
|
384
|
+
export interface LineUp {
|
|
385
|
+
outcome: LineUpOutcome
|
|
386
|
+
computed: BlastLevel
|
|
387
|
+
declared?: BlastLevel
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const ORDER: BlastLevel[] = ['low', 'medium', 'high']
|
|
391
|
+
|
|
392
|
+
/** Lines the computed level up against the hand-typed declared one. An absent or `unknown` declared
|
|
393
|
+
* blast is not an error — it is `no-declared`, and the computed level still stands on its own. */
|
|
394
|
+
export function lineUp(computed: BlastLevel, declared?: DeclaredBlast): LineUp {
|
|
395
|
+
if (declared === undefined || declared === 'unknown') return { outcome: 'no-declared', computed }
|
|
396
|
+
const di = ORDER.indexOf(declared)
|
|
397
|
+
const ci = ORDER.indexOf(computed)
|
|
398
|
+
if (di === ci) return { outcome: 'agrees', computed, declared }
|
|
399
|
+
if (di < ci) return { outcome: 'under-called', computed, declared }
|
|
400
|
+
return { outcome: 'over-called', computed, declared }
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// ── The whole estimate — pure once handed a corpus scan ──
|
|
404
|
+
|
|
405
|
+
export interface EstimateResult {
|
|
406
|
+
touchSet: string[]
|
|
407
|
+
resolved: string[]
|
|
408
|
+
unresolved: string[]
|
|
409
|
+
computed: BlastLevel | 'unknown' | null
|
|
410
|
+
reasons: Reasons | null
|
|
411
|
+
lineUp: LineUp | null
|
|
412
|
+
error?: string
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** What `estimateBlast` needs beyond the touch-set and the layouts. `root` is the repo root the
|
|
416
|
+
* layouts' repo-relative roots (and the opt-in sensitive-paths file) resolve against. */
|
|
417
|
+
export interface EstimateOptions {
|
|
418
|
+
root?: string
|
|
419
|
+
declared?: DeclaredBlast
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* estimateBlast — the whole derivation, over INJECTED layouts. Discovers the corpus's work areas by
|
|
424
|
+
* mapping every file under every declared root through touch-set-correction's pure `fileToNode`,
|
|
425
|
+
* resolves the touch-set against those areas (unresolved areas are surfaced, never dropped), reads
|
|
426
|
+
* the opt-in sensitive-paths file (fails loud on a malformed one — computed/reasons/lineUp all come
|
|
427
|
+
* back null, `error` names why), and — when at least one area resolved — scores
|
|
428
|
+
* breadth × centrality × sensitivity and lines the result up against `opts.declared`. A touch-set
|
|
429
|
+
* that resolves to zero known areas (including the empty touch-set) computes `unknown`: nothing
|
|
430
|
+
* touched is not evidence of low reach.
|
|
431
|
+
*
|
|
432
|
+
* Layouts are INJECTED rather than discovered here: `fileToNode` is pure, so tests construct layouts
|
|
433
|
+
* as fixtures over a constructed corpus (the frozen suite's "never the live corpus" preamble holds),
|
|
434
|
+
* while the CLI sources them from `discoverLayouts`.
|
|
435
|
+
*/
|
|
436
|
+
export function estimateBlast(
|
|
437
|
+
touchSet: string[],
|
|
438
|
+
layouts: ProjectLayout[],
|
|
439
|
+
opts: EstimateOptions = {},
|
|
440
|
+
): EstimateResult {
|
|
441
|
+
const root = opts.root ?? '.'
|
|
442
|
+
const byNode = discoverWorkAreas(layouts, root)
|
|
443
|
+
const known = new Set(byNode.keys())
|
|
444
|
+
// A touch-set is a SET of work areas: dedupe before counting. `count` measures how many distinct
|
|
445
|
+
// areas are disturbed, so naming one area twice must not read as twice the reach — otherwise the
|
|
446
|
+
// same change scores a higher level for being typed redundantly. The sibling touch-set-correction
|
|
447
|
+
// dedupes its own output, but the mission-graph store does not, so a duplicate genuinely arrives.
|
|
448
|
+
const unique = [...new Set(touchSet)]
|
|
449
|
+
const resolved = unique.filter((a) => known.has(a))
|
|
450
|
+
const unresolved = unique.filter((a) => !known.has(a))
|
|
451
|
+
|
|
452
|
+
const sensitive = readSensitivePaths(root)
|
|
453
|
+
if (!sensitive.ok) {
|
|
454
|
+
return { touchSet, resolved, unresolved, computed: null, reasons: null, lineUp: null, error: sensitive.error }
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (resolved.length === 0) {
|
|
458
|
+
return {
|
|
459
|
+
touchSet,
|
|
460
|
+
resolved,
|
|
461
|
+
unresolved,
|
|
462
|
+
computed: 'unknown',
|
|
463
|
+
reasons: { count: 0, maxFanIn: 0, sensitiveAreas: [], projectWide: [] },
|
|
464
|
+
lineUp: null,
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const markedSet = new Set(sensitive.marked)
|
|
469
|
+
// Only the resolved areas are scored — fan-in for the rest of the corpus is never asked for.
|
|
470
|
+
const fanInMap = computeFanInMap(byNode, root, layouts, resolved)
|
|
471
|
+
const maxFanIn = Math.max(...resolved.map((a) => fanInMap.get(a) ?? 0))
|
|
472
|
+
const sensitiveAreas = resolved.filter((a) => markedSet.has(a))
|
|
473
|
+
const projectWide = projectsCoveredEntirely(resolved, byNode)
|
|
474
|
+
const { level, reasons } = scoreBlast(resolved.length, maxFanIn, sensitiveAreas, projectWide)
|
|
475
|
+
|
|
476
|
+
return { touchSet, resolved, unresolved, computed: level, reasons, lineUp: lineUp(level, opts.declared) }
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// ── Render (TOON — the token-efficient tabular form the repo's other sdd engines emit) ──
|
|
480
|
+
|
|
481
|
+
function toonQuote(v: string): string {
|
|
482
|
+
if (v === '' || /[",;]/.test(v) || v !== v.trim()) return `"${v.replace(/"/g, '""')}"`
|
|
483
|
+
return v
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
export function renderResultToon(r: EstimateResult): string {
|
|
487
|
+
if (r.error) {
|
|
488
|
+
return [
|
|
489
|
+
`blast-estimate: error="${r.error}"`,
|
|
490
|
+
`unresolved[${r.unresolved.length}]: ${r.unresolved.map(toonQuote).join(';')}`,
|
|
491
|
+
].join('\n')
|
|
492
|
+
}
|
|
493
|
+
const lines: string[] = []
|
|
494
|
+
const lu = r.lineUp
|
|
495
|
+
const lineUpPart = lu
|
|
496
|
+
? lu.outcome === 'no-declared'
|
|
497
|
+
? 'lineup=no-declared'
|
|
498
|
+
: `lineup=${lu.outcome}(declared=${lu.declared})`
|
|
499
|
+
: 'lineup=n/a'
|
|
500
|
+
lines.push(`blast-estimate: computed=${r.computed} ${lineUpPart}`)
|
|
501
|
+
if (r.reasons) {
|
|
502
|
+
lines.push(
|
|
503
|
+
`reasons: count=${r.reasons.count} maxFanIn=${r.reasons.maxFanIn} sensitiveAreas=[${r.reasons.sensitiveAreas.join(';')}] projectWide=[${r.reasons.projectWide.join(';')}]`,
|
|
504
|
+
)
|
|
505
|
+
}
|
|
506
|
+
lines.push(`resolved[${r.resolved.length}]: ${r.resolved.map(toonQuote).join(';')}`)
|
|
507
|
+
lines.push(`unresolved[${r.unresolved.length}]: ${r.unresolved.map(toonQuote).join(';')}`)
|
|
508
|
+
return lines.join('\n')
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// ── CLI ──
|
|
512
|
+
|
|
513
|
+
function flag(argv: string[], name: string): string | undefined {
|
|
514
|
+
const i = argv.indexOf(name)
|
|
515
|
+
return i === -1 ? undefined : argv[i + 1]
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function allFlags(argv: string[], name: string): string[] {
|
|
519
|
+
const out: string[] = []
|
|
520
|
+
for (let i = 0; i < argv.length; i++) {
|
|
521
|
+
if (argv[i] === name && argv[i + 1] !== undefined) out.push(argv[i + 1])
|
|
522
|
+
}
|
|
523
|
+
return out
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function splitCsv(v: string | undefined): string[] {
|
|
527
|
+
if (v === undefined || v === '') return []
|
|
528
|
+
return v
|
|
529
|
+
.split(',')
|
|
530
|
+
.map((s) => s.trim())
|
|
531
|
+
.filter((s) => s.length > 0)
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/** Parses one `--layout '<project>:<root1>,<root2>'` flag value into a ProjectLayout (the same shape
|
|
535
|
+
* and flag touch-set-correction accepts, so the two tools take identical layout overrides). */
|
|
536
|
+
export function parseLayoutFlag(value: string): ProjectLayout | null {
|
|
537
|
+
const idx = value.indexOf(':')
|
|
538
|
+
if (idx === -1) return null
|
|
539
|
+
const project = value.slice(0, idx).trim()
|
|
540
|
+
const roots = splitCsv(value.slice(idx + 1))
|
|
541
|
+
if (project === '' || roots.length === 0) return null
|
|
542
|
+
return { project, roots }
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/** The legal `--declared` values, exactly. Returns `null` for anything else so the CLI can fail loud
|
|
546
|
+
* rather than let an unranked value masquerade as "below everything". Case-sensitive on purpose: the
|
|
547
|
+
* store writes these lowercase, and quietly accepting `High` would invite a second spelling. */
|
|
548
|
+
export function parseDeclared(raw: string): DeclaredBlast | null {
|
|
549
|
+
return raw === 'low' || raw === 'medium' || raw === 'high' || raw === 'unknown' ? raw : null
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
export function main(argv: string[]): number {
|
|
553
|
+
const root = flag(argv, '--root') ?? '.'
|
|
554
|
+
const touchSet = splitCsv(flag(argv, '--touch-set'))
|
|
555
|
+
const declaredRaw = flag(argv, '--declared')
|
|
556
|
+
// Validate rather than cast. `lineUp` ranks via ORDER.indexOf, so an unrecognized value scores -1
|
|
557
|
+
// and reads as "below every computed level" — silently fabricating the `under-called` finding this
|
|
558
|
+
// tool exists to raise, out of nothing but a typo. Fail loud instead: the same duty the
|
|
559
|
+
// sensitive-paths reader honors, and the dangerous direction is the one to refuse.
|
|
560
|
+
const declared = declaredRaw === undefined ? undefined : parseDeclared(declaredRaw)
|
|
561
|
+
if (declared === null) {
|
|
562
|
+
process.stderr.write(
|
|
563
|
+
`blast-estimate: --declared must be one of low, medium, high, unknown (got "${declaredRaw}")\n`,
|
|
564
|
+
)
|
|
565
|
+
return 1
|
|
566
|
+
}
|
|
567
|
+
const format = flag(argv, '--format') === 'json' ? 'json' : 'toon'
|
|
568
|
+
|
|
569
|
+
// Layouts come from `--layout` when given, else from discover-specs via discoverLayouts — the
|
|
570
|
+
// same resolution order touch-set-correction uses.
|
|
571
|
+
const layoutFlags = allFlags(argv, '--layout')
|
|
572
|
+
const layouts =
|
|
573
|
+
layoutFlags.length > 0
|
|
574
|
+
? layoutFlags.map(parseLayoutFlag).filter((l): l is ProjectLayout => l !== null)
|
|
575
|
+
: discoverLayouts(root, root)
|
|
576
|
+
|
|
577
|
+
const result = estimateBlast(touchSet, layouts, { root, declared })
|
|
578
|
+
|
|
579
|
+
process.stdout.write(`${format === 'json' ? JSON.stringify(result, null, 2) : renderResultToon(result)}\n`)
|
|
580
|
+
return result.error ? 1 : 0
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (import.meta.main) process.exit(main(process.argv.slice(2)))
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# builder-impl-governance
|
|
2
|
+
|
|
3
|
+
This is an internal SDD governance about the Builder's bar at the **impl gate**.
|
|
4
|
+
|
|
5
|
+
It answers two questions: does the implementation meet the frozen suite, and is each scenario
|
|
6
|
+
verified at a level that earns confidence? In SDD, the `.feature` suite is frozen at the spec gate —
|
|
7
|
+
it is the contract the implementation is built against and cannot be edited to make the build pass.
|
|
8
|
+
This bar governs how conformance to that contract is demonstrated.
|
|
9
|
+
|
|
10
|
+
It is one half of a matched pair: its sibling **`builder-spec-governance`** asks "is the contract
|
|
11
|
+
testable and covered?" at the spec gate; this bar asks "does the implementation meet that frozen
|
|
12
|
+
contract?" at the impl gate.
|
|
13
|
+
|
|
14
|
+
## What it requires
|
|
15
|
+
|
|
16
|
+
| Requirement | What it means |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| **The bar is not self-set** | Each check derives from the frozen suite — one per scenario — never free-authored from the producer's sense of done. The cold impl-judge re-derives the oracle (the expected answer) independently (ADR-0016). |
|
|
19
|
+
| **Verify as high as it doesn't hurt** | Choose each scenario's verification *level* to maximize confidence until cost, fragility, or feasibility bites: a cheap base, a thin end-to-end cap on the paths that matter, and boundary (the external dependency mocked) as the honest substitute where end-to-end is infeasible or unsafe. Record the level chosen and why. |
|
|
20
|
+
| **A graded subject still yields a boolean** | A non-deterministic subject reaches the per-scenario boolean through a rubric plus a threshold over N runs; the rubric stays out of the `.feature`. |
|
|
21
|
+
| **No green-by-tampering** | Passing means the behavior holds — never that a check was edited to pass or the frozen suite modified to make the implementation conform. |
|
|
22
|
+
| **Deterministic combinatorics go to units** | Where the domain has a deterministic inner layer, its combinatorial space (truth tables, matrices) is covered with unit tests drawn from the inner rules — the pyramid's base, separate from the one-verification-per-scenario duty. Missing that coverage is its own finding and withholds the pass. A non-deterministic subject has no such layer — verify at the acceptance level only. |
|
|
23
|
+
|
|
24
|
+
This is the SDD default for the `builder` impl bar; a plugin may bind its own per artifact-type, and
|
|
25
|
+
this one loads when the registry leaves `builder`/`impl` unbound.
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
One merged bar loaded by both faces at the **impl gate**:
|
|
30
|
+
|
|
31
|
+
- **impl-producer:** builds to it — derives its checks from the frozen suite and picks each
|
|
32
|
+
scenario's verification level
|
|
33
|
+
- **impl-judge:** verifies against it, re-deriving the oracle independently
|
|
34
|
+
|
|
35
|
+
`producer ≠ judge` holds at the agent level — the same bar, two independent readers.
|
|
36
|
+
|
|
37
|
+
## Related governances
|
|
38
|
+
|
|
39
|
+
This bar owns conformance and per-scenario verification level. Its neighbors own everything
|
|
40
|
+
around that:
|
|
41
|
+
|
|
42
|
+
- **`builder-spec-governance`** — the other half of the pair: testability and coverage of the
|
|
43
|
+
contract, judged at the spec gate. That bar freezes what must hold; this one verifies it held.
|
|
44
|
+
- **`architect-impl-governance`** — the suite's overall pyramid shape. This bar picks each
|
|
45
|
+
scenario's level; the whole-suite shape is the architect's call.
|
|
46
|
+
|
|
47
|
+
Internal SDD governance (`user-invocable: false`). Not triggered by users directly.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: builder-impl-governance
|
|
3
|
+
description: "Partial Skill: invoke by name only"
|
|
4
|
+
user-invocable: false
|
|
5
|
+
metadata:
|
|
6
|
+
actor: builder
|
|
7
|
+
gate: impl
|
|
8
|
+
compose: union
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Builder-Impl Governance — the conformance & verification-level bar
|
|
12
|
+
|
|
13
|
+
The **Builder** bar at the **impl gate**: does the implementation meet the frozen suite, and is
|
|
14
|
+
each scenario verified at a level that earns confidence? Loaded by both faces. The SDD default for the
|
|
15
|
+
`builder` impl bar; a plugin may bind its own, and this loads when the registry leaves `builder`/`impl`
|
|
16
|
+
unbound.
|
|
17
|
+
|
|
18
|
+
## The bar
|
|
19
|
+
|
|
20
|
+
- **The bar is not self-set.** Each check derives from the frozen suite — one per scenario —
|
|
21
|
+
never free-authored from the producer's sense of done. The cold impl-judge re-derives the oracle
|
|
22
|
+
independently (ADR-0016).
|
|
23
|
+
- **Verify as high as it doesn't hurt.** Choose each scenario's verification **level** to maximize
|
|
24
|
+
confidence until cost, fragility, or feasibility bites: a cheap base, a **thin e2e cap** on the
|
|
25
|
+
paths that matter, **boundary** (the external mocked) as the honest substitute where e2e is
|
|
26
|
+
infeasible or unsafe. **Record the level and why.** The suite's overall pyramid shape is the
|
|
27
|
+
architect's call (`sdd:architect-impl-governance`).
|
|
28
|
+
- **A graded subject still yields a boolean.** Reach the per-scenario boolean through a rubric +
|
|
29
|
+
threshold over N runs; the rubric stays out of the `.feature`.
|
|
30
|
+
- **No green-by-tampering.** Passing means the behavior holds, not that a check was edited to pass;
|
|
31
|
+
the frozen suite is never modified to make the implementation conform.
|
|
32
|
+
- **Deterministic combinatorics go to units.** Where the domain has a deterministic inner layer, cover
|
|
33
|
+
its combinatorial space (truth tables, matrices) with unit tests drawn from the inner rules — the
|
|
34
|
+
pyramid's base, separate from the one-verification-per-scenario duty. Missing that coverage is its
|
|
35
|
+
own finding and withholds the pass. A non-deterministic subject has no such layer — verify at the
|
|
36
|
+
acceptance level only.
|
|
37
|
+
|
|
38
|
+
## Key points (read-check)
|
|
39
|
+
|
|
40
|
+
1. **The bar is not self-set** — checks derive from the frozen suite, one per scenario; the judge
|
|
41
|
+
re-derives the oracle independently.
|
|
42
|
+
2. **Verify as high as it doesn't hurt** — cheap base, thin e2e cap, boundary as substitute where e2e
|
|
43
|
+
is infeasible/unsafe; record level and why.
|
|
44
|
+
3. **No green-by-tampering** — passing is the behavior holding, never an edited check or a modified
|
|
45
|
+
suite.
|
|
46
|
+
4. **Deterministic combinatorics go to units** (the pyramid base); missing that coverage withholds the
|
|
47
|
+
pass; a non-deterministic subject verifies at the acceptance level only.
|