@warnyin/sdlc 0.5.0 → 0.5.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.
Files changed (61) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/LICENSE +21 -21
  3. package/bin/cli.mjs +12 -1
  4. package/lib/caps.mjs +45 -45
  5. package/lib/config.mjs +41 -41
  6. package/lib/delta.mjs +227 -160
  7. package/lib/frontmatter.mjs +59 -59
  8. package/lib/glob.mjs +29 -29
  9. package/lib/manifest.mjs +99 -99
  10. package/lib/settings-merge.mjs +63 -63
  11. package/lib/usage.mjs +49 -46
  12. package/lib/validate.mjs +196 -186
  13. package/package.json +42 -42
  14. package/payload/adapters/agents-md.md +8 -8
  15. package/payload/adapters/claude/agents/sdlc-architect.md +12 -12
  16. package/payload/adapters/claude/agents/sdlc-builder.md +14 -14
  17. package/payload/adapters/claude/agents/sdlc-contractor.md +13 -13
  18. package/payload/adapters/claude/agents/sdlc-evaluator.md +13 -13
  19. package/payload/adapters/claude/agents/sdlc-learner.md +16 -16
  20. package/payload/adapters/claude/agents/sdlc-ops.md +11 -11
  21. package/payload/adapters/claude/agents/sdlc-quality.md +13 -13
  22. package/payload/adapters/claude/agents/sdlc-security.md +12 -12
  23. package/payload/adapters/claude/commands/sdlc/converge.md +5 -5
  24. package/payload/adapters/claude/commands/sdlc/init.md +4 -4
  25. package/payload/adapters/claude/commands/sdlc/next.md +4 -4
  26. package/payload/adapters/claude/commands/sdlc/observe.md +4 -4
  27. package/payload/adapters/claude/commands/sdlc/steer.md +4 -4
  28. package/payload/adapters/claude/skills/contract-writing/SKILL.md +26 -26
  29. package/payload/adapters/claude/skills/delta-spec-format/SKILL.md +36 -33
  30. package/payload/adapters/claude/skills/sdlc-conventions/SKILL.md +26 -26
  31. package/payload/adapters/cline.md +8 -8
  32. package/payload/adapters/copilot.md +8 -8
  33. package/payload/adapters/cursor.mdc +7 -7
  34. package/payload/adapters/gemini.md +8 -8
  35. package/payload/adapters/windsurf.md +4 -4
  36. package/payload/hooks/_shared.mjs +154 -154
  37. package/payload/hooks/guard-writes.mjs +83 -83
  38. package/payload/hooks/inject-context.mjs +55 -55
  39. package/payload/hooks/journal.mjs +58 -58
  40. package/payload/hooks/session-summary.mjs +50 -50
  41. package/payload/hooks/validate-artifact.mjs +80 -80
  42. package/payload/playbook/context.md +26 -26
  43. package/payload/playbook/converge.md +19 -19
  44. package/payload/playbook/init.md +22 -22
  45. package/payload/playbook/observe.md +20 -20
  46. package/payload/playbook/principles.md +28 -28
  47. package/payload/playbook/routing.md +19 -19
  48. package/payload/playbook/rules-card.md +16 -16
  49. package/payload/playbook/ship.md +2 -0
  50. package/payload/playbook/steer.md +21 -21
  51. package/payload/templates/change-deep.md +29 -29
  52. package/payload/templates/change-standard.md +28 -28
  53. package/payload/templates/change-vibe.md +19 -19
  54. package/payload/templates/config.yaml +8 -8
  55. package/payload/templates/constitution.md +14 -14
  56. package/payload/templates/contract-evals.md +9 -9
  57. package/payload/templates/contract-tests.md +9 -9
  58. package/payload/templates/harness.md +33 -33
  59. package/payload/templates/spec.md +14 -14
  60. package/payload/templates/steering.md +9 -9
  61. package/scripts/validate.mjs +47 -47
