cyber-sdd 0.7.0 → 0.7.2
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 +1 -2
- package/.codex-plugin/plugin.json +1 -2
- package/CHANGELOG.md +320 -0
- package/agents/sdd-scanner.md +1 -1
- package/package.json +11 -12
- package/plugin.json +55 -0
- package/skills/align-spec/SKILL.md +1 -1
- package/skills/align-spec/scripts/align-spec.mjs +6755 -12
- package/skills/blast-estimate/README.md +3 -3
- package/skills/blast-estimate/scripts/blast-estimate.mjs +601 -12
- package/skills/check-field-mandates/scripts/check-field-mandates.mjs +253 -12
- package/skills/check-partition-quality/scripts/check-partition-quality.mjs +235 -12
- package/skills/check-plan-safety/SKILL.md +2 -3
- package/skills/check-plan-safety/scripts/check-plan-safety.mjs +109 -12
- package/skills/check-plugin-manifests/scripts/check-plugin-manifests.mjs +250 -12
- package/skills/check-project-specs/SKILL.md +1 -1
- package/skills/check-project-specs/scripts/check-project-specs.mjs +593 -12
- package/skills/check-retired-terms/SKILL.md +2 -3
- package/skills/check-retired-terms/scripts/check-retired-terms.mjs +186 -12
- package/skills/check-scenario-overlap/SKILL.md +1 -2
- package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mjs +177 -12
- package/skills/check-spec-references/SKILL.md +2 -2
- package/skills/check-spec-references/scripts/check-spec-references.mjs +287 -12
- package/skills/check-spec-structure/SKILL.md +1 -2
- package/skills/check-spec-structure/scripts/check-spec-structure.mjs +243 -12
- package/skills/collision-ladder/README.md +2 -2
- package/skills/collision-ladder/scripts/collision-ladder.mjs +7047 -12
- package/skills/combat-log-governance/SKILL.md +1 -1
- package/skills/concept-index/SKILL.md +1 -1
- package/skills/concept-index/scripts/concept-index.mjs +183 -12
- package/skills/discover-plans/README.md +3 -3
- package/skills/discover-plans/SKILL.md +1 -1
- package/skills/discover-plans/scripts/discover-plans.mjs +137 -12
- package/skills/discover-specs/README.md +3 -3
- package/skills/discover-specs/SKILL.md +1 -1
- package/skills/discover-specs/scripts/discover-specs.mjs +321 -12
- package/skills/doctrine-loop/SKILL.md +1 -1
- package/skills/gate-validation-governance/SKILL.md +1 -1
- package/skills/init/README.md +2 -2
- package/skills/init/scripts/wire-statusline.mjs +168 -12
- package/skills/manage-ignore/README.md +3 -3
- package/skills/manage-ignore/SKILL.md +1 -2
- package/skills/manage-ignore/scripts/manage-ignore.mjs +285 -12
- package/skills/manage-scenario-bridge/README.md +3 -3
- package/skills/manage-scenario-bridge/SKILL.md +1 -1
- package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mjs +239 -12
- package/skills/manage-spec-anchors/README.md +3 -3
- package/skills/manage-spec-anchors/SKILL.md +1 -1
- package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mjs +310 -12
- package/skills/mission-graph/README.md +2 -2
- package/skills/mission-graph/SKILL.md +1 -1
- package/skills/mission-graph/scripts/mission-graph.mjs +1040 -12
- package/skills/pause-mission/SKILL.md +1 -1
- package/skills/place-node/SKILL.md +2 -2
- package/skills/place-node/scripts/place-node.mjs +117 -12
- package/skills/plan-retirement/README.md +2 -2
- package/skills/plan-retirement/scripts/retire-plans.mjs +123 -12
- package/skills/resolve-governances/README.md +3 -3
- package/skills/resolve-governances/SKILL.md +1 -1
- package/skills/resolve-governances/scripts/resolve-governances.mjs +352 -12
- package/skills/resolve-tracking/SKILL.md +1 -1
- package/skills/resolve-tracking/scripts/resolve-tracking.mjs +152 -12
- package/skills/scaffold-project-spec/SKILL.md +1 -1
- package/skills/spec-gate/README.md +12 -9
- package/skills/spec-gate/SKILL.md +2 -2
- package/skills/spec-gate/scripts/check-spec-state.mjs +422 -12
- package/skills/spec-gate/scripts/check-suite.mjs +6500 -12
- package/skills/spec-gate/scripts/classify-edit-class.mjs +6579 -12
- package/skills/spec-producer-governance/SKILL.md +2 -2
- package/skills/start-mission/SKILL.md +1 -1
- package/skills/suite-format-governance/README.md +1 -1
- package/skills/suite-format-governance/SKILL.md +1 -1
- package/skills/touch-set-correction/README.md +5 -5
- package/skills/touch-set-correction/SKILL.md +1 -1
- package/skills/touch-set-correction/scripts/touch-set-correction.mjs +6663 -12
- package/skills/verify-scenarios/README.md +6 -6
- package/skills/verify-scenarios/scripts/verify-scenarios.mjs +6539 -12
- package/.plugin/pins.json +0 -3
- package/.plugin/plugin.json +0 -19
- package/dist/align-spec/scripts/align-spec.js +0 -102
- package/dist/check-project-specs/scripts/check-project-specs.js +0 -312
- package/dist/check-scenario-overlap/scripts/check-scenario-overlap.js +0 -175
- package/dist/check-spec-references/scripts/check-spec-references.js +0 -285
- package/dist/check-spec-structure/scripts/check-spec-structure.js +0 -241
- package/dist/concept-index/scripts/concept-index.js +0 -181
- package/dist/discover-specs/scripts/discover-specs.js +0 -318
- package/dist/spec-gate/scripts/check-spec-state.js +0 -420
- package/dist/spec-gate/scripts/check-suite.js +0 -317
- package/dist/spec-gate/scripts/classify-edit-class.js +0 -263
- package/dist/spec-gate/vendor/gherkin-cli.js +0 -6369
- package/skills/align-spec/scripts/align-spec.bundle.mjs +0 -6753
- package/skills/align-spec/scripts/align-spec.mts +0 -188
- package/skills/blast-estimate/scripts/blast-estimate.bundle.mjs +0 -593
- package/skills/blast-estimate/scripts/blast-estimate.mts +0 -586
- package/skills/check-field-mandates/scripts/check-field-mandates.bundle.mjs +0 -251
- package/skills/check-field-mandates/scripts/check-field-mandates.mts +0 -337
- package/skills/check-partition-quality/scripts/check-partition-quality.bundle.mjs +0 -233
- package/skills/check-partition-quality/scripts/check-partition-quality.mts +0 -338
- package/skills/check-plan-safety/scripts/check-plan-safety.bundle.mjs +0 -107
- package/skills/check-plan-safety/scripts/check-plan-safety.mts +0 -148
- package/skills/check-plugin-manifests/scripts/check-plugin-manifests.bundle.mjs +0 -248
- package/skills/check-plugin-manifests/scripts/check-plugin-manifests.mts +0 -303
- package/skills/check-project-specs/scripts/check-project-specs.bundle.mjs +0 -584
- package/skills/check-project-specs/scripts/check-project-specs.mts +0 -370
- package/skills/check-retired-terms/scripts/check-retired-terms.bundle.mjs +0 -184
- package/skills/check-retired-terms/scripts/check-retired-terms.mts +0 -293
- package/skills/check-scenario-overlap/scripts/check-scenario-overlap.bundle.mjs +0 -175
- package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mts +0 -250
- package/skills/check-spec-references/scripts/check-spec-references.bundle.mjs +0 -285
- package/skills/check-spec-references/scripts/check-spec-references.mts +0 -422
- package/skills/check-spec-structure/scripts/check-spec-structure.bundle.mjs +0 -241
- package/skills/check-spec-structure/scripts/check-spec-structure.mts +0 -347
- package/skills/collision-ladder/scripts/collision-ladder.bundle.mjs +0 -7041
- package/skills/collision-ladder/scripts/collision-ladder.mts +0 -662
- package/skills/concept-index/scripts/concept-index.bundle.mjs +0 -181
- package/skills/concept-index/scripts/concept-index.mts +0 -246
- package/skills/discover-plans/scripts/discover-plans.bundle.mjs +0 -135
- package/skills/discover-plans/scripts/discover-plans.mts +0 -215
- package/skills/discover-specs/scripts/discover-specs.bundle.mjs +0 -319
- package/skills/discover-specs/scripts/discover-specs.mts +0 -420
- package/skills/init/scripts/wire-statusline.bundle.mjs +0 -166
- package/skills/init/scripts/wire-statusline.mts +0 -279
- package/skills/manage-ignore/scripts/manage-ignore.bundle.mjs +0 -244
- package/skills/manage-ignore/scripts/manage-ignore.mts +0 -297
- package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.bundle.mjs +0 -237
- package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mts +0 -159
- package/skills/manage-spec-anchors/scripts/manage-spec-anchors.bundle.mjs +0 -308
- package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mts +0 -331
- package/skills/mission-graph/scripts/mission-graph.bundle.mjs +0 -1038
- package/skills/mission-graph/scripts/mission-graph.mts +0 -1333
- package/skills/place-node/scripts/place-node.bundle.mjs +0 -115
- package/skills/place-node/scripts/place-node.mts +0 -158
- package/skills/plan-retirement/scripts/retire-plans.bundle.mjs +0 -121
- package/skills/plan-retirement/scripts/retire-plans.mts +0 -205
- package/skills/resolve-governances/scripts/resolve-governances.bundle.mjs +0 -350
- package/skills/resolve-governances/scripts/resolve-governances.mts +0 -518
- package/skills/resolve-tracking/scripts/resolve-tracking.bundle.mjs +0 -150
- package/skills/resolve-tracking/scripts/resolve-tracking.mts +0 -214
- package/skills/spec-gate/scripts/check-spec-state.bundle.mjs +0 -420
- package/skills/spec-gate/scripts/check-spec-state.mts +0 -637
- package/skills/spec-gate/scripts/check-suite.bundle.mjs +0 -6498
- package/skills/spec-gate/scripts/check-suite.mts +0 -538
- package/skills/spec-gate/scripts/classify-edit-class.bundle.mjs +0 -6577
- package/skills/spec-gate/scripts/classify-edit-class.mts +0 -414
- package/skills/spec-gate/vendor/gherkin-cli.d.mts +0 -2
- package/skills/spec-gate/vendor/gherkin-cli.mjs +0 -6545
- package/skills/touch-set-correction/scripts/touch-set-correction.bundle.mjs +0 -6655
- package/skills/touch-set-correction/scripts/touch-set-correction.mts +0 -429
- package/skills/verify-scenarios/scripts/verify-scenarios.bundle.mjs +0 -6537
- package/skills/verify-scenarios/scripts/verify-scenarios.mts +0 -470
|
@@ -1,518 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Deterministic governance MATCHER for SDD production-chain roles. For a touched
|
|
3
|
-
// file's artifact-type it NAMES, per role, the agent that runs it plus the
|
|
4
|
-
// resolved-actor bar candidates it loads — matching governances by
|
|
5
|
-
// metadata{artifact-type, actor, gate} across the caller-passed project anchors,
|
|
6
|
-
// the matched plugin squad, and the sdd defaults. It does NOT order by precedence
|
|
7
|
-
// or compose: each bar's candidates come back BUCKETED BY TIER (project /
|
|
8
|
-
// project-root / plugin / sdd), and the consuming agent loads each ref and
|
|
9
|
-
// composes by precedence (sdd-default < plugin < project-root < project), reading
|
|
10
|
-
// each governance's own `compose` at load time (see sdd:plugin-contract-governance).
|
|
11
|
-
// The conductor and the cold judges never hand-enumerate — they run this and load
|
|
12
|
-
// what it names.
|
|
13
|
-
//
|
|
14
|
-
// Anchors are CALLER-PASSED, never discovered: --project <path> (the file's own
|
|
15
|
-
// project) and optional --project-root <path> (the outer shared layer in a
|
|
16
|
-
// monorepo); a single-project repo passes only --project. --root is the registry
|
|
17
|
-
// location (`.agents/universal-plugin.json`), default ".".
|
|
18
|
-
//
|
|
19
|
-
// Pure functions are exported for node:test; running the file directly drives the
|
|
20
|
-
// CLI. No dependencies — plain node strips the types.
|
|
21
|
-
|
|
22
|
-
import { type Dirent, existsSync, readdirSync, readFileSync, realpathSync } from 'node:fs'
|
|
23
|
-
import { join } from 'node:path'
|
|
24
|
-
import { pathToFileURL } from 'node:url'
|
|
25
|
-
|
|
26
|
-
// ─── the closed sets ───────────────────────────────────────────────────────────
|
|
27
|
-
|
|
28
|
-
export const ROLE_KEYS = ['spec-producer', 'solution-producer', 'spec-judge', 'impl-producer', 'impl-judge'] as const
|
|
29
|
-
export type RoleKey = (typeof ROLE_KEYS)[number]
|
|
30
|
-
|
|
31
|
-
export const BAR_KEYS = ['oracle-spec', 'builder-spec', 'builder-impl', 'architect-spec', 'architect-impl'] as const
|
|
32
|
-
export type BarKey = (typeof BAR_KEYS)[number]
|
|
33
|
-
|
|
34
|
-
export const ACTORS = ['oracle', 'builder', 'architect'] as const
|
|
35
|
-
export const GATES = ['spec', 'impl'] as const
|
|
36
|
-
|
|
37
|
-
// The resolved-actor bars each role loads (plugin-contract-governance "Which
|
|
38
|
-
// governances each role loads"). Only the resolved-actor bars are matched here;
|
|
39
|
-
// the FIXED-UNIVERSAL governances (ownership, lifecycle, spec-format,
|
|
40
|
-
// suite-format, gate-validation, combat-log) are invariant per role and stay
|
|
41
|
-
// declared in each role/agent definition — this matcher does not re-emit them.
|
|
42
|
-
export const ROLE_LOADOUT: Record<RoleKey, { bars: BarKey[] }> = {
|
|
43
|
-
'spec-producer': { bars: ['oracle-spec', 'builder-spec'] },
|
|
44
|
-
'solution-producer': { bars: ['architect-spec'] },
|
|
45
|
-
'spec-judge': { bars: ['oracle-spec', 'builder-spec', 'architect-spec'] },
|
|
46
|
-
'impl-producer': { bars: ['builder-impl', 'architect-impl'] },
|
|
47
|
-
'impl-judge': { bars: ['builder-impl', 'architect-impl'] },
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// The SDD-default agent per role. A null ref means the conductor runs the role
|
|
51
|
-
// INLINE in the main session (spec/solution-producer) or via a generic spawned
|
|
52
|
-
// builder (impl-producer); both are recorded produced-by sdd:sdd-automaton. The two
|
|
53
|
-
// judges are spawned cold by name.
|
|
54
|
-
export const SDD_DEFAULT_AGENT: Record<RoleKey, string | null> = {
|
|
55
|
-
'spec-producer': null,
|
|
56
|
-
'solution-producer': null,
|
|
57
|
-
'spec-judge': 'sdd-spec-judge',
|
|
58
|
-
'impl-producer': null,
|
|
59
|
-
'impl-judge': 'sdd-impl-judge',
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// ─── types ──────────────────────────────────────────────────────────────────────
|
|
63
|
-
|
|
64
|
-
// A squad serves a SET of artifact-types with one production chain. A plugin lists
|
|
65
|
-
// one or more squads (specialists-and-squads.md "Registry SHAPE"); a type appears
|
|
66
|
-
// in at most one squad per plugin.
|
|
67
|
-
export interface Squad {
|
|
68
|
-
'artifact-types': string[]
|
|
69
|
-
roles: Partial<Record<RoleKey, string | null>>
|
|
70
|
-
governances: Partial<Record<BarKey, string | null>>
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export interface RegistryEntry {
|
|
74
|
-
name: string
|
|
75
|
-
version?: string
|
|
76
|
-
squads: Squad[]
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export interface Registry {
|
|
80
|
-
'sdd-plugins': RegistryEntry[]
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// A matched squad carries its owning plugin name (for `<plugin>:<bar>` /
|
|
84
|
-
// `<plugin>-<role>` refs).
|
|
85
|
-
export interface SquadMatch {
|
|
86
|
-
plugin: string
|
|
87
|
-
squad: Squad
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export interface GovMetadata {
|
|
91
|
-
artifactType: string | null
|
|
92
|
-
actor: string | null
|
|
93
|
-
gate: string | null
|
|
94
|
-
compose: 'union' | 'replace' // parsed but NOT emitted — the agent reads it from the loaded file
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// The two project tiers a candidate can come from. project = the file's own
|
|
98
|
-
// project (most specific); project-root = the outer shared layer in a monorepo.
|
|
99
|
-
export type Tier = 'project' | 'project-root'
|
|
100
|
-
|
|
101
|
-
export interface GovCandidate extends GovMetadata {
|
|
102
|
-
tier: Tier
|
|
103
|
-
path: string // root-relative file path (direct-read ref)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// One bar's matched candidates, BUCKETED BY TIER — never ordered, never composed.
|
|
107
|
-
// project / project-root are direct-read file paths; plugin / sdd are harness-load
|
|
108
|
-
// skill refs. The agent applies precedence sdd < plugin < project-root < project.
|
|
109
|
-
export interface BarPlan {
|
|
110
|
-
key: BarKey
|
|
111
|
-
candidates: {
|
|
112
|
-
project: string[]
|
|
113
|
-
'project-root': string[]
|
|
114
|
-
plugin: string | null // <plugin>:<bar> harness-load ref, or null when no squad bar
|
|
115
|
-
sdd: string // sdd:<key>-governance harness-load ref (always present)
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export interface AgentResolution {
|
|
120
|
-
source: 'plugin' | 'sdd'
|
|
121
|
-
ref: string | null // agent name; null = inline (producer) / generic builder (impl-producer)
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export interface RolePlan {
|
|
125
|
-
role: RoleKey
|
|
126
|
-
agent: AgentResolution
|
|
127
|
-
bars: BarPlan[] // resolved-actor bars only; fixed-universal live in the role/agent def
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export interface LoadPlan {
|
|
131
|
-
artifactType: string | null
|
|
132
|
-
status: 'complete' | 'needs-input'
|
|
133
|
-
plugin: string | null
|
|
134
|
-
ambiguous: string[] // plugin names, when status is needs-input
|
|
135
|
-
roles: RolePlan[]
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// ─── registry parse + migrate-on-read ───────────────────────────────────────────
|
|
139
|
-
|
|
140
|
-
// Within a squad: rename the legacy plan-producer role key to solution-producer
|
|
141
|
-
// and expand the flat governances{oracle,builder,architect} to the Model-B
|
|
142
|
-
// (actor,gate) keys.
|
|
143
|
-
function migrateRoles(rolesIn: Record<string, string | null> | undefined): Partial<Record<RoleKey, string | null>> {
|
|
144
|
-
const roles = { ...(rolesIn ?? {}) }
|
|
145
|
-
if ('plan-producer' in roles) {
|
|
146
|
-
const legacy = roles['plan-producer']
|
|
147
|
-
if (!('solution-producer' in roles)) roles['solution-producer'] = legacy
|
|
148
|
-
delete roles['plan-producer']
|
|
149
|
-
}
|
|
150
|
-
return roles as Partial<Record<RoleKey, string | null>>
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function migrateGovernances(gIn: Record<string, string | null> | undefined): Partial<Record<BarKey, string | null>> {
|
|
154
|
-
const g = (gIn ?? {}) as Record<string, string | null>
|
|
155
|
-
const isLegacy = 'oracle' in g || 'builder' in g || 'architect' in g
|
|
156
|
-
return isLegacy
|
|
157
|
-
? {
|
|
158
|
-
'oracle-spec': g.oracle ?? null,
|
|
159
|
-
'builder-spec': g.builder ?? null,
|
|
160
|
-
'builder-impl': g.builder ?? null,
|
|
161
|
-
'architect-spec': g.architect ?? null,
|
|
162
|
-
'architect-impl': g.architect ?? null,
|
|
163
|
-
}
|
|
164
|
-
: { ...(g as Partial<Record<BarKey, string | null>>) }
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function migrateSquad(sq: Record<string, unknown>): Squad {
|
|
168
|
-
const types = (sq['artifact-types'] ?? sq.domains ?? []) as string[]
|
|
169
|
-
return {
|
|
170
|
-
'artifact-types': Array.isArray(types) ? types : [],
|
|
171
|
-
roles: migrateRoles(sq.roles as Record<string, string | null>),
|
|
172
|
-
governances: migrateGovernances(sq.governances as Record<string, string | null>),
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Migrate a registry entry on read. The legacy shape carried `domains[]` + a
|
|
177
|
-
// shared `roles{}` + `governances{}` directly on the entry; fold it into one
|
|
178
|
-
// squad. A live registry still on the legacy shape is migrated on encounter
|
|
179
|
-
// (plugin-contract-governance).
|
|
180
|
-
export function migrateEntry(entry: Record<string, unknown>): RegistryEntry {
|
|
181
|
-
const squads = Array.isArray(entry.squads)
|
|
182
|
-
? (entry.squads as Record<string, unknown>[]).map(migrateSquad)
|
|
183
|
-
: [migrateSquad({ 'artifact-types': entry.domains, roles: entry.roles, governances: entry.governances })]
|
|
184
|
-
return { name: entry.name as string, version: entry.version as string | undefined, squads }
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
export function parseRegistry(text: string): Registry {
|
|
188
|
-
let raw: unknown
|
|
189
|
-
try {
|
|
190
|
-
raw = JSON.parse(text)
|
|
191
|
-
} catch (e) {
|
|
192
|
-
throw new Error(`registry is not valid JSON: ${(e as Error).message}`)
|
|
193
|
-
}
|
|
194
|
-
if (!raw || typeof raw !== 'object' || !Array.isArray((raw as { 'sdd-plugins': unknown })['sdd-plugins']))
|
|
195
|
-
throw new Error('registry has no sdd-plugins array')
|
|
196
|
-
const entries = (raw as { 'sdd-plugins': Record<string, unknown>[] })['sdd-plugins'].map(migrateEntry)
|
|
197
|
-
return { 'sdd-plugins': entries }
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// A project anchor with no registry resolves to all-SDD-defaults — a missing file
|
|
201
|
-
// is legal, not an error.
|
|
202
|
-
export function loadRegistry(root: string): Registry {
|
|
203
|
-
const path = join(root, '.agents', 'universal-plugin.json')
|
|
204
|
-
if (!existsSync(path)) return { 'sdd-plugins': [] }
|
|
205
|
-
return parseRegistry(readFileSync(path, 'utf8'))
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// ─── artifact-type tiebreaker map (.agents/sdd/artifact-types.toml) ──────────────
|
|
209
|
-
|
|
210
|
-
// An OPTIONAL, agent-maintained lookup that records resolved path->type bindings
|
|
211
|
-
// for the ambiguities convention can't settle (the artifact-type model). It is
|
|
212
|
-
// NOT the primary classifier — convention is, and that is the conductor's
|
|
213
|
-
// judgment; this table is consulted only on a known ambiguity or a user-flagged
|
|
214
|
-
// path. A flat TOML map "<path-or-glob>" = "<artifact-type>"; most-specific glob
|
|
215
|
-
// wins. No TOML dependency — the table is intentionally flat key=value.
|
|
216
|
-
export interface TypeBinding {
|
|
217
|
-
glob: string
|
|
218
|
-
type: string
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
export function parseArtifactTypeMap(text: string): TypeBinding[] {
|
|
222
|
-
const out: TypeBinding[] = []
|
|
223
|
-
for (const raw of text.split('\n')) {
|
|
224
|
-
const line = raw.trim()
|
|
225
|
-
if (!line || line.startsWith('#') || line.startsWith('[')) continue // blanks, comments, [section] headers
|
|
226
|
-
const m = /^(?:"([^"]+)"|'([^']+)'|(\S+))\s*=\s*(?:"([^"]+)"|'([^']+)')\s*(?:#.*)?$/.exec(line)
|
|
227
|
-
if (!m) continue
|
|
228
|
-
const glob = m[1] ?? m[2] ?? m[3]
|
|
229
|
-
const type = m[4] ?? m[5]
|
|
230
|
-
if (glob && type) out.push({ glob, type })
|
|
231
|
-
}
|
|
232
|
-
return out
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Compile a glob to a full-match RegExp: ** spans path separators, * does not.
|
|
236
|
-
function globToRegExp(glob: string): RegExp {
|
|
237
|
-
const re = glob
|
|
238
|
-
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // escape regex specials (leave * ?)
|
|
239
|
-
.replace(/\*+/g, (m) => (m.length > 1 ? '.*' : '[^/]*')) // ** spans separators, * does not
|
|
240
|
-
.replace(/\?/g, '.')
|
|
241
|
-
return new RegExp(`^${re}$`)
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// Resolve a file path to an artifact-type via the tiebreaker map: the
|
|
245
|
-
// most-specific matching glob wins (most literal characters, then longest
|
|
246
|
-
// pattern). Returns null when nothing matches — the caller falls back to
|
|
247
|
-
// convention.
|
|
248
|
-
export function resolveArtifactTypeFromMap(map: TypeBinding[], path: string): string | null {
|
|
249
|
-
const matches = map.filter((b) => globToRegExp(b.glob).test(path))
|
|
250
|
-
if (matches.length === 0) return null
|
|
251
|
-
const literal = (g: string) => g.replace(/[*?]/g, '').length
|
|
252
|
-
matches.sort((a, b) => literal(b.glob) - literal(a.glob) || b.glob.length - a.glob.length)
|
|
253
|
-
return matches[0].type
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// Load the optional tiebreaker map. A missing file is legal (most projects need
|
|
257
|
-
// none) — returns an empty table.
|
|
258
|
-
export function loadArtifactTypeMap(root: string): TypeBinding[] {
|
|
259
|
-
const path = join(root, '.agents', 'sdd', 'artifact-types.toml')
|
|
260
|
-
if (!existsSync(path)) return []
|
|
261
|
-
return parseArtifactTypeMap(readFileSync(path, 'utf8'))
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// ─── project governance discovery ───────────────────────────────────────────────
|
|
265
|
-
|
|
266
|
-
function frontmatter(text: string): string[] {
|
|
267
|
-
const m = /^---\n([\s\S]*?)\n---/.exec(text)
|
|
268
|
-
return m ? m[1].split('\n') : []
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const unquote = (s: string) => s.trim().replace(/^["']|["']$/g, '')
|
|
272
|
-
|
|
273
|
-
// Read metadata{artifact-type?, actor, gate, compose} from a governance file's
|
|
274
|
-
// frontmatter. Returns null when there is no metadata block with an actor+gate —
|
|
275
|
-
// a plain doc that is not a resolvable bar.
|
|
276
|
-
export function parseGovernanceFrontmatter(text: string): GovMetadata | null {
|
|
277
|
-
const lines = frontmatter(text)
|
|
278
|
-
let inMeta = false
|
|
279
|
-
let artifactType: string | null = null
|
|
280
|
-
let actor: string | null = null
|
|
281
|
-
let gate: string | null = null
|
|
282
|
-
let compose: 'union' | 'replace' = 'union'
|
|
283
|
-
|
|
284
|
-
for (const line of lines) {
|
|
285
|
-
if (/^metadata:\s*$/.test(line)) {
|
|
286
|
-
inMeta = true
|
|
287
|
-
continue
|
|
288
|
-
}
|
|
289
|
-
if (inMeta && /^\S/.test(line)) inMeta = false // dedent ends the block
|
|
290
|
-
if (!inMeta) continue
|
|
291
|
-
const at = /^\s+artifact-type:\s*(.+)$/.exec(line)
|
|
292
|
-
if (at) artifactType = unquote(at[1])
|
|
293
|
-
const ac = /^\s+actor:\s*(.+)$/.exec(line)
|
|
294
|
-
if (ac) actor = unquote(ac[1])
|
|
295
|
-
const ga = /^\s+gate:\s*(.+)$/.exec(line)
|
|
296
|
-
if (ga) gate = unquote(ga[1])
|
|
297
|
-
const co = /^\s+compose:\s*(.+)$/.exec(line)
|
|
298
|
-
if (co) compose = unquote(co[1]) === 'replace' ? 'replace' : 'union'
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
if (!actor || !gate) return null
|
|
302
|
-
return { artifactType, actor, gate, compose }
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// Collect project governance candidates from the CALLER-PASSED anchors — never a
|
|
306
|
-
// tree walk. Each anchor names a tier and a root; the candidate carries its tier
|
|
307
|
-
// (for the agent's precedence) and its root-relative path (the direct-read ref).
|
|
308
|
-
// A missing `.agents/governances/` at an anchor contributes nothing. Both .md and
|
|
309
|
-
// SKILL.md are read in place — no build.
|
|
310
|
-
export function collectAnchorGovernances(anchors: { tier: Tier; root: string }[]): GovCandidate[] {
|
|
311
|
-
const out: GovCandidate[] = []
|
|
312
|
-
for (const { tier, root } of anchors) {
|
|
313
|
-
const dir = join(root, '.agents', 'governances')
|
|
314
|
-
let entries: Dirent[]
|
|
315
|
-
try {
|
|
316
|
-
entries = readdirSync(dir, { withFileTypes: true })
|
|
317
|
-
} catch {
|
|
318
|
-
continue
|
|
319
|
-
}
|
|
320
|
-
for (const e of entries) {
|
|
321
|
-
if (!e.isFile() || !e.name.endsWith('.md')) continue
|
|
322
|
-
const meta = parseGovernanceFrontmatter(readFileSync(join(dir, e.name), 'utf8'))
|
|
323
|
-
if (meta) out.push({ ...meta, tier, path: join(root, '.agents', 'governances', e.name) })
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
return out
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// ─── plugin matching ────────────────────────────────────────────────────────────
|
|
330
|
-
|
|
331
|
-
// Match an artifact-type against each plugin's squads: the squad whose
|
|
332
|
-
// artifact-types contains it serves the file. Zero matches → all SDD defaults;
|
|
333
|
-
// one → that squad; two or more distinct plugins → ambiguous (the conductor
|
|
334
|
-
// consults the contested-type choice or asks).
|
|
335
|
-
export function matchSquad(
|
|
336
|
-
registry: Registry,
|
|
337
|
-
artifactType: string | null,
|
|
338
|
-
): { match: SquadMatch | null; ambiguous: string[] } {
|
|
339
|
-
if (!artifactType) return { match: null, ambiguous: [] }
|
|
340
|
-
const matches: SquadMatch[] = []
|
|
341
|
-
for (const p of registry['sdd-plugins'])
|
|
342
|
-
for (const sq of p.squads ?? [])
|
|
343
|
-
if (sq['artifact-types']?.includes(artifactType)) matches.push({ plugin: p.name, squad: sq })
|
|
344
|
-
if (matches.length === 0) return { match: null, ambiguous: [] }
|
|
345
|
-
if (matches.length === 1) return { match: matches[0], ambiguous: [] }
|
|
346
|
-
return { match: null, ambiguous: [...new Set(matches.map((m) => m.plugin))] }
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// ─── bar matching ───────────────────────────────────────────────────────────────
|
|
350
|
-
|
|
351
|
-
// Match one (actor, gate) bar across the sources and return the candidates
|
|
352
|
-
// BUCKETED BY TIER — no ordering, no compose collapse. A project candidate matches
|
|
353
|
-
// when its frontmatter (actor, gate) matches and its artifact-type is the file's
|
|
354
|
-
// type OR typeless (a typeless project bar applies to every type). The agent reads
|
|
355
|
-
// each candidate's own `compose` at load time and composes by precedence
|
|
356
|
-
// (sdd-default < plugin < project-root < project).
|
|
357
|
-
export function matchBar(
|
|
358
|
-
artifactType: string | null,
|
|
359
|
-
actor: string,
|
|
360
|
-
gate: string,
|
|
361
|
-
ctx: { match: SquadMatch | null; projectGovs: GovCandidate[] },
|
|
362
|
-
): BarPlan {
|
|
363
|
-
const key = `${actor}-${gate}` as BarKey
|
|
364
|
-
const bucket = (tier: Tier) =>
|
|
365
|
-
ctx.projectGovs
|
|
366
|
-
.filter((c) => c.tier === tier && c.actor === actor && c.gate === gate)
|
|
367
|
-
.filter((c) => c.artifactType === artifactType || c.artifactType === null)
|
|
368
|
-
.map((c) => c.path)
|
|
369
|
-
|
|
370
|
-
const pluginBar = ctx.match?.squad.governances?.[key]
|
|
371
|
-
return {
|
|
372
|
-
key,
|
|
373
|
-
candidates: {
|
|
374
|
-
project: bucket('project'),
|
|
375
|
-
'project-root': bucket('project-root'),
|
|
376
|
-
plugin: ctx.match && pluginBar ? `${ctx.match.plugin}:${pluginBar}` : null,
|
|
377
|
-
sdd: `sdd:${key}-governance`,
|
|
378
|
-
},
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// ─── role + plan resolution ─────────────────────────────────────────────────────
|
|
383
|
-
|
|
384
|
-
// Resolve the agent that runs a role: a named plugin delegate, the SDD default,
|
|
385
|
-
// or — for a present-but-missing role key — the <plugin>-<role> convention.
|
|
386
|
-
export function resolveAgent(role: RoleKey, match: SquadMatch | null): AgentResolution {
|
|
387
|
-
if (!match) return { source: 'sdd', ref: SDD_DEFAULT_AGENT[role] }
|
|
388
|
-
if (role in match.squad.roles) {
|
|
389
|
-
const named = match.squad.roles[role]
|
|
390
|
-
if (named) return { source: 'plugin', ref: named }
|
|
391
|
-
return { source: 'sdd', ref: SDD_DEFAULT_AGENT[role] } // explicit null = SDD default
|
|
392
|
-
}
|
|
393
|
-
return { source: 'plugin', ref: `${match.plugin}-${role}` } // omitted = convention
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
export function resolveRole(
|
|
397
|
-
role: RoleKey,
|
|
398
|
-
artifactType: string | null,
|
|
399
|
-
ctx: { match: SquadMatch | null; projectGovs: GovCandidate[] },
|
|
400
|
-
): RolePlan {
|
|
401
|
-
const bars = ROLE_LOADOUT[role].bars.map((b) => {
|
|
402
|
-
const [actor, gate] = b.split('-')
|
|
403
|
-
return matchBar(artifactType, actor, gate, ctx)
|
|
404
|
-
})
|
|
405
|
-
return { role, agent: resolveAgent(role, ctx.match), bars }
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
export function buildLoadPlan(artifactType: string | null, registry: Registry, projectGovs: GovCandidate[]): LoadPlan {
|
|
409
|
-
const { match, ambiguous } = matchSquad(registry, artifactType)
|
|
410
|
-
if (ambiguous.length > 0)
|
|
411
|
-
return {
|
|
412
|
-
artifactType,
|
|
413
|
-
status: 'needs-input',
|
|
414
|
-
plugin: null,
|
|
415
|
-
ambiguous,
|
|
416
|
-
roles: [],
|
|
417
|
-
}
|
|
418
|
-
const ctx = { match, projectGovs }
|
|
419
|
-
return {
|
|
420
|
-
artifactType,
|
|
421
|
-
status: 'complete',
|
|
422
|
-
plugin: match?.plugin ?? null,
|
|
423
|
-
ambiguous: [],
|
|
424
|
-
roles: ROLE_KEYS.map((r) => resolveRole(r, artifactType, ctx)),
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
// ─── registry structural validation (the no-artifact-type CLI gate) ─────────────
|
|
429
|
-
|
|
430
|
-
// Validate the registry is well-formed and unambiguous: known role/governance
|
|
431
|
-
// keys, a type in at most one squad per plugin, and no artifact-type claimed by
|
|
432
|
-
// two plugins (which would force every spec of that type into needs-input).
|
|
433
|
-
export function validateRegistry(registry: Registry): string[] {
|
|
434
|
-
const v: string[] = []
|
|
435
|
-
const typeOwners = new Map<string, string[]>()
|
|
436
|
-
for (const entry of registry['sdd-plugins']) {
|
|
437
|
-
const where = entry.name || '<unnamed plugin>'
|
|
438
|
-
if (!entry.name) v.push('an sdd-plugins entry has no name')
|
|
439
|
-
if (!Array.isArray(entry.squads)) {
|
|
440
|
-
v.push(`${where}: squads is not an array`)
|
|
441
|
-
continue
|
|
442
|
-
}
|
|
443
|
-
const seenTypes = new Set<string>()
|
|
444
|
-
for (const sq of entry.squads) {
|
|
445
|
-
if (!Array.isArray(sq['artifact-types'])) v.push(`${where}: a squad's artifact-types is not an array`)
|
|
446
|
-
for (const r of Object.keys(sq.roles ?? {}))
|
|
447
|
-
if (!ROLE_KEYS.includes(r as RoleKey)) v.push(`${where}: unknown role key "${r}"`)
|
|
448
|
-
for (const k of Object.keys(sq.governances ?? {}))
|
|
449
|
-
if (!BAR_KEYS.includes(k as BarKey)) v.push(`${where}: unknown governance key "${k}"`)
|
|
450
|
-
for (const t of sq['artifact-types'] ?? []) {
|
|
451
|
-
if (seenTypes.has(t)) v.push(`${where}: artifact-type "${t}" appears in more than one squad`)
|
|
452
|
-
seenTypes.add(t)
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
for (const t of seenTypes) typeOwners.set(t, [...(typeOwners.get(t) ?? []), where])
|
|
456
|
-
}
|
|
457
|
-
for (const [t, owners] of typeOwners)
|
|
458
|
-
if (owners.length > 1)
|
|
459
|
-
v.push(`artifact-type "${t}" is claimed by ${owners.length} plugins (${owners.join(', ')}) — ambiguous`)
|
|
460
|
-
return v
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
// ─── CLI entry ──────────────────────────────────────────────────────────────────
|
|
464
|
-
|
|
465
|
-
export function main(argv: string[]): number {
|
|
466
|
-
const root = argv.includes('--root') ? argv[argv.indexOf('--root') + 1] : '.'
|
|
467
|
-
const explicitType = argv.includes('--artifact-type') ? argv[argv.indexOf('--artifact-type') + 1] : null
|
|
468
|
-
const pathArg = argv.includes('--path') ? argv[argv.indexOf('--path') + 1] : null
|
|
469
|
-
// Caller-passed project anchors. --project defaults to --root (single-project);
|
|
470
|
-
// --project-root is the outer shared layer in a monorepo (omitted otherwise).
|
|
471
|
-
const projectArg = argv.includes('--project') ? argv[argv.indexOf('--project') + 1] : root
|
|
472
|
-
const projectRootArg = argv.includes('--project-root') ? argv[argv.indexOf('--project-root') + 1] : null
|
|
473
|
-
const anchors: { tier: Tier; root: string }[] = [{ tier: 'project', root: projectArg }]
|
|
474
|
-
if (projectRootArg) anchors.push({ tier: 'project-root', root: projectRootArg })
|
|
475
|
-
|
|
476
|
-
// --artifact-type wins (explicit override); else --path consults the optional
|
|
477
|
-
// tiebreaker map (most-specific glob). A --path with no map match stays null —
|
|
478
|
-
// the caller falls back to convention, it is not a registry-validation request.
|
|
479
|
-
let artifactType = explicitType
|
|
480
|
-
if (!artifactType && pathArg) artifactType = resolveArtifactTypeFromMap(loadArtifactTypeMap(root), pathArg)
|
|
481
|
-
|
|
482
|
-
let registry: Registry
|
|
483
|
-
try {
|
|
484
|
-
registry = loadRegistry(root)
|
|
485
|
-
} catch (e) {
|
|
486
|
-
console.error(`✗ ${(e as Error).message}`)
|
|
487
|
-
return 1
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
if (!artifactType && pathArg) {
|
|
491
|
-
process.stdout.write(
|
|
492
|
-
`${JSON.stringify({ path: pathArg, artifactType: null, note: 'no tiebreaker match — classify by convention' }, null, 2)}\n`,
|
|
493
|
-
)
|
|
494
|
-
return 0
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
// No artifact-type and no path → validate the registry is well-formed + unambiguous.
|
|
498
|
-
if (!artifactType) {
|
|
499
|
-
const violations = validateRegistry(registry)
|
|
500
|
-
if (violations.length) {
|
|
501
|
-
for (const line of violations) console.error(`✗ ${line}`)
|
|
502
|
-
return 1
|
|
503
|
-
}
|
|
504
|
-
process.stdout.write('governance registry OK\n')
|
|
505
|
-
return 0
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
// With an artifact-type → emit the per-role plan (agent + tier-bucketed bars).
|
|
509
|
-
const plan = buildLoadPlan(artifactType, registry, collectAnchorGovernances(anchors))
|
|
510
|
-
process.stdout.write(`${JSON.stringify(plan, null, 2)}\n`)
|
|
511
|
-
if (plan.status === 'needs-input')
|
|
512
|
-
console.error(`needs-input: artifact-type "${artifactType}" is claimed by ${plan.ambiguous.join(', ')}`)
|
|
513
|
-
return 0
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
|
|
517
|
-
process.exit(main(process.argv.slice(2)))
|
|
518
|
-
}
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { pathToFileURL } from "node:url";
|
|
4
|
-
//#region skills/resolve-tracking/scripts/resolve-tracking.mts
|
|
5
|
-
function globToRegExpBody(glob) {
|
|
6
|
-
return glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*+/g, (m) => m.length > 1 ? ".*" : "[^/]*").replace(/\?/g, "[^/]");
|
|
7
|
-
}
|
|
8
|
-
function parseIgnoreLine(raw) {
|
|
9
|
-
const trimmed = raw.replace(/\r$/, "").trim();
|
|
10
|
-
if (!trimmed || trimmed.startsWith("#")) return "skip";
|
|
11
|
-
let body = trimmed;
|
|
12
|
-
const negated = body.startsWith("!");
|
|
13
|
-
if (negated) body = body.slice(1);
|
|
14
|
-
if (body.endsWith("/")) body = body.slice(0, -1);
|
|
15
|
-
let anchored = false;
|
|
16
|
-
if (body.startsWith("/")) {
|
|
17
|
-
anchored = true;
|
|
18
|
-
body = body.slice(1);
|
|
19
|
-
} else if (body.includes("/")) anchored = true;
|
|
20
|
-
if (body === "") return null;
|
|
21
|
-
const prefix = anchored ? "^" : "^(?:.*/)?";
|
|
22
|
-
const core = globToRegExpBody(body);
|
|
23
|
-
const suffix = "(?:/.*)?$";
|
|
24
|
-
try {
|
|
25
|
-
return {
|
|
26
|
-
negated,
|
|
27
|
-
regex: new RegExp(prefix + core + suffix),
|
|
28
|
-
source: trimmed
|
|
29
|
-
};
|
|
30
|
-
} catch {
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
function parseIgnoreFile(text) {
|
|
35
|
-
const out = [];
|
|
36
|
-
for (const raw of text.split("\n")) {
|
|
37
|
-
const rule = parseIgnoreLine(raw);
|
|
38
|
-
if (rule && rule !== "skip") out.push(rule);
|
|
39
|
-
}
|
|
40
|
-
return out;
|
|
41
|
-
}
|
|
42
|
-
function loadIgnoreRules(root) {
|
|
43
|
-
const path = join(root, ".agents", "sdd", ".sddignore");
|
|
44
|
-
if (!existsSync(path)) return [];
|
|
45
|
-
return parseIgnoreFile(readFileSync(path, "utf8"));
|
|
46
|
-
}
|
|
47
|
-
function resolveFromIgnore(rules, path) {
|
|
48
|
-
let value = null;
|
|
49
|
-
for (const rule of rules) if (rule.regex.test(path)) value = rule.negated ? "tracked" : "ignored";
|
|
50
|
-
return value;
|
|
51
|
-
}
|
|
52
|
-
const KIND_DEFAULT_GLOBS = {
|
|
53
|
-
skill: {
|
|
54
|
-
ignored: [".agents/skills/**"],
|
|
55
|
-
tracked: [
|
|
56
|
-
"skills/**",
|
|
57
|
-
"plugins/*/skills/**",
|
|
58
|
-
"packages/*/skills/**"
|
|
59
|
-
]
|
|
60
|
-
},
|
|
61
|
-
subagent: {
|
|
62
|
-
ignored: [".agents/agents/**"],
|
|
63
|
-
tracked: ["plugins/*/agents/**", "packages/*/agents/**"]
|
|
64
|
-
},
|
|
65
|
-
command: {
|
|
66
|
-
ignored: [".agents/commands/**"],
|
|
67
|
-
tracked: ["plugins/*/commands/**", "packages/*/commands/**"]
|
|
68
|
-
}
|
|
69
|
-
};
|
|
70
|
-
function globMatch(glob, path) {
|
|
71
|
-
return new RegExp(`^${globToRegExpBody(glob)}$`).test(path);
|
|
72
|
-
}
|
|
73
|
-
function resolveKindDefault(artifactType, path) {
|
|
74
|
-
if (!artifactType) return null;
|
|
75
|
-
const kind = KIND_DEFAULT_GLOBS[artifactType];
|
|
76
|
-
if (!kind) return null;
|
|
77
|
-
if (kind.ignored.some((g) => globMatch(g, path))) return "ignored";
|
|
78
|
-
if (kind.tracked.some((g) => globMatch(g, path))) return "tracked";
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
function resolveTracking(input) {
|
|
82
|
-
if (input.explicit) return {
|
|
83
|
-
value: input.explicit,
|
|
84
|
-
reason: "explicit override"
|
|
85
|
-
};
|
|
86
|
-
const ignoreMatch = resolveFromIgnore(loadIgnoreRules(input.root), input.path);
|
|
87
|
-
if (ignoreMatch) return {
|
|
88
|
-
value: ignoreMatch,
|
|
89
|
-
reason: ".sddignore"
|
|
90
|
-
};
|
|
91
|
-
const kindDefault = resolveKindDefault(input.artifactType, input.path);
|
|
92
|
-
if (kindDefault) return {
|
|
93
|
-
value: kindDefault,
|
|
94
|
-
reason: `kind-default (${input.artifactType})`
|
|
95
|
-
};
|
|
96
|
-
return {
|
|
97
|
-
value: "tracked",
|
|
98
|
-
reason: "fail-closed (no signal)"
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
function validateIgnoreFile(root) {
|
|
102
|
-
const path = join(root, ".agents", "sdd", ".sddignore");
|
|
103
|
-
if (!existsSync(path)) return {
|
|
104
|
-
ok: true,
|
|
105
|
-
message: ".sddignore OK (absent, fine)"
|
|
106
|
-
};
|
|
107
|
-
const text = readFileSync(path, "utf8");
|
|
108
|
-
const notes = [];
|
|
109
|
-
text.split("\n").forEach((raw, i) => {
|
|
110
|
-
if (parseIgnoreLine(raw) === null) notes.push(`line ${i + 1}: not a valid gitignore pattern: ${raw.trim()}`);
|
|
111
|
-
});
|
|
112
|
-
if (notes.length > 0) return {
|
|
113
|
-
ok: false,
|
|
114
|
-
message: notes.join("\n")
|
|
115
|
-
};
|
|
116
|
-
return {
|
|
117
|
-
ok: true,
|
|
118
|
-
message: `.sddignore OK (${parseIgnoreFile(text).length} rule(s))`
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
function parseArgs(argv) {
|
|
122
|
-
const out = {};
|
|
123
|
-
for (let i = 0; i < argv.length; i++) if (argv[i].startsWith("--")) {
|
|
124
|
-
out[argv[i].slice(2)] = argv[i + 1];
|
|
125
|
-
i++;
|
|
126
|
-
}
|
|
127
|
-
return out;
|
|
128
|
-
}
|
|
129
|
-
function main(argv) {
|
|
130
|
-
const args = parseArgs(argv);
|
|
131
|
-
const root = args.root ?? ".";
|
|
132
|
-
if (!args.path) {
|
|
133
|
-
const result = validateIgnoreFile(root);
|
|
134
|
-
process.stdout.write(`${result.message}\n`);
|
|
135
|
-
if (!result.ok) process.exitCode = 1;
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
const explicit = args.explicit === "tracked" || args.explicit === "ignored" ? args.explicit : void 0;
|
|
139
|
-
const result = resolveTracking({
|
|
140
|
-
root,
|
|
141
|
-
path: args.path,
|
|
142
|
-
artifactType: args["artifact-type"],
|
|
143
|
-
explicit
|
|
144
|
-
});
|
|
145
|
-
process.stdout.write(`${result.value}\n`);
|
|
146
|
-
process.stdout.write(`reason: ${result.reason}\n`);
|
|
147
|
-
}
|
|
148
|
-
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) main(process.argv.slice(2));
|
|
149
|
-
//#endregion
|
|
150
|
-
export { loadIgnoreRules, main, parseIgnoreFile, parseIgnoreLine, resolveFromIgnore, resolveKindDefault, resolveTracking, validateIgnoreFile };
|