cyber-sdd 0.5.0 → 0.6.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.
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "homepage": "https://github.com/cyberuni/cyber-sdd",
9
9
  "repository": "https://github.com/cyberuni/cyber-sdd",
10
- "version": "0.5.0",
10
+ "version": "0.6.0",
11
11
  "skills": "./skills",
12
12
  "agents": [
13
13
  "./agents/sdd-automaton.md",
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "homepage": "https://github.com/cyberuni/cyber-sdd",
9
9
  "repository": "https://github.com/cyberuni/cyber-sdd",
10
- "version": "0.5.0",
10
+ "version": "0.6.0",
11
11
  "skills": "./skills",
12
12
  "agents": [
13
13
  "./agents/sdd-automaton.md",
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "homepage": "https://github.com/cyberuni/cyber-sdd",
9
9
  "repository": "https://github.com/cyberuni/cyber-sdd",
10
- "version": "0.5.0",
10
+ "version": "0.6.0",
11
11
  "skills": "./skills",
12
12
  "agents": [
13
13
  "./agents/sdd-automaton.md",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cyber-sdd",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Spec-Driven Development. Scaffold, validate, and maintain behavioral specs (spec.md + .feature files) for software features.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -21,6 +21,11 @@ Every `plugins/<plugin>/skills/**/SKILL.md` and `plugins/<plugin>/agents/*.md`,
21
21
  - **unexplained** — a block declares a field that carries no gloss and that the prose never names.
22
22
  - **undeclared** — the prose names a known field in a code span, and none of the file's blocks
23
23
  declare it.
24
+ - **miscased** — the prose names, in a code span, a field this file's block declares, but spelled
25
+ in the other case (`governances_loaded` against `GOVERNANCES_LOADED`).
26
+
27
+ **A field token** is `UPPER_CASE` or `snake_case`; the two spellings name the same field. A lowercase
28
+ word needs an underscore to be a token, so ordinary prose (`owner`) is never a field.
24
29
 
25
30
  **A structured block** is a fenced code block with no info string (or `text`). A **declaration** is a
26
31
  block line opening with field tokens then a colon (`STATUS: complete | blocked`), a comma list of two
@@ -7,11 +7,17 @@
7
7
  // engine diffs the token sets (spec: .agents/specs/sdd/plugin/check-field-mandates/):
8
8
  // unexplained — a block declares a field that carries no gloss and the prose never names
9
9
  // undeclared — the prose mandates a known field (a code span) that none of the file's blocks declare
10
+ // miscased — the prose mandates a field this file declares, but spelled in the other case
10
11
  //
11
12
  // A field is explained by a gloss OR by the prose — requiring the prose to repeat a glossed
12
13
  // declaration would demand filler in every definition. A mandate is a code span naming a KNOWN field
13
14
  // (declared somewhere in the tree); that vocabulary is what separates a field from `TODO`.
14
15
  //
16
+ // A field token is UPPER_CASE or snake_case. A lowercase word needs an underscore to be a token, so
17
+ // ordinary prose words (`owner`, `summary`) never become fields. Case decides whether two tokens are the
18
+ // same field only for the miscased finding: `governances_loaded` and `GOVERNANCES_LOADED` are one
19
+ // field, spelled two ways, and a file that does both is reported.
20
+ //
15
21
  // Pure functions are exported for node:test; running the file directly drives the CLI. No
16
22
  // dependencies (the repo's node-≥23.6 / no-deps convention).
17
23
 
@@ -19,7 +25,7 @@ import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from 'n
19
25
  import { join, relative, sep } from 'node:path'
20
26
  import { pathToFileURL } from 'node:url'
21
27
 
22
- export type FindingKind = 'unexplained' | 'undeclared'
28
+ export type FindingKind = 'unexplained' | 'undeclared' | 'miscased'
23
29
 
24
30
  export interface Finding {
25
31
  kind: FindingKind
@@ -28,6 +34,8 @@ export interface Finding {
28
34
  /** 1-based line: the declaration for `unexplained`, the prose line for `undeclared`. */
29
35
  line: number
30
36
  token: string
37
+ /** For `miscased`: the spelling this file's declaration uses. */
38
+ declared?: string
31
39
  }
32
40
 
33
41
  interface Declaration {
@@ -48,7 +56,8 @@ export interface ParsedDefinition {
48
56
  proseWords: Set<string>
49
57
  }
50
58
 
51
- const TOKEN = '[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*'
59
+ /** UPPER_CASE, or snake_case with at least one underscore — a bare lowercase word is prose. */
60
+ const TOKEN = '(?:[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*|[a-z][a-z0-9]*(?:_[a-z0-9]+)+)'
52
61
  const TOKEN_ITEM = `${TOKEN}(?:\\(s\\))?`
53
62
  /** A block line opening with a comma list of tokens, then a colon, a trailing comment, or nothing. */
54
63
  const BLOCK_DECLARATION_RE = new RegExp(`^\\s*(${TOKEN_ITEM}(?:\\s*,\\s*${TOKEN_ITEM})*)\\s*(:.*|#.*)?$`)
@@ -67,15 +76,27 @@ function isField(token: string): boolean {
67
76
  return token.length >= 2
68
77
  }
69
78
 
70
- /** The forms a token may take and still be the same field — equal, or one trailing `S` apart. */
79
+ /** A token's case-folded form: the spelling-independent name of the field. */
80
+ function fold(token: string): string {
81
+ return token.toUpperCase()
82
+ }
83
+
84
+ /** The folded forms a token may take and still be the same field — equal, or one trailing `S` apart. */
71
85
  function sameFieldForms(token: string): string[] {
72
- const forms = [token, `${token}S`]
73
- if (token.endsWith('S') && token.length > 2) forms.push(token.slice(0, -1))
86
+ const t = fold(token)
87
+ const forms = [t, `${t}S`]
88
+ if (t.endsWith('S') && t.length > 2) forms.push(t.slice(0, -1))
74
89
  return forms
75
90
  }
76
91
 
77
- function hasSameField(set: Set<string>, token: string): boolean {
78
- return sameFieldForms(token).some((f) => set.has(f))
92
+ /** Does the set of folded tokens hold the same field as `token`? */
93
+ function hasSameField(folded: Set<string>, token: string): boolean {
94
+ return sameFieldForms(token).some((f) => folded.has(f))
95
+ }
96
+
97
+ /** Is the token spelled lowercase (snake_case) rather than uppercase? */
98
+ function isLower(token: string): boolean {
99
+ return token !== fold(token)
79
100
  }
80
101
 
81
102
  // ── Parse ──
@@ -212,30 +233,40 @@ export function discoverDefinitions(root: string): string[] {
212
233
 
213
234
  export function check(parsed: Map<string, ParsedDefinition>): Finding[] {
214
235
  const known = new Set<string>()
215
- for (const p of parsed.values()) for (const d of p.declarations) known.add(d.token)
236
+ for (const p of parsed.values()) for (const d of p.declarations) known.add(fold(d.token))
216
237
 
217
238
  const findings: Finding[] = []
218
239
  for (const [file, p] of parsed) {
219
240
  if (p.declarations.length === 0) continue
220
- const declared = new Set(p.declarations.map((d) => d.token))
241
+ const declared = new Set(p.declarations.map((d) => fold(d.token)))
242
+ const proseWords = new Set([...p.proseWords].map(fold))
221
243
 
222
244
  const seen = new Set<string>()
223
245
  for (const d of p.declarations) {
224
246
  if (seen.has(d.token)) continue
225
247
  seen.add(d.token)
226
248
  const glossed = p.declarations.some((o) => o.token === d.token && o.glossed)
227
- if (!glossed && !hasSameField(p.proseWords, d.token)) {
249
+ if (!glossed && !hasSameField(proseWords, d.token)) {
228
250
  findings.push({ kind: 'unexplained', file, line: d.line, token: d.token })
229
251
  }
230
252
  }
231
253
 
232
254
  const reported = new Set<string>()
233
255
  for (const m of p.mandates) {
234
- if (!hasSameField(known, m.token) || hasSameField(declared, m.token)) continue
256
+ if (!hasSameField(known, m.token)) continue
235
257
  const key = `${m.line}:${m.token}`
236
258
  if (reported.has(key)) continue
259
+ if (!hasSameField(declared, m.token)) {
260
+ reported.add(key)
261
+ findings.push({ kind: 'undeclared', file, line: m.line, token: m.token })
262
+ continue
263
+ }
264
+ // Declared here — but is any same-field declaration spelled in the mandate's case?
265
+ const forms = sameFieldForms(m.token)
266
+ const same = p.declarations.filter((d) => forms.includes(fold(d.token)))
267
+ if (same.some((d) => isLower(d.token) === isLower(m.token))) continue
237
268
  reported.add(key)
238
- findings.push({ kind: 'undeclared', file, line: m.line, token: m.token })
269
+ findings.push({ kind: 'miscased', file, line: m.line, token: m.token, declared: same[0]?.token })
239
270
  }
240
271
  }
241
272
  return findings.sort((a, b) => (a.file === b.file ? a.line - b.line : a.file < b.file ? -1 : 1))
@@ -246,13 +277,17 @@ export function check(parsed: Map<string, ParsedDefinition>): Finding[] {
246
277
  const DETAIL: Record<FindingKind, string> = {
247
278
  unexplained: 'declared in a block, but no gloss and no prose explains it',
248
279
  undeclared: 'mandated in the prose, but no block in this file declares it',
280
+ miscased: 'mandated in the prose, but this file declares it in the other case',
249
281
  }
250
282
 
251
283
  export function formatReport(definitions: string[], findings: Finding[]): string {
252
284
  if (definitions.length === 0) return 'check-field-mandates: no skill or agent definition found\n'
253
285
  if (findings.length === 0) return `check-field-mandates: ${definitions.length} definition(s) OK\n`
254
286
  const rows = findings
255
- .map((f) => ` ${f.kind.padEnd(11)} ${f.file}:${f.line} ${f.token} — ${DETAIL[f.kind]}`)
287
+ .map(
288
+ (f) =>
289
+ ` ${f.kind.padEnd(11)} ${f.file}:${f.line} ${f.token} — ${DETAIL[f.kind]}${f.declared ? ` (${f.declared})` : ''}`,
290
+ )
256
291
  .join('\n')
257
292
  return `${rows}\ncheck-field-mandates: ${findings.length} finding(s) across ${definitions.length} definition(s)\n`
258
293
  }
@@ -83,7 +83,7 @@ REMEDIATION: <per finding answered: verdict, rule, swept, ruled-out, prove
83
83
  STATUS: complete | needs-input | blocked
84
84
  SCENARIOS_WRITTEN: <count>
85
85
  NOTES: <what was written / revised>
86
- GOVERNANCES_LOADED: [ every governance name loaded before writing — required, [] when none, never written into spec.md or the .feature ]
86
+ governances_loaded: [ every governance name loaded before writing — required, [] when none, never written into spec.md or the .feature ]
87
87
  QUESTIONS: [ batched, when needs-input ]
88
88
  CONTENT_GAPS: [ { artifact, location, gap } ] # become <!-- open: --> markers
89
89
  OBSERVATIONS: [ { owner: architect | strategist, note, evidence } ]