package/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.2 (2026-08-25)
4
+
5
+ - **Fix (delta)**: a `### MODIFIED Requirement:` body replaces the requirement wholesale,
6
+ so one that carried over only some of the spec's scenarios dropped the rest in silence —
7
+ no error, no warning, `spec merged` printed either way. Both shapes are now reported:
8
+ the scenario name gone from the replacement body, and the name surviving while WHEN/THEN
9
+ clauses it promised have no counterpart (the one a name-level comparison cannot see).
10
+ `archive` prints the report before it writes a byte and counts it in the summary;
11
+ `validate` reports the same at warn level, so the loss is visible while the change folder
12
+ is still readable rather than after ship archived it. A warning, never an error — removing
13
+ a scenario is sometimes the point of the change, and only the silence was ever the bug.
14
+ A reworded clause reports the same as a deleted one: nothing mechanical can tell "said
15
+ better" from "promises less". Cosmetic churn — indentation, bullet marker, clause order,
16
+ heading case, whitespace — is normalized away and never warns. (#1)
17
+
18
+ ## 0.5.1 (2026-08-25)
19
+
20
+ - **Fix (cost)**: `costUsd()` never charged cache-write tokens, the highest-rate of
21
+ the four classes the usage parser collects. Every cost `/sdlc:observe` and the
22
+ session summary have printed was therefore low. A model priced without a
23
+ `cacheWrite` rate now charges nothing for that class rather than inferring one from
24
+ `input` — the module's rule is never to guess a price, and a guess reports as
25
+ confidently as a known rate. Existing journalled costs are left alone: backfilling
26
+ would rewrite history from a rate that was not in force at the time.
27
+
3
28
  ## 0.5.0 (2026-08-25)
4
29
 
5
30
  - **`--auto` on every pipeline stage.** `/sdlc:auto` already ran the whole pipeline,
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 warnyin
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 warnyin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/bin/cli.mjs CHANGED
@@ -479,14 +479,22 @@ export function cmdArchive(projectRoot, changeId, { strict = true } = {}) {
479
479
 
480
480
  // Phase 1: compute every merge before writing anything (all-or-nothing).
481
481
  const merged = [];
482
+ const driftWarnings = [];
482
483
  for (const d of deltas) {
483
484
  const specPath = path.join(sdlcRoot, 'specs', d.capability, 'spec.md');
484
485
  const specText = fs.existsSync(specPath) ? fs.readFileSync(specPath, 'utf8') : null;
485
486
  const result = mergeDelta(specText, d.ops, d.capability);
486
487
  if (!result.ok) throw new Error(`spec merge failed for "${d.capability}": ${result.errors.join('; ')}`);
488
+ driftWarnings.push(...result.warnings);
487
489
  merged.push({ specPath, content: result.content, capability: d.capability });
488
490
  }
489
491
 
492
+ // A MODIFIED body replaces the requirement wholesale, so it can carry away a
493
+ // scenario the spec still promised. That is allowed — but it is said out loud
494
+ // here, while the change folder is still readable, not discovered in a diff
495
+ // after the folder moved under changes/archive/.
496
+ for (const w of driftWarnings) console.error(`⚠ ${w}`);
497
+
490
498
  // Phase 2: write specs, promote evals, stamp status, move to archive.
491
499
  for (const m of merged) writeFileNormalized(m.specPath, m.content);
492
500
 
@@ -508,8 +516,11 @@ export function cmdArchive(projectRoot, changeId, { strict = true } = {}) {
508
516
 
509
517
  console.log(`shipped: ${changeId}`);
510
518
  for (const m of merged) console.log(` spec merged: specs/${m.capability}/spec.md`);
519
+ if (driftWarnings.length) {
520
+ console.log(` ⚠ ${driftWarnings.length} scenario warning(s) above — re-read the spec diff before pushing`);
521
+ }
511
522
  console.log(` archived: changes/archive/${date}-${changeId}/`);
512
- return { archived: `${date}-${changeId}`, specs: merged.map((m) => m.capability) };
523
+ return { archived: `${date}-${changeId}`, specs: merged.map((m) => m.capability), warnings: driftWarnings };
513
524
  }
514
525
 
515
526
  // ---------- shared ----------
package/lib/caps.mjs CHANGED
@@ -1,45 +1,45 @@
1
- // Canonical line-cap table — the single source of truth for artifact
2
- // residency budgets. Templates quote these numbers in HTML comments and
3
- // tests/caps-sync.test.mjs asserts both stay identical.
4
- //
5
- // Rationale (Day-1 economics): every resident line is paid for in every
6
- // turn. Caps make "learning = distilling" structural, not disciplinary.
7
-
8
- import { parseFrontmatter } from './frontmatter.mjs';
9
-
10
- export const CAPS = Object.freeze({
11
- constitution: 30, // sdlc/context/constitution.md — always loaded
12
- steeringFile: 40, // each sdlc/context/steering/*.md
13
- alwaysBudget: 60, // constitution + all `inclusion: always` steering, combined
14
- harness: 60, // sdlc/harness.md
15
- spec: 150, // sdlc/specs/<capability>/spec.md (soft — split capability beyond)
16
- change: Object.freeze({ vibe: 40, standard: 100, deep: 150 }),
17
- contractTests: 60, // changes/<id>/contract/tests.md
18
- contractEvals: 40, // changes/<id>/contract/evals.md
19
- });
20
-
21
- export const TIERS = Object.freeze(['vibe', 'standard', 'deep']);
22
-
23
- export const STATUSES = Object.freeze([
24
- 'new', 'contracted', 'building', 'verified', 'shipped',
25
- ]);
26
-
27
- // Effective lines = body lines after frontmatter, excluding blanks and
28
- // single-line HTML comments. Caps meter prose the model must carry, not
29
- // machine metadata or annotation comments.
30
- export function countEffectiveLines(text) {
31
- const { body } = parseFrontmatter(text ?? '');
32
- return body
33
- .split(/\r?\n/)
34
- .filter((line) => {
35
- const t = line.trim();
36
- if (t === '') return false;
37
- if (t.startsWith('<!--') && t.endsWith('-->')) return false;
38
- return true;
39
- })
40
- .length;
41
- }
42
-
43
- export function capForChange(tier) {
44
- return CAPS.change[tier] ?? CAPS.change.standard;
45
- }
1
+ // Canonical line-cap table — the single source of truth for artifact
2
+ // residency budgets. Templates quote these numbers in HTML comments and
3
+ // tests/caps-sync.test.mjs asserts both stay identical.
4
+ //
5
+ // Rationale (Day-1 economics): every resident line is paid for in every
6
+ // turn. Caps make "learning = distilling" structural, not disciplinary.
7
+
8
+ import { parseFrontmatter } from './frontmatter.mjs';
9
+
10
+ export const CAPS = Object.freeze({
11
+ constitution: 30, // sdlc/context/constitution.md — always loaded
12
+ steeringFile: 40, // each sdlc/context/steering/*.md
13
+ alwaysBudget: 60, // constitution + all `inclusion: always` steering, combined
14
+ harness: 60, // sdlc/harness.md
15
+ spec: 150, // sdlc/specs/<capability>/spec.md (soft — split capability beyond)
16
+ change: Object.freeze({ vibe: 40, standard: 100, deep: 150 }),
17
+ contractTests: 60, // changes/<id>/contract/tests.md
18
+ contractEvals: 40, // changes/<id>/contract/evals.md
19
+ });
20
+
21
+ export const TIERS = Object.freeze(['vibe', 'standard', 'deep']);
22
+
23
+ export const STATUSES = Object.freeze([
24
+ 'new', 'contracted', 'building', 'verified', 'shipped',
25
+ ]);
26
+
27
+ // Effective lines = body lines after frontmatter, excluding blanks and
28
+ // single-line HTML comments. Caps meter prose the model must carry, not
29
+ // machine metadata or annotation comments.
30
+ export function countEffectiveLines(text) {
31
+ const { body } = parseFrontmatter(text ?? '');
32
+ return body
33
+ .split(/\r?\n/)
34
+ .filter((line) => {
35
+ const t = line.trim();
36
+ if (t === '') return false;
37
+ if (t.startsWith('<!--') && t.endsWith('-->')) return false;
38
+ return true;
39
+ })
40
+ .length;
41
+ }
42
+
43
+ export function capForChange(tier) {
44
+ return CAPS.change[tier] ?? CAPS.change.standard;
45
+ }
package/lib/config.mjs CHANGED
@@ -1,41 +1,41 @@
1
- // Minimal reader for sdlc/config.yaml — supports exactly what the template
2
- // documents: scalar keys, inline arrays, and a `prices:` block of inline
3
- // objects. Anything else is ignored (never crash a hook on config).
4
-
5
- export function parseConfig(text) {
6
- const config = { language: 'en', tools: [], prices: null };
7
- const lines = (text ?? '').split(/\r?\n/);
8
- let inPrices = false;
9
- for (const raw of lines) {
10
- if (!raw.trim() || raw.trim().startsWith('#')) continue;
11
- const isIndented = /^\s/.test(raw);
12
- if (!isIndented) inPrices = false;
13
-
14
- if (inPrices) {
15
- const m = raw.match(/^\s+([^:#]+):\s*\{(.*)\}\s*$/);
16
- if (!m) continue;
17
- const model = m[1].trim();
18
- const obj = {};
19
- for (const part of m[2].split(',')) {
20
- const kv = part.split(':');
21
- if (kv.length !== 2) continue;
22
- const num = Number(kv[1].trim());
23
- if (!Number.isNaN(num)) obj[kv[0].trim()] = num;
24
- }
25
- config.prices = { ...(config.prices ?? {}), [model]: obj };
26
- continue;
27
- }
28
-
29
- const kv = raw.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
30
- if (!kv) continue;
31
- const [, key, valueRaw] = kv;
32
- const value = valueRaw.replace(/\s+#.*$/, '').trim();
33
- if (key === 'prices' && value === '') { inPrices = true; continue; }
34
- if (value.startsWith('[') && value.endsWith(']')) {
35
- config[key] = value.slice(1, -1).split(',').map((s) => s.trim()).filter(Boolean);
36
- } else if (value !== '') {
37
- config[key] = value;
38
- }
39
- }
40
- return config;
41
- }
1
+ // Minimal reader for sdlc/config.yaml — supports exactly what the template
2
+ // documents: scalar keys, inline arrays, and a `prices:` block of inline
3
+ // objects. Anything else is ignored (never crash a hook on config).
4
+
5
+ export function parseConfig(text) {
6
+ const config = { language: 'en', tools: [], prices: null };
7
+ const lines = (text ?? '').split(/\r?\n/);
8
+ let inPrices = false;
9
+ for (const raw of lines) {
10
+ if (!raw.trim() || raw.trim().startsWith('#')) continue;
11
+ const isIndented = /^\s/.test(raw);
12
+ if (!isIndented) inPrices = false;
13
+
14
+ if (inPrices) {
15
+ const m = raw.match(/^\s+([^:#]+):\s*\{(.*)\}\s*$/);
16
+ if (!m) continue;
17
+ const model = m[1].trim();
18
+ const obj = {};
19
+ for (const part of m[2].split(',')) {
20
+ const kv = part.split(':');
21
+ if (kv.length !== 2) continue;
22
+ const num = Number(kv[1].trim());
23
+ if (!Number.isNaN(num)) obj[kv[0].trim()] = num;
24
+ }
25
+ config.prices = { ...(config.prices ?? {}), [model]: obj };
26
+ continue;
27
+ }
28
+
29
+ const kv = raw.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
30
+ if (!kv) continue;
31
+ const [, key, valueRaw] = kv;
32
+ const value = valueRaw.replace(/\s+#.*$/, '').trim();
33
+ if (key === 'prices' && value === '') { inPrices = true; continue; }
34
+ if (value.startsWith('[') && value.endsWith(']')) {
35
+ config[key] = value.slice(1, -1).split(',').map((s) => s.trim()).filter(Boolean);
36
+ } else if (value !== '') {
37
+ config[key] = value;
38
+ }
39
+ }
40
+ return config;
41
+ }