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,657 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// collision-ladder — the finer-than-node ladder (issue #189, second bullet). A read-only, pairwise
|
|
3
|
+
// collision CLASSIFIER: given a KNOWN node-level collision between two missions (the mission-graph
|
|
4
|
+
// WAW-mutex already found it), descend a ladder of finer grains — file → region → semantic(scenario)
|
|
5
|
+
// — and stop at the first rung that classifies the clash HARD (must serialize) vs SOFT (can run in
|
|
6
|
+
// parallel, reconciled by rebase). Plus the shared-thin-file hard→soft downgrade: a file touched by
|
|
7
|
+
// many missions (router/barrel/registry) that would over-serialize gets the region/semantic descent
|
|
8
|
+
// to downgrade, and is flagged as an architectural smell. See
|
|
9
|
+
// .agents/specs/sdd/collision-ladder/README.md for the full contract.
|
|
10
|
+
//
|
|
11
|
+
// Architecture — pure derivation kept apart from IO, on purpose (touch-set-correction.mts's
|
|
12
|
+
// convention it mirrors):
|
|
13
|
+
// - isFeature / isCode / hunksDisjoint / classifyFile / classify are PURE: they take and return
|
|
14
|
+
// plain data — no fs/network access. Tests exercise these directly over CONSTRUCTED pairwise
|
|
15
|
+
// touched-detail — never a live git diff or the live mission-graph store.
|
|
16
|
+
// - readFileHunks / collectMissionTouch are the thin IO SEAM: readFileHunks shells out to
|
|
17
|
+
// `git diff -U0` for a file's touched line-ranges (the new region source); collectMissionTouch
|
|
18
|
+
// REUSES the sibling touch-set-correction's `collectChangedFiles` (which itself composes
|
|
19
|
+
// `resolve-governances` for artifact-type and `gherkin-cli diff` for changed scenarios — never a
|
|
20
|
+
// reimplemented differ) and layers the hunks on top. NOT unit-tested (network/binary/fs boundary).
|
|
21
|
+
// - main() is a thin CLI: it classifies a CONSTRUCTED ClassifyInput read as JSON (--input) or one
|
|
22
|
+
// sourced from two git ranges (--from-git), rendering TOON by default or `--format json`.
|
|
23
|
+
//
|
|
24
|
+
// This tool is READ-ONLY with respect to the mission graph: it never writes to it, never detects a
|
|
25
|
+
// collision (that is the mission-graph WAW-mutex's job — this runs only AFTER one is found), and
|
|
26
|
+
// never schedules (it returns a verdict; the scheduler consumes it). It descends a CODE collision to
|
|
27
|
+
// the SYMBOL rung — produced/consumed symbols classify it disjoint(soft) / write-write(hard,
|
|
28
|
+
// `symbol-waw`) / read-after-write(hard, `symbol-raw`); when the symbols cannot be inferred it stays
|
|
29
|
+
// hard, flagged `symbol-rung-deferred`. It does NOT do the ★ SSA-lowering doctrine (issue #189's
|
|
30
|
+
// third-bullet capstone: partitioning a change into missions, versioning a write-write into an ordered
|
|
31
|
+
// dependency) — that stays deferred. It descends only to DOWNGRADE a suspected false-hard —
|
|
32
|
+
// conservative-first, relax-on-evidence — never to raise a new collision.
|
|
33
|
+
//
|
|
34
|
+
// No dependencies (the repo's node-≥23.6 / no-deps convention). Pure functions are exported for
|
|
35
|
+
// node:test; running the file directly drives the CLI.
|
|
36
|
+
|
|
37
|
+
import { execFileSync } from 'node:child_process'
|
|
38
|
+
import { dirname, join } from 'node:path'
|
|
39
|
+
import { fileURLToPath } from 'node:url'
|
|
40
|
+
import {
|
|
41
|
+
collectChangedFiles,
|
|
42
|
+
fileToNode,
|
|
43
|
+
type ProjectLayout,
|
|
44
|
+
} from '../../touch-set-correction/scripts/touch-set-correction.mts'
|
|
45
|
+
|
|
46
|
+
// ── Types ──
|
|
47
|
+
|
|
48
|
+
/** An inclusive touched line-range within a file (the region rung's grain). */
|
|
49
|
+
export interface LineRange {
|
|
50
|
+
start: number
|
|
51
|
+
end: number
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The produced (written) and consumed (read) symbol names a code file's diff yields — the symbol
|
|
55
|
+
* rung's grain. */
|
|
56
|
+
export interface SymbolSet {
|
|
57
|
+
produced: string[]
|
|
58
|
+
consumed: string[]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** One changed file of one mission under the colliding node. `hunks: null` means the line-hunks
|
|
62
|
+
* were not recorded (disjointness cannot be proven — the region rung must not clear it). `[]` means
|
|
63
|
+
* recorded-but-empty. `changedScenarios` is meaningful only for a `.feature` (the semantic rung).
|
|
64
|
+
* `symbols` is meaningful only for code (the symbol rung): `null` or `undefined` (both treated
|
|
65
|
+
* identically — un-inferable) means the produced/consumed detail could not be extracted, so the
|
|
66
|
+
* symbol rung stays hard, flagged `symbol-rung-deferred`. Optional so existing constructed fixtures
|
|
67
|
+
* that predate the symbol rung stay valid. */
|
|
68
|
+
export interface FileTouch {
|
|
69
|
+
path: string
|
|
70
|
+
artifactType: string
|
|
71
|
+
hunks: LineRange[] | null
|
|
72
|
+
changedScenarios: string[]
|
|
73
|
+
symbols?: SymbolSet | null
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** One mission's touched detail for the single colliding node. */
|
|
77
|
+
export interface MissionTouch {
|
|
78
|
+
mission: string
|
|
79
|
+
files: FileTouch[]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The classifier's whole input: the colliding node, the two missions' touched detail, an optional
|
|
83
|
+
* fleet-wide touching-mission degree per path (for shared-thin detection — defaults to the pair's
|
|
84
|
+
* 2), and the shared-thin degree threshold (default 3). */
|
|
85
|
+
export interface ClassifyInput {
|
|
86
|
+
node: string
|
|
87
|
+
x: MissionTouch
|
|
88
|
+
y: MissionTouch
|
|
89
|
+
degrees?: Record<string, number>
|
|
90
|
+
sharedThinThreshold?: number
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export type Collision = 'hard' | 'soft'
|
|
94
|
+
export type Rung = 'file' | 'region' | 'semantic' | 'symbol' | 'node'
|
|
95
|
+
export type Confidence = 'high' | 'medium' | 'low'
|
|
96
|
+
|
|
97
|
+
/** The per-shared-file verdict — the atom the node rollup is folded from. */
|
|
98
|
+
export interface FileVerdict {
|
|
99
|
+
path: string
|
|
100
|
+
collision: Collision
|
|
101
|
+
rung: Rung
|
|
102
|
+
reason: string
|
|
103
|
+
sharedThin: boolean
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The node-level verdict — hard if ANY shared file is hard; the decisive rung + a confidence that
|
|
107
|
+
* decays down the ladder; the per-shared-file detail; the shared-thin smells; and the files whose
|
|
108
|
+
* downgrade is deferred to the ★ symbol rung. */
|
|
109
|
+
export interface LadderVerdict {
|
|
110
|
+
node: string
|
|
111
|
+
collision: Collision
|
|
112
|
+
rung: Rung
|
|
113
|
+
confidence: Confidence
|
|
114
|
+
sharedFiles: FileVerdict[]
|
|
115
|
+
smells: string[]
|
|
116
|
+
deferrals: string[]
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── Pure derivations ──
|
|
120
|
+
|
|
121
|
+
const DEFAULT_SHARED_THIN_THRESHOLD = 3
|
|
122
|
+
|
|
123
|
+
/** The scenario-rung gate is STRUCTURAL — the `.feature` extension — never the resolved
|
|
124
|
+
* artifact-type (matches the sibling touch-set-correction's isFeature). */
|
|
125
|
+
export function isFeature(path: string): boolean {
|
|
126
|
+
return path.endsWith('.feature')
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const CODE_EXTENSIONS = [
|
|
130
|
+
'.mts',
|
|
131
|
+
'.ts',
|
|
132
|
+
'.tsx',
|
|
133
|
+
'.cts',
|
|
134
|
+
'.mjs',
|
|
135
|
+
'.js',
|
|
136
|
+
'.jsx',
|
|
137
|
+
'.cjs',
|
|
138
|
+
'.py',
|
|
139
|
+
'.go',
|
|
140
|
+
'.rs',
|
|
141
|
+
'.java',
|
|
142
|
+
'.rb',
|
|
143
|
+
'.c',
|
|
144
|
+
'.h',
|
|
145
|
+
'.cpp',
|
|
146
|
+
'.hpp',
|
|
147
|
+
'.cs',
|
|
148
|
+
'.swift',
|
|
149
|
+
'.kt',
|
|
150
|
+
'.sh',
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
/** A file whose semantic-rung anchor is a SYMBOL (code) rather than a scenario or prose section.
|
|
154
|
+
* Its downgrade needs symbol-level analysis — the ★ deferred capstone — so an overlapping-region
|
|
155
|
+
* code file stays hard. Detected by extension (structural, like isFeature); never a `.feature`. */
|
|
156
|
+
export function isCode(path: string): boolean {
|
|
157
|
+
if (isFeature(path)) return false
|
|
158
|
+
return CODE_EXTENSIONS.some((ext) => path.endsWith(ext))
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Two hunk sets are disjoint when no range in one overlaps any range in the other. A `null` (unknown)
|
|
162
|
+
* hunk set is NOT disjoint from anything — disjointness cannot be proven, so the region rung must not
|
|
163
|
+
* clear it. Two inclusive ranges [a,b] and [c,d] overlap iff a ≤ d and c ≤ b. */
|
|
164
|
+
export function hunksDisjoint(a: LineRange[] | null, b: LineRange[] | null): boolean {
|
|
165
|
+
if (a === null || b === null) return false
|
|
166
|
+
for (const ra of a) {
|
|
167
|
+
for (const rb of b) {
|
|
168
|
+
if (ra.start <= rb.end && rb.start <= ra.end) return false
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return true
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const RUNG_CONFIDENCE: Record<Rung, Confidence> = {
|
|
175
|
+
file: 'high',
|
|
176
|
+
region: 'medium',
|
|
177
|
+
semantic: 'low',
|
|
178
|
+
symbol: 'low',
|
|
179
|
+
node: 'low',
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const RUNG_DEPTH: Record<Rung, number> = { file: 1, region: 2, semantic: 3, symbol: 4, node: 5 }
|
|
183
|
+
const CONFIDENCE_RANK: Record<Confidence, number> = { low: 1, medium: 2, high: 3 }
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* classifyFile — the ladder descent for ONE shared file (present in both missions). Region first: if
|
|
187
|
+
* both sides' hunks are known and disjoint ⇒ SOFT at `region`. Else descend to the semantic rung,
|
|
188
|
+
* split by artifact-type (keyed structurally off the path):
|
|
189
|
+
* - `.feature` (behavioral prose) ⇒ the SCENARIO: different scenarios ⇒ SOFT, the same scenario ⇒ HARD.
|
|
190
|
+
* - code ⇒ descend further to the SYMBOL rung (see `classifySymbols`).
|
|
191
|
+
* - non-behavioral prose (no suite to anchor) ⇒ do NOT descend, stay node-serial: HARD, reason `no-anchor`.
|
|
192
|
+
* The shared-thin flag rides along (degree ≥ threshold) — the descent above IS its hard→soft downgrade.
|
|
193
|
+
*/
|
|
194
|
+
export function classifyFile(fx: FileTouch, fy: FileTouch, degree: number, threshold: number): FileVerdict {
|
|
195
|
+
const path = fx.path
|
|
196
|
+
const sharedThin = degree >= threshold
|
|
197
|
+
const base = { path, sharedThin }
|
|
198
|
+
|
|
199
|
+
// region rung — textual, artifact-neutral
|
|
200
|
+
if (hunksDisjoint(fx.hunks, fy.hunks)) {
|
|
201
|
+
return { ...base, collision: 'soft', rung: 'region', reason: 'disjoint-hunks' }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// semantic rung — artifact-type-specific
|
|
205
|
+
if (isFeature(path)) {
|
|
206
|
+
const ys = new Set(fy.changedScenarios)
|
|
207
|
+
const shared = fx.changedScenarios.some((s) => ys.has(s))
|
|
208
|
+
return shared
|
|
209
|
+
? { ...base, collision: 'hard', rung: 'semantic', reason: 'same-scenario' }
|
|
210
|
+
: { ...base, collision: 'soft', rung: 'semantic', reason: 'disjoint-scenarios' }
|
|
211
|
+
}
|
|
212
|
+
if (isCode(path)) {
|
|
213
|
+
return classifySymbols(fx, fy, base)
|
|
214
|
+
}
|
|
215
|
+
return { ...base, collision: 'hard', rung: 'node', reason: 'no-anchor' }
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* classifySymbols — the symbol rung, a code collision's finest grain (★ #189, first half). Compares
|
|
220
|
+
* each side's produced (written) and consumed (read) symbol names, in precedence order:
|
|
221
|
+
* 1. either side's `symbols` is `null`/`undefined` (un-inferable) ⇒ HARD, `symbol-rung-deferred`
|
|
222
|
+
* (conservative-first — a parse gap must never relax a real clash).
|
|
223
|
+
* 2. the two `produced` sets intersect (both write the same symbol) ⇒ HARD, `symbol-waw`.
|
|
224
|
+
* 3. one side's `consumed` intersects the other side's `produced` (either direction) ⇒ HARD,
|
|
225
|
+
* `symbol-raw`.
|
|
226
|
+
* 4. no symbol in common ⇒ SOFT, `disjoint-symbols`.
|
|
227
|
+
* Pure — no fs/network access; deterministic.
|
|
228
|
+
*/
|
|
229
|
+
export function classifySymbols(
|
|
230
|
+
fx: FileTouch,
|
|
231
|
+
fy: FileTouch,
|
|
232
|
+
base: { path: string; sharedThin: boolean },
|
|
233
|
+
): FileVerdict {
|
|
234
|
+
if (fx.symbols === null || fx.symbols === undefined || fy.symbols === null || fy.symbols === undefined) {
|
|
235
|
+
return { ...base, collision: 'hard', rung: 'symbol', reason: 'symbol-rung-deferred' }
|
|
236
|
+
}
|
|
237
|
+
const xProduced = new Set(fx.symbols.produced)
|
|
238
|
+
const yProduced = new Set(fy.symbols.produced)
|
|
239
|
+
if (fx.symbols.produced.some((s) => yProduced.has(s))) {
|
|
240
|
+
return { ...base, collision: 'hard', rung: 'symbol', reason: 'symbol-waw' }
|
|
241
|
+
}
|
|
242
|
+
const rawClash =
|
|
243
|
+
fx.symbols.consumed.some((s) => yProduced.has(s)) || fy.symbols.consumed.some((s) => xProduced.has(s))
|
|
244
|
+
if (rawClash) {
|
|
245
|
+
return { ...base, collision: 'hard', rung: 'symbol', reason: 'symbol-raw' }
|
|
246
|
+
}
|
|
247
|
+
return { ...base, collision: 'soft', rung: 'symbol', reason: 'disjoint-symbols' }
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function bySharedThenPath(a: FileVerdict, b: FileVerdict): number {
|
|
251
|
+
return a.path < b.path ? -1 : a.path > b.path ? 1 : 0
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* classify — the whole pure ladder. Recovers the SHARED files (same path in both missions' detail),
|
|
256
|
+
* classifies each, and folds the node rollup: the node collision is HARD if ANY shared file is hard,
|
|
257
|
+
* else SOFT. When no file is shared, the file rung already clears it (soft, high confidence). The
|
|
258
|
+
* decisive rung is the deepest rung among the files that DECIDE the verdict (the hard files when hard,
|
|
259
|
+
* the soft-cleared files when soft), and the confidence is that rung's — so it decays down the ladder.
|
|
260
|
+
* Deterministic + stably ordered for fixed inputs; no fs/network access of its own.
|
|
261
|
+
*/
|
|
262
|
+
export function classify(input: ClassifyInput): LadderVerdict {
|
|
263
|
+
const threshold = input.sharedThinThreshold ?? DEFAULT_SHARED_THIN_THRESHOLD
|
|
264
|
+
const degrees = input.degrees ?? {}
|
|
265
|
+
const xByPath = new Map(input.x.files.map((f) => [f.path, f]))
|
|
266
|
+
const sharedPaths = input.y.files.filter((f) => xByPath.has(f.path)).map((f) => f.path)
|
|
267
|
+
const uniqueSharedPaths = [...new Set(sharedPaths)].sort()
|
|
268
|
+
|
|
269
|
+
// file rung — disjoint files never really collide
|
|
270
|
+
if (uniqueSharedPaths.length === 0) {
|
|
271
|
+
return {
|
|
272
|
+
node: input.node,
|
|
273
|
+
collision: 'soft',
|
|
274
|
+
rung: 'file',
|
|
275
|
+
confidence: 'high',
|
|
276
|
+
sharedFiles: [],
|
|
277
|
+
smells: [],
|
|
278
|
+
deferrals: [],
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const yByPath = new Map(input.y.files.map((f) => [f.path, f]))
|
|
283
|
+
const sharedFiles: FileVerdict[] = uniqueSharedPaths
|
|
284
|
+
.map((path) => {
|
|
285
|
+
const fx = xByPath.get(path) as FileTouch
|
|
286
|
+
const fy = yByPath.get(path) as FileTouch
|
|
287
|
+
const degree = degrees[path] ?? 2
|
|
288
|
+
return classifyFile(fx, fy, degree, threshold)
|
|
289
|
+
})
|
|
290
|
+
.sort(bySharedThenPath)
|
|
291
|
+
|
|
292
|
+
const collision: Collision = sharedFiles.some((f) => f.collision === 'hard') ? 'hard' : 'soft'
|
|
293
|
+
const deciding = sharedFiles.filter((f) => f.collision === collision)
|
|
294
|
+
const decisive = deciding.reduce(
|
|
295
|
+
(deepest, f) => (RUNG_DEPTH[f.rung] > RUNG_DEPTH[deepest.rung] ? f : deepest),
|
|
296
|
+
deciding[0],
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
const smells = sharedFiles
|
|
300
|
+
.filter((f) => f.sharedThin)
|
|
301
|
+
.map((f) => f.path)
|
|
302
|
+
.sort()
|
|
303
|
+
const deferrals = sharedFiles
|
|
304
|
+
.filter((f) => f.reason === 'symbol-rung-deferred')
|
|
305
|
+
.map((f) => f.path)
|
|
306
|
+
.sort()
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
node: input.node,
|
|
310
|
+
collision,
|
|
311
|
+
rung: decisive.rung,
|
|
312
|
+
confidence: RUNG_CONFIDENCE[decisive.rung],
|
|
313
|
+
sharedFiles,
|
|
314
|
+
smells,
|
|
315
|
+
deferrals,
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** Confidence ordering helper (exported for callers comparing two verdicts, e.g. the confidence-decay
|
|
320
|
+
* scenario): higher rank = more trustworthy. */
|
|
321
|
+
export function confidenceRank(c: Confidence): number {
|
|
322
|
+
return CONFIDENCE_RANK[c]
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ── IO seam (thin — network/binary/fs boundary; not unit-tested, mirrors touch-set-correction.mts) ──
|
|
326
|
+
|
|
327
|
+
/** A file's touched line-ranges of `base..head` via `git diff -U0` (zero context, so each hunk's
|
|
328
|
+
* `+A,B` header is exactly the changed lines). Returns the added-side ranges (the new-file lines the
|
|
329
|
+
* mission wrote). On any git failure (absent binary, bad refs, not a repo) returns null (unknown —
|
|
330
|
+
* the region rung will not clear it). */
|
|
331
|
+
export function readFileHunks(base: string, head: string, path: string, cwd: string): LineRange[] | null {
|
|
332
|
+
let out: string
|
|
333
|
+
try {
|
|
334
|
+
out = execFileSync('git', ['diff', '-U0', `${base}..${head}`, '--', path], {
|
|
335
|
+
encoding: 'utf8',
|
|
336
|
+
cwd,
|
|
337
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
338
|
+
})
|
|
339
|
+
} catch {
|
|
340
|
+
return null
|
|
341
|
+
}
|
|
342
|
+
const ranges: LineRange[] = []
|
|
343
|
+
for (const line of out.split('\n')) {
|
|
344
|
+
// @@ -a,b +c,d @@ — the +c,d side is the added lines; d defaults to 1 when omitted
|
|
345
|
+
const m = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/.exec(line)
|
|
346
|
+
if (!m) continue
|
|
347
|
+
const start = Number(m[1])
|
|
348
|
+
const count = m[2] === undefined ? 1 : Number(m[2])
|
|
349
|
+
if (count === 0) continue // a pure deletion touches no new-file line
|
|
350
|
+
ranges.push({ start, end: start + count - 1 })
|
|
351
|
+
}
|
|
352
|
+
return ranges
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// TS/JS-family extensions the symbol extractor attempts — a reasonable heuristic subset of
|
|
356
|
+
// CODE_EXTENSIONS. Any other code language (.py .go .rs etc.) always defers (returns null).
|
|
357
|
+
const JS_TS_EXTENSIONS = ['.mts', '.ts', '.tsx', '.cts', '.mjs', '.js', '.jsx', '.cjs']
|
|
358
|
+
|
|
359
|
+
const JS_KEYWORDS = new Set([
|
|
360
|
+
'const',
|
|
361
|
+
'let',
|
|
362
|
+
'var',
|
|
363
|
+
'function',
|
|
364
|
+
'class',
|
|
365
|
+
'return',
|
|
366
|
+
'if',
|
|
367
|
+
'else',
|
|
368
|
+
'for',
|
|
369
|
+
'while',
|
|
370
|
+
'do',
|
|
371
|
+
'switch',
|
|
372
|
+
'case',
|
|
373
|
+
'break',
|
|
374
|
+
'continue',
|
|
375
|
+
'default',
|
|
376
|
+
'new',
|
|
377
|
+
'this',
|
|
378
|
+
'super',
|
|
379
|
+
'import',
|
|
380
|
+
'export',
|
|
381
|
+
'from',
|
|
382
|
+
'as',
|
|
383
|
+
'async',
|
|
384
|
+
'await',
|
|
385
|
+
'try',
|
|
386
|
+
'catch',
|
|
387
|
+
'finally',
|
|
388
|
+
'throw',
|
|
389
|
+
'typeof',
|
|
390
|
+
'instanceof',
|
|
391
|
+
'in',
|
|
392
|
+
'of',
|
|
393
|
+
'null',
|
|
394
|
+
'undefined',
|
|
395
|
+
'true',
|
|
396
|
+
'false',
|
|
397
|
+
'void',
|
|
398
|
+
'delete',
|
|
399
|
+
'yield',
|
|
400
|
+
'extends',
|
|
401
|
+
'implements',
|
|
402
|
+
'interface',
|
|
403
|
+
'type',
|
|
404
|
+
'enum',
|
|
405
|
+
'namespace',
|
|
406
|
+
'public',
|
|
407
|
+
'private',
|
|
408
|
+
'protected',
|
|
409
|
+
'readonly',
|
|
410
|
+
'static',
|
|
411
|
+
'get',
|
|
412
|
+
'set',
|
|
413
|
+
])
|
|
414
|
+
|
|
415
|
+
const IDENTIFIER_RE = /\b[A-Za-z_$][A-Za-z0-9_$]*\b/g
|
|
416
|
+
|
|
417
|
+
/** Best-effort, CONSERVATIVE symbol extraction for a TS/JS-family code file's `base..head` diff (the
|
|
418
|
+
* symbol rung's thin IO seam, mirroring `readFileHunks`). Shells `git diff <base>..<head> -- <path>`;
|
|
419
|
+
* from ADDED lines (`+`) derives `produced` (declared names — `function NAME`, `class NAME`,
|
|
420
|
+
* `(export )?(const|let|var) NAME`, a method/assignment head `NAME(` or `NAME =`, `export function
|
|
421
|
+
* NAME`) and `consumed` (other referenced identifiers minus the produced set and minus JS keywords).
|
|
422
|
+
* Only attempted for TS/JS-family extensions — any other code language, or any failure, returns
|
|
423
|
+
* `null` (defer — a parse gap must never relax a real clash). Untested (fs/binary boundary). */
|
|
424
|
+
export function extractSymbols(base: string, head: string, path: string, cwd: string): SymbolSet | null {
|
|
425
|
+
if (!JS_TS_EXTENSIONS.some((ext) => path.endsWith(ext))) return null
|
|
426
|
+
let out: string
|
|
427
|
+
try {
|
|
428
|
+
out = execFileSync('git', ['diff', `${base}..${head}`, '--', path], {
|
|
429
|
+
encoding: 'utf8',
|
|
430
|
+
cwd,
|
|
431
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
432
|
+
})
|
|
433
|
+
} catch {
|
|
434
|
+
return null
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const produced = new Set<string>()
|
|
438
|
+
const consumed = new Set<string>()
|
|
439
|
+
const declPatterns = [
|
|
440
|
+
/^\s*export\s+function\s+([A-Za-z_$][A-Za-z0-9_$]*)/,
|
|
441
|
+
/^\s*function\s+([A-Za-z_$][A-Za-z0-9_$]*)/,
|
|
442
|
+
/^\s*export\s+class\s+([A-Za-z_$][A-Za-z0-9_$]*)/,
|
|
443
|
+
/^\s*class\s+([A-Za-z_$][A-Za-z0-9_$]*)/,
|
|
444
|
+
/^\s*export\s+(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)/,
|
|
445
|
+
/^\s*(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)/,
|
|
446
|
+
/^\s*([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/, // method/function-call head
|
|
447
|
+
/^\s*([A-Za-z_$][A-Za-z0-9_$]*)\s*=[^=]/, // plain assignment head
|
|
448
|
+
]
|
|
449
|
+
|
|
450
|
+
let sawAddedLine = false
|
|
451
|
+
for (const rawLine of out.split('\n')) {
|
|
452
|
+
if (!rawLine.startsWith('+') || rawLine.startsWith('+++')) continue
|
|
453
|
+
const line = rawLine.slice(1)
|
|
454
|
+
if (line.trim() === '') continue
|
|
455
|
+
sawAddedLine = true
|
|
456
|
+
|
|
457
|
+
let declared: string | null = null
|
|
458
|
+
for (const re of declPatterns) {
|
|
459
|
+
const m = re.exec(line)
|
|
460
|
+
if (m?.[1]) {
|
|
461
|
+
declared = m[1]
|
|
462
|
+
break
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
if (declared) produced.add(declared)
|
|
466
|
+
|
|
467
|
+
const ids = line.match(IDENTIFIER_RE) ?? []
|
|
468
|
+
for (const id of ids) {
|
|
469
|
+
if (id === declared) continue
|
|
470
|
+
if (JS_KEYWORDS.has(id)) continue
|
|
471
|
+
if (/^\d/.test(id)) continue
|
|
472
|
+
consumed.add(id)
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (!sawAddedLine) return null
|
|
477
|
+
for (const p of produced) consumed.delete(p)
|
|
478
|
+
if (produced.size === 0 && consumed.size === 0) return null
|
|
479
|
+
|
|
480
|
+
return { produced: [...produced].sort(), consumed: [...consumed].sort() }
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/** Sources one mission's MissionTouch for the colliding node from a `base..head` range: REUSES the
|
|
484
|
+
* sibling touch-set-correction's `collectChangedFiles` (git diff + resolve-governances + gherkin-cli
|
|
485
|
+
* diff) for each file's artifact-type + changed scenarios, keeps only the files that map to `node`
|
|
486
|
+
* (via the shared `fileToNode`), and layers the region hunks + (for code) produced/consumed symbols
|
|
487
|
+
* on top. Thin IO — not unit-tested. */
|
|
488
|
+
export function collectMissionTouch(
|
|
489
|
+
mission: string,
|
|
490
|
+
node: string,
|
|
491
|
+
base: string,
|
|
492
|
+
head: string,
|
|
493
|
+
root: string,
|
|
494
|
+
cwd: string,
|
|
495
|
+
layouts: ProjectLayout[],
|
|
496
|
+
): MissionTouch {
|
|
497
|
+
const changed = collectChangedFiles(base, head, root, cwd)
|
|
498
|
+
const files: FileTouch[] = changed
|
|
499
|
+
.filter((f) => fileToNode(f.path, layouts) === node)
|
|
500
|
+
.map((f) => ({
|
|
501
|
+
path: f.path,
|
|
502
|
+
artifactType: f.artifactType,
|
|
503
|
+
hunks: readFileHunks(base, head, f.path, cwd),
|
|
504
|
+
changedScenarios: f.changedScenarios,
|
|
505
|
+
symbols: isCode(f.path) ? extractSymbols(base, head, f.path, cwd) : null,
|
|
506
|
+
}))
|
|
507
|
+
return { mission, files }
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// ── Render (TOON — the token-efficient tabular form the repo's other sdd engines emit) ──
|
|
511
|
+
|
|
512
|
+
function toonQuote(v: string): string {
|
|
513
|
+
if (v === '' || /[",;]/.test(v) || v !== v.trim()) return `"${v.replace(/"/g, '""')}"`
|
|
514
|
+
return v
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
export function renderVerdictToon(v: LadderVerdict): string {
|
|
518
|
+
const lines: string[] = []
|
|
519
|
+
lines.push(`node: ${toonQuote(v.node)}`)
|
|
520
|
+
lines.push(`collision: ${v.collision}`)
|
|
521
|
+
lines.push(`rung: ${v.rung}`)
|
|
522
|
+
lines.push(`confidence: ${v.confidence}`)
|
|
523
|
+
lines.push(`sharedFiles[${v.sharedFiles.length}]{path,collision,rung,reason,sharedThin}:`)
|
|
524
|
+
for (const f of v.sharedFiles) {
|
|
525
|
+
lines.push(` ${toonQuote(f.path)},${f.collision},${f.rung},${f.reason},${f.sharedThin}`)
|
|
526
|
+
}
|
|
527
|
+
lines.push(`smells[${v.smells.length}]: ${v.smells.map(toonQuote).join(';')}`)
|
|
528
|
+
lines.push(`deferrals[${v.deferrals.length}]: ${v.deferrals.map(toonQuote).join(';')}`)
|
|
529
|
+
return lines.join('\n')
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// ── CLI ──
|
|
533
|
+
|
|
534
|
+
function flag(argv: string[], name: string): string | undefined {
|
|
535
|
+
const i = argv.indexOf(name)
|
|
536
|
+
return i === -1 ? undefined : argv[i + 1]
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function allFlags(argv: string[], name: string): string[] {
|
|
540
|
+
const out: string[] = []
|
|
541
|
+
for (let i = 0; i < argv.length; i++) {
|
|
542
|
+
if (argv[i] === name && argv[i + 1] !== undefined) out.push(argv[i + 1])
|
|
543
|
+
}
|
|
544
|
+
return out
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function splitCsv(v: string | undefined): string[] {
|
|
548
|
+
if (v === undefined || v === '') return []
|
|
549
|
+
return v
|
|
550
|
+
.split(',')
|
|
551
|
+
.map((s) => s.trim())
|
|
552
|
+
.filter((s) => s.length > 0)
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/** Parses one `--layout '<project>:<root1>,<root2>'` flag value into a ProjectLayout (same grammar as
|
|
556
|
+
* the sibling touch-set-correction CLI). */
|
|
557
|
+
export function parseLayoutFlag(value: string): ProjectLayout | null {
|
|
558
|
+
const idx = value.indexOf(':')
|
|
559
|
+
if (idx === -1) return null
|
|
560
|
+
const project = value.slice(0, idx).trim()
|
|
561
|
+
const roots = splitCsv(value.slice(idx + 1))
|
|
562
|
+
if (project === '' || roots.length === 0) return null
|
|
563
|
+
return { project, roots }
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function readStdin(): string {
|
|
567
|
+
try {
|
|
568
|
+
return execFileSync('cat', [], { encoding: 'utf8', stdio: ['inherit', 'pipe', 'ignore'] })
|
|
569
|
+
} catch {
|
|
570
|
+
return ''
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
575
|
+
const DISCOVER_SPECS_PATH = join(HERE, '..', '..', 'discover-specs', 'scripts', 'discover-specs.mts')
|
|
576
|
+
|
|
577
|
+
interface DiscoveredSpec {
|
|
578
|
+
path: string
|
|
579
|
+
name: string
|
|
580
|
+
projectPath: string
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/** Best-effort project-layout discovery via `discover-specs` (same convention as the sibling). Used
|
|
584
|
+
* only by --from-git; --layout overrides it. On any failure returns []. */
|
|
585
|
+
function discoverLayouts(root: string, cwd: string): ProjectLayout[] {
|
|
586
|
+
try {
|
|
587
|
+
const out = execFileSync('node', [DISCOVER_SPECS_PATH, '--root', root, '--format', 'json'], {
|
|
588
|
+
encoding: 'utf8',
|
|
589
|
+
cwd,
|
|
590
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
591
|
+
})
|
|
592
|
+
const specs = JSON.parse(out) as DiscoveredSpec[]
|
|
593
|
+
return specs.map((s) => {
|
|
594
|
+
const roots = [s.path]
|
|
595
|
+
const projectPath = s.projectPath ?? ''
|
|
596
|
+
const pluginsMatch = /^plugins\/([^/]+)$/.exec(projectPath)
|
|
597
|
+
const packagesMatch = /^packages\/([^/]+)$/.exec(projectPath)
|
|
598
|
+
if (pluginsMatch) roots.push(`plugins/${pluginsMatch[1]}/skills`)
|
|
599
|
+
else if (packagesMatch) roots.push(`packages/${packagesMatch[1]}/src`)
|
|
600
|
+
else if (projectPath) roots.push(projectPath)
|
|
601
|
+
return { project: s.name, roots }
|
|
602
|
+
})
|
|
603
|
+
} catch {
|
|
604
|
+
return []
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
export function main(argv: string[]): number {
|
|
609
|
+
const format = flag(argv, '--format') === 'json' ? 'json' : 'toon'
|
|
610
|
+
let input: ClassifyInput
|
|
611
|
+
|
|
612
|
+
const fromGit = argv.includes('--from-git')
|
|
613
|
+
if (fromGit) {
|
|
614
|
+
const node = flag(argv, '--node')
|
|
615
|
+
const xRange = flag(argv, '--x')
|
|
616
|
+
const yRange = flag(argv, '--y')
|
|
617
|
+
if (node === undefined || xRange === undefined || yRange === undefined) {
|
|
618
|
+
process.stderr.write('collision-ladder: --from-git needs --node <id> --x <base..head> --y <base..head>\n')
|
|
619
|
+
return 1
|
|
620
|
+
}
|
|
621
|
+
const root = flag(argv, '--root') ?? '.'
|
|
622
|
+
const [xb, xh] = xRange.split('..')
|
|
623
|
+
const [yb, yh] = yRange.split('..')
|
|
624
|
+
const layoutFlags = allFlags(argv, '--layout')
|
|
625
|
+
const layouts =
|
|
626
|
+
layoutFlags.length > 0
|
|
627
|
+
? layoutFlags.map(parseLayoutFlag).filter((l): l is ProjectLayout => l !== null)
|
|
628
|
+
: discoverLayouts(root, root)
|
|
629
|
+
const x = collectMissionTouch(xRange, node, xb, xh ?? 'HEAD', root, root, layouts)
|
|
630
|
+
const y = collectMissionTouch(yRange, node, yb, yh ?? 'HEAD', root, root, layouts)
|
|
631
|
+
const threshold = flag(argv, '--shared-thin-threshold')
|
|
632
|
+
input = { node, x, y, sharedThinThreshold: threshold ? Number(threshold) : undefined }
|
|
633
|
+
} else {
|
|
634
|
+
const inputSrc = flag(argv, '--input')
|
|
635
|
+
if (inputSrc === undefined) {
|
|
636
|
+
process.stderr.write('collision-ladder: --input <file|-> (a ClassifyInput JSON) or --from-git is required\n')
|
|
637
|
+
return 1
|
|
638
|
+
}
|
|
639
|
+
let raw: string
|
|
640
|
+
try {
|
|
641
|
+
raw =
|
|
642
|
+
inputSrc === '-'
|
|
643
|
+
? readStdin()
|
|
644
|
+
: execFileSync('cat', [inputSrc], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })
|
|
645
|
+
input = JSON.parse(raw) as ClassifyInput
|
|
646
|
+
} catch {
|
|
647
|
+
process.stderr.write(`collision-ladder: could not read/parse ClassifyInput from ${inputSrc}\n`)
|
|
648
|
+
return 1
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const verdict = classify(input)
|
|
653
|
+
process.stdout.write(`${format === 'json' ? JSON.stringify(verdict, null, 2) : renderVerdictToon(verdict)}\n`)
|
|
654
|
+
return 0
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if (import.meta.main) process.exit(main(process.argv.slice(2)))
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# combat-log-governance
|
|
2
|
+
|
|
3
|
+
Internal SDD governance (`user-invocable: false`). The **combat-log** contract — the shape of the
|
|
4
|
+
production provenance record: the two-face record (current-state frontmatter + the durable ledger),
|
|
5
|
+
the tracked combat log (in the plan) vs the durable ledger (root-spec sibling), the seven entry kinds
|
|
6
|
+
(`report` / `correction` / `halt` → the combat log; `leash` / `gate` / `strategy` / `followup` → the
|
|
7
|
+
ledger), the CR-scoped `seq`, the write-time UTC `ts`, the pseudonymous `handle`, the safe-to-publish
|
|
8
|
+
floor, and the matchable `cause` enum.
|
|
9
|
+
|
|
10
|
+
A fixed-universal SDD governance, invariant per role. Loaded by the conductor, spec-gate, and the
|
|
11
|
+
doctrine-loop Scanner. The tracked deletion of a retired plan is the `plan-retirement` skill;
|
|
12
|
+
freeze/gating in `lifecycle-governance`; write-ownership in `ownership-governance`. Not triggered by
|
|
13
|
+
users directly.
|