@llman-sdd/core 0.3.1 → 0.5.1

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 (108) hide show
  1. package/package.json +2 -1
  2. package/src/archive/freeze.ts +86 -18
  3. package/src/archive/frozenCard.ts +105 -0
  4. package/src/archive/sevenzip.ts +15 -13
  5. package/src/change/closeOutHarness.ts +29 -0
  6. package/src/change/collect.ts +140 -0
  7. package/src/change/frontmatter.ts +48 -6
  8. package/src/change/id.ts +2 -6
  9. package/src/change/lifecycle.ts +285 -86
  10. package/src/change/nextId.ts +63 -2
  11. package/src/change/resolve.ts +2 -2
  12. package/src/change/tasks.ts +59 -0
  13. package/src/config/changeId.ts +14 -12
  14. package/src/config/load.ts +14 -0
  15. package/src/config/schema.ts +4 -41
  16. package/src/config/surface.ts +6 -36
  17. package/src/context/indexStore.ts +7 -3
  18. package/src/context/retrieve.ts +8 -10
  19. package/src/context/tree.ts +28 -24
  20. package/src/git/spawnGit.ts +90 -2
  21. package/src/index.ts +81 -54
  22. package/src/init/defaultConfig.ts +1 -5
  23. package/src/init/init.ts +19 -4
  24. package/src/ports.ts +1 -7
  25. package/src/project/migrateNotes.ts +104 -0
  26. package/src/render/machine.ts +30 -0
  27. package/src/report/collect.ts +11 -127
  28. package/src/report/graph/analysis.ts +152 -0
  29. package/src/report/graph/deps.ts +30 -0
  30. package/src/report/graph/graphData.ts +53 -0
  31. package/src/report/graph/nodes.ts +130 -0
  32. package/src/report/graph/render.ts +83 -0
  33. package/src/report/graph/types.ts +47 -0
  34. package/src/report/graph.ts +9 -381
  35. package/src/report/show.ts +20 -22
  36. package/src/report/specHelpers.ts +45 -22
  37. package/src/report/specs.ts +23 -25
  38. package/src/review/review.ts +45 -30
  39. package/src/spec/authoring.ts +147 -71
  40. package/src/spec/ir.ts +43 -15
  41. package/src/spec/keywords.ts +147 -0
  42. package/src/spec/migrateNative.ts +201 -0
  43. package/src/spec/parser.ts +95 -83
  44. package/src/spec/reqRegistry.ts +31 -15
  45. package/src/templates/embedded.ts +10 -16
  46. package/src/templates/engine.ts +10 -5
  47. package/src/templates/locale.ts +1 -1
  48. package/src/templates/skills.ts +4 -5
  49. package/src/validation/changeCheck.ts +128 -105
  50. package/src/validation/harness.ts +161 -0
  51. package/src/validation/staleness.ts +9 -5
  52. package/src/validation/validate.ts +60 -88
  53. package/templates/en/skills/llman-sdd-apply-cycle.md +20 -28
  54. package/templates/en/skills/llman-sdd-apply.md +58 -76
  55. package/templates/en/skills/llman-sdd-arch-review.md +12 -19
  56. package/templates/en/skills/llman-sdd-archive.md +27 -42
  57. package/templates/en/skills/llman-sdd-continue.md +17 -24
  58. package/templates/en/skills/llman-sdd-draft.md +17 -28
  59. package/templates/en/skills/llman-sdd-explore.md +29 -43
  60. package/templates/en/skills/llman-sdd-ff.md +12 -17
  61. package/templates/en/skills/llman-sdd-graph.md +14 -32
  62. package/templates/en/skills/llman-sdd-propose.md +48 -63
  63. package/templates/en/skills/llman-sdd-quick.md +12 -27
  64. package/templates/en/skills/llman-sdd-research.md +13 -24
  65. package/templates/en/skills/llman-sdd-specs-compact.md +14 -39
  66. package/templates/en/skills/llman-sdd-validate.md +11 -15
  67. package/templates/en/skills/llman-sdd-verify.md +23 -44
  68. package/templates/en/skills/llman-sdd-wayfinder.md +18 -22
  69. package/templates/en/units/skills/cli-footer.md +2 -0
  70. package/templates/en/units/skills/git-native-flow-brief.md +7 -6
  71. package/templates/en/units/skills/git-native-flow.md +21 -11
  72. package/templates/en/units/skills/human-readable-summary.md +2 -3
  73. package/templates/en/units/skills/stage-guard.md +7 -7
  74. package/templates/en/units/skills/structured-protocol.md +5 -8
  75. package/templates/en/units/skills/validation-hints.md +10 -14
  76. package/templates/en/units/spec/feature-contract.md +27 -16
  77. package/templates/en/units/workflow/archive-freeze-guidance.md +6 -3
  78. package/templates/zh-Hans/skills/llman-sdd-apply-cycle.md +23 -31
  79. package/templates/zh-Hans/skills/llman-sdd-apply.md +63 -81
  80. package/templates/zh-Hans/skills/llman-sdd-arch-review.md +21 -28
  81. package/templates/zh-Hans/skills/llman-sdd-archive.md +29 -44
  82. package/templates/zh-Hans/skills/llman-sdd-continue.md +17 -24
  83. package/templates/zh-Hans/skills/llman-sdd-draft.md +18 -29
  84. package/templates/zh-Hans/skills/llman-sdd-explore.md +34 -48
  85. package/templates/zh-Hans/skills/llman-sdd-ff.md +13 -18
  86. package/templates/zh-Hans/skills/llman-sdd-graph.md +16 -34
  87. package/templates/zh-Hans/skills/llman-sdd-propose.md +51 -65
  88. package/templates/zh-Hans/skills/llman-sdd-quick.md +15 -30
  89. package/templates/zh-Hans/skills/llman-sdd-research.md +17 -28
  90. package/templates/zh-Hans/skills/llman-sdd-specs-compact.md +15 -40
  91. package/templates/zh-Hans/skills/llman-sdd-validate.md +11 -15
  92. package/templates/zh-Hans/skills/llman-sdd-verify.md +26 -47
  93. package/templates/zh-Hans/skills/llman-sdd-wayfinder.md +25 -29
  94. package/templates/zh-Hans/units/skills/cli-footer.md +2 -0
  95. package/templates/zh-Hans/units/skills/git-native-flow-brief.md +7 -6
  96. package/templates/zh-Hans/units/skills/git-native-flow.md +22 -12
  97. package/templates/zh-Hans/units/skills/human-readable-summary.md +4 -5
  98. package/templates/zh-Hans/units/skills/stage-guard.md +9 -9
  99. package/templates/zh-Hans/units/skills/structured-protocol.md +5 -8
  100. package/templates/zh-Hans/units/skills/validation-hints.md +10 -14
  101. package/templates/zh-Hans/units/spec/feature-contract.md +25 -16
  102. package/templates/zh-Hans/units/workflow/archive-freeze-guidance.md +6 -2
  103. package/templates/en/skills/llman-sdd-onboard.md +0 -34
  104. package/templates/en/skills/llman-sdd-show.md +0 -24
  105. package/templates/en/units/migrate-prompt.md +0 -28
  106. package/templates/zh-Hans/skills/llman-sdd-onboard.md +0 -34
  107. package/templates/zh-Hans/skills/llman-sdd-show.md +0 -24
  108. package/templates/zh-Hans/units/migrate-prompt.md +0 -28
@@ -0,0 +1,161 @@
1
+ /**
2
+ * BDD harness execution for validate (validation capability, r13/r48):
3
+ * expands bdd.run_command per capability, executes each expanded command
4
+ * string at most once per validate invocation (batch-once), and maps results
5
+ * to per-capability issues. Pure — the subprocess runs through the injected
6
+ * HarnessRunner; this module never reads the environment or the wall clock
7
+ * (the nested-invocation guard is read by the CLI and passed in as a
8
+ * parameter).
9
+ */
10
+ import type { ValidationItem } from './validate.ts';
11
+
12
+ /**
13
+ * Harness runner port (r13/r48): executes one expanded bdd.run_command
14
+ * string; the CLI adapter owns the subprocess (and the nested-invocation
15
+ * guard env it exports). exitCode is null only when the command could not
16
+ * start (spawnError then carries the reason). Lives here rather than in
17
+ * ports.ts so the validation module gains no new cross-module edge (r72).
18
+ */
19
+ export interface HarnessRunner {
20
+ run(
21
+ command: string,
22
+ cwd: string,
23
+ ): {
24
+ exitCode: number | null;
25
+ output: string;
26
+ spawnError?: string;
27
+ };
28
+ }
29
+
30
+ export interface HarnessTarget {
31
+ /** Capability id ({feature_name}). */
32
+ capability: string;
33
+ /** Repo-root-relative path of the capability's main .feature file. */
34
+ featurePath: string;
35
+ }
36
+
37
+ /** Trigger state assembled by the CLI (env guard + flags + config + runner). */
38
+ export interface HarnessGate {
39
+ /** LLMAN_SDD_HARNESS_ACTIVE=1 was set by an enclosing validate invocation. */
40
+ nested: boolean;
41
+ check: 'default' | 'on' | 'off';
42
+ /** Undefined when bdd.run_command is not configured (non-empty string). */
43
+ runner: HarnessRunner | undefined;
44
+ runCommand: string | null;
45
+ /** Project root — the cwd every harness subprocess runs in. */
46
+ cwd: string;
47
+ /** CLI emits its one-line stderr banner through this hook before the first execution. */
48
+ onBeforeFirstRun?: (expanded: string) => void;
49
+ }
50
+
51
+ export interface HarnessRunOutcome {
52
+ /** Harness issues per capability, in target order. */
53
+ issuesByCapability: Map<string, ValidationItem[]>;
54
+ /** True when at least one command actually executed in this call. */
55
+ executed: boolean;
56
+ }
57
+
58
+ /** Plain-text placeholder expansion — capability ids are kebab-constrained. */
59
+ export function expandRunCommand(command: string, target: HarnessTarget): string {
60
+ const dirEnd = target.featurePath.lastIndexOf('/');
61
+ const featureDir = dirEnd === -1 ? target.featurePath : target.featurePath.slice(0, dirEnd);
62
+ return command
63
+ .replaceAll('{feature_path}', target.featurePath)
64
+ .replaceAll('{feature_dir}', featureDir)
65
+ .replaceAll('{feature_name}', target.capability);
66
+ }
67
+
68
+ interface CacheEntry {
69
+ success: boolean;
70
+ /** First ERROR message (≤200 chars) so a cached failure points at the cause. */
71
+ failureSummary: string | null;
72
+ }
73
+
74
+ const OUTPUT_TAIL = 200;
75
+
76
+ /**
77
+ * Trigger matrix (r13): --no-check skips silently; a nested invocation skips
78
+ * with a per-spec INFO; an explicit --check without a configured run_command
79
+ * yields a single INFO on the first spec; otherwise every expanded command
80
+ * executes at most once (cache keyed by the expanded string).
81
+ */
82
+ export function runHarnessForSpecs(
83
+ targets: readonly HarnessTarget[],
84
+ gate: HarnessGate,
85
+ ): HarnessRunOutcome {
86
+ const issuesByCapability = new Map<string, ValidationItem[]>();
87
+ if (gate.check === 'off') return { issuesByCapability, executed: false };
88
+ if (gate.nested) {
89
+ for (const target of targets) {
90
+ issuesByCapability.set(target.capability, [
91
+ {
92
+ level: 'INFO',
93
+ id: target.featurePath,
94
+ message: 'bdd harness skipped: nested invocation',
95
+ },
96
+ ]);
97
+ }
98
+ return { issuesByCapability, executed: false };
99
+ }
100
+ if (gate.runner === undefined || gate.runCommand === null || gate.runCommand === '') {
101
+ if (gate.check === 'on' && targets.length > 0) {
102
+ const first = targets[0] as HarnessTarget;
103
+ issuesByCapability.set(first.capability, [
104
+ {
105
+ level: 'INFO',
106
+ id: first.featurePath,
107
+ message: '--check has no effect: bdd.run_command is not configured',
108
+ },
109
+ ]);
110
+ }
111
+ return { issuesByCapability, executed: false };
112
+ }
113
+ const cache = new Map<string, CacheEntry>();
114
+ let executed = false;
115
+ let announced = false;
116
+ for (const target of targets) {
117
+ const expanded = expandRunCommand(gate.runCommand, target);
118
+ const cached = cache.get(expanded);
119
+ let issues: ValidationItem[];
120
+ if (cached !== undefined) {
121
+ issues = cached.success
122
+ ? [
123
+ {
124
+ level: 'INFO',
125
+ id: target.featurePath,
126
+ message: `bdd harness passed (cached): ${expanded}`,
127
+ },
128
+ ]
129
+ : [
130
+ {
131
+ level: 'ERROR',
132
+ id: target.featurePath,
133
+ message: `bdd harness failed (cached result of ${expanded}): ${cached.failureSummary}`,
134
+ },
135
+ ];
136
+ } else {
137
+ if (!announced) {
138
+ gate.onBeforeFirstRun?.(expanded);
139
+ announced = true;
140
+ }
141
+ executed = true;
142
+ const result = gate.runner.run(expanded, gate.cwd);
143
+ const firstError =
144
+ result.spawnError !== undefined
145
+ ? `bdd harness could not start: ${expanded}: ${result.spawnError}`
146
+ : result.exitCode === 0
147
+ ? null
148
+ : `bdd harness failed (exit ${result.exitCode}): ${expanded}: ${result.output.slice(-OUTPUT_TAIL)}`;
149
+ issues =
150
+ firstError === null
151
+ ? [{ level: 'INFO', id: target.featurePath, message: `bdd harness passed: ${expanded}` }]
152
+ : [{ level: 'ERROR', id: target.featurePath, message: firstError }];
153
+ cache.set(expanded, {
154
+ success: firstError === null,
155
+ failureSummary: firstError === null ? null : firstError.slice(0, OUTPUT_TAIL),
156
+ });
157
+ }
158
+ issuesByCapability.set(target.capability, issues);
159
+ }
160
+ return { issuesByCapability, executed };
161
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Staleness evaluator (validation/staleness parity): v1 `sdd/spec/staleness.rs`
2
+ * Staleness evaluator (validation/staleness parity): predecessor `sdd/spec/staleness.rs`
3
3
  * observable contract — status/baseRef/scope/touchedPaths/specUpdated/dirty/
4
4
  * notes plus per-capability staleness issues. Pure: git + env are injected.
5
5
  */
@@ -62,9 +62,9 @@ const BASE_MISSING_MSG =
62
62
  "Note: Unable to resolve base ref for staleness check. Set LLMANSPEC_BASE_REF (e.g. 'main' or 'origin/main'), or upgrade llman if below 0.0.60.";
63
63
 
64
64
  /**
65
- * Evaluate staleness for one capability spec — v1 status semantics.
65
+ * Evaluate staleness for one capability spec — predecessor status semantics.
66
66
  * Returns the info object plus warning/info issues (strict escalation is the
67
- * caller's job, matching v1 `apply_strict`).
67
+ * caller's job, matching predecessor `apply_strict`).
68
68
  */
69
69
  export function evaluateStaleness(deps: StalenessDeps): {
70
70
  info: StalenessInfo;
@@ -125,7 +125,7 @@ export function evaluateStaleness(deps: StalenessDeps): {
125
125
  }
126
126
  }
127
127
 
128
- // Tolerant: outside a git repo (or on git failure) treat as dirty (v1 unwrap_or(true)).
128
+ // Tolerant: outside a git repo (or on git failure) treat as dirty (predecessor unwrap_or(true)).
129
129
  const dirty = (git.runOpt(['status', '--porcelain']) ?? 'dirty') !== '';
130
130
  if (dirty) {
131
131
  if (status === 'OK' || status === 'STALE') status = 'INFO';
@@ -143,13 +143,17 @@ export function evaluateStaleness(deps: StalenessDeps): {
143
143
  }
144
144
  }
145
145
 
146
- const STALE_MSG = 'Note: Spec files changed on the base branch; re-review the spec.';
146
+ const STALE_MSG =
147
+ "Note: Code in this spec's scope changed on this branch but the spec was not updated; re-review the spec.";
147
148
  const SPEC_UPDATED_MSG = 'Note: Spec updated on this branch.';
148
149
 
149
150
  function revParse(git: GitLike, ref: string): string | null {
150
151
  return git.runOpt(['rev-parse', '--verify', '--quiet', ref]) ?? null;
151
152
  }
152
153
 
154
+ // Deliberately NOT shared with defaultBranch() in git/spawnGit.ts — this
155
+ // predecessor staleness probe is local-only (main → master, 'main' fallback, no origin
156
+ // consultation). Do not merge the two.
153
157
  function defaultBranchNameFn(git: GitLike): string {
154
158
  if (git.runOpt(['show-ref', '--verify', '--quiet', 'refs/heads/main']) !== null) return 'main';
155
159
  if (git.runOpt(['show-ref', '--verify', '--quiet', 'refs/heads/master']) !== null)
@@ -1,18 +1,18 @@
1
1
  /**
2
2
  * Validation engine (validation capability): aggregates Phase-2 parse errors
3
- * plus the verdict-equivalent gates (r11/r12, ordered to match v1
3
+ * plus the verdict-equivalent gates (r11/r12, ordered to match predecessor
4
4
  * `spec/validation.rs` observable issue order). Pure — filesystem access is
5
5
  * injected via SpecIo.
6
6
  */
7
7
  import type { CapabilityDoc } from '../spec/ir.ts';
8
- import { MUST_WORD_RE } from '../spec/ir.ts';
8
+ import { specIdOf } from '../spec/ir.ts';
9
9
  import { buildReqRegistry } from '../spec/reqRegistry.ts';
10
10
 
11
11
  export type ValidationLevel = 'ERROR' | 'WARNING' | 'INFO';
12
12
 
13
13
  export interface ValidationItem {
14
14
  level: ValidationLevel;
15
- /** Gate anchor, e.g. `t/rule/ok` or `t/valid_scope` (v1-style `path`). */
15
+ /** Gate anchor, e.g. `t/rule/ok` or `t/valid_scope` (predecessor-style `path`). */
16
16
  id: string;
17
17
  message: string;
18
18
  }
@@ -41,7 +41,6 @@ export interface ValidationReport {
41
41
  }
42
42
 
43
43
  const rulesPath = (cap: string): string => `${cap}/rules`;
44
- const acceptanceReqPath = (cap: string, name: string): string => `${cap}/acceptance/${name}/@req`;
45
44
  const coveragePath = (cap: string): string => `${cap}/coverage`;
46
45
 
47
46
  export function validateCapability(
@@ -51,7 +50,7 @@ export function validateCapability(
51
50
  opts: { strict?: boolean } = {},
52
51
  ): SpecVerdict {
53
52
  const { doc } = entry;
54
- const cap = doc.header.capability ?? entry.fileName;
53
+ const cap = specIdOf(entry);
55
54
  const strict = opts.strict === true;
56
55
  const items: ValidationItem[] = [];
57
56
 
@@ -59,27 +58,23 @@ export function validateCapability(
59
58
  items.push({ level, id, message });
60
59
  };
61
60
 
62
- // Header gates (r12 / v1 spec_meta). v1 treats a missing `# capability:`
61
+ // Header gates (r12 / predecessor spec_meta). predecessor treats a missing `# capability:`
63
62
  // header as a parse-level failure: only `file` + registry-scan issues are
64
63
  // emitted and all single-track gates are skipped.
65
64
  if (doc.header.capability === null) {
66
- const msg = `spec \`${cap}\`: missing \`# capability:\` header comment (spec-format r133)`;
65
+ const msg = `spec \`${cap}\`: missing \`# capability:\` header comment`;
67
66
  push('ERROR', 'file', msg);
68
67
  push('ERROR', 'llmanspec/specs', `Failed to scan req_id index: ${msg}`);
69
68
  return { fileName: entry.fileName, capability: cap, ok: false, items };
70
69
  }
71
70
  if (doc.header.purpose === null || doc.header.purpose.trim() === '') {
72
- push(
73
- 'ERROR',
74
- `${cap}/purpose`,
75
- '`# purpose:` header comment must not be empty (spec-format r133)',
76
- );
71
+ push('ERROR', `${cap}/purpose`, '`# purpose:` header comment must not be empty');
77
72
  }
78
73
  if (doc.header.scope === null) {
79
74
  push(
80
75
  'ERROR',
81
76
  `${cap}/valid_scope`,
82
- 'Spec valid_scope must not be empty (add it inside the .toon document).',
77
+ 'Spec valid_scope must not be empty (declare it in the "# scope:" header comment).',
83
78
  );
84
79
  } else {
85
80
  // r42: missing valid_scope paths are independent failures —
@@ -112,86 +107,50 @@ export function validateCapability(
112
107
  push('ERROR', `${cap}/feature`, 'Feature line must carry a title');
113
108
  }
114
109
 
115
- // Parser-level structural errors (mutual exclusion, removed-tag migration,
116
- // nested rule scenarios). Header gates and the MUST-word gate are owned here
117
- // (mapped below), so the parser's duplicate findings are skipped.
118
- const OWNED_BY_THIS_LAYER = ['missing-header:', 'rule:missing-must-word'];
110
+ // Parser-level structural errors. Header gates are owned here (mapped below);
111
+ // the parser's duplicate header findings are skipped.
112
+ const OWNED_BY_THIS_LAYER = ['missing-header:'];
119
113
  for (const err of doc.errors) {
120
114
  if (OWNED_BY_THIS_LAYER.some((prefix) => err.code.startsWith(prefix))) continue;
121
115
  push('ERROR', err.code.startsWith('file') ? 'file' : `${cap}/${err.code}`, err.message);
122
116
  }
123
117
 
124
- // Single-track gates (v1 validate_single_track order).
125
- const human = doc.scenarios.filter((s) => s.classification === 'human');
126
- const acceptance = doc.scenarios.filter((s) => s.classification === 'executable');
118
+ // Native single-track gates: `规则:` blocks with `@req` handles; nested
119
+ // `场景:` are their executable examples; top-level scenarios are orphans.
120
+ const rules = doc.rules;
127
121
 
128
- if (human.length === 0) {
129
- push('ERROR', rulesPath(cap), 'spec must define at least one @human constraint scenario');
122
+ if (rules.length === 0) {
123
+ push('ERROR', rulesPath(cap), 'spec must define at least one rule');
130
124
  }
131
125
 
132
- for (const scenario of doc.scenarios) {
133
- const anchor = `${cap}/rule/${scenario.name}`;
134
- if (scenario.classification === 'human') {
135
- if (scenario.reqIds.length === 0) {
136
- push('ERROR', anchor, '@human constraint scenario must carry an @req:<req_id> tag');
137
- }
138
- if (!MUST_WORD_RE.test(scenario.statement)) {
139
- push('ERROR', anchor, 'constraint statement must contain MUST/SHALL (or 必须/不得/禁止)');
140
- }
141
- } else if (scenario.classification === 'executable') {
142
- // (pairing handled below, after all rule req ids are known)
143
- } else {
126
+ for (const rule of rules) {
127
+ const anchor = `${cap}/rule/${rule.title}`;
128
+ if (rule.reqId === '') {
129
+ push('ERROR', anchor, 'rule must carry an @req:<req_id> tag on the rule header');
130
+ } else if (duplicatesFor(rule.reqId)) {
144
131
  push(
145
- 'WARNING',
146
- `${cap}/scenario/${scenario.name}`,
147
- `scenario \`${scenario.name}\` carries neither @human nor @executable; tag it or drop it`,
132
+ 'ERROR',
133
+ `${cap}/registry/${rule.reqId}`,
134
+ `global duplicate req_id \`${rule.reqId}\` used by multiple capabilities`,
148
135
  );
149
136
  }
150
- for (const reqId of scenario.reqIds) {
151
- if (duplicatesFor(reqId)) {
152
- push(
153
- 'ERROR',
154
- `${cap}/registry/${reqId}`,
155
- `global duplicate req_id \`${reqId}\` used by multiple capabilities`,
156
- );
157
- }
158
- }
159
137
  }
160
138
 
161
- // Dangling acceptance @req links (v1 order: acceptance/@req then coverage).
162
- const ruleReqIds = new Set(human.flatMap((s) => s.reqIds));
163
- for (const sc of acceptance) {
164
- // r65: orphan acceptance scenario — no @req link at all (v1 r132 WARNING).
165
- if (sc.reqIds.length === 0) {
166
- push(
167
- 'WARNING',
168
- `${cap}/acceptance/${sc.name}`,
169
- `orphan acceptance scenario \`${sc.name}\` has no @req:<req_id> link`,
170
- );
171
- }
172
- for (const rid of sc.reqIds) {
173
- if (!ruleReqIds.has(rid)) {
174
- push(
175
- 'ERROR',
176
- acceptanceReqPath(cap, sc.name),
177
- `@req:${rid} on acceptance scenario \`${sc.name}\` has no matching @human constraint`,
178
- );
179
- }
180
- }
181
- }
139
+ // Top-level `场景:` outside any rule are plain feature-level examples
140
+ // (native Gherkin); they carry no rule handle, so no warning or signal —
141
+ // they simply aren't part of rule accounting.
182
142
 
183
- // Rule coverage INFO (r134 pending rules).
184
- for (const sc of human) {
185
- for (const rid of sc.reqIds) {
186
- const covered = acceptance.some((a) => a.reqIds.includes(rid));
187
- if (!covered) {
188
- push(
189
- 'INFO',
190
- coveragePath(cap),
191
- `rule ${rid} is pending: no @executable acceptance scenario`,
192
- );
193
- }
194
- }
143
+ // Bare-rule aggregate (r134 migrated): rules with no nested executable
144
+ // scenario, aggregated per capability (never one issue per rule), INFO so it
145
+ // never blocks anything. The real accountability lives in the review
146
+ // `pending` signal and the specs-compact workflow.
147
+ const bare = rules.filter((r) => r.scenarios.length === 0).length;
148
+ if (bare > 0) {
149
+ push(
150
+ 'INFO',
151
+ coveragePath(cap),
152
+ `${bare} bare rule(s) without any executable scenario — convert to 场景: or compact`,
153
+ );
195
154
  }
196
155
 
197
156
  return {
@@ -202,13 +161,28 @@ export function validateCapability(
202
161
  };
203
162
  }
204
163
 
205
- export function validateAllSpecs(entries: readonly SpecEntry[], io: SpecIo): ValidationReport {
164
+ /**
165
+ * Shared req_id duplicate gate — single source of truth for the CLI
166
+ * (`validate <spec>` path) and the full sweep. The registry is built from the
167
+ * already-parsed docs, so a duplicate is reported for every involved
168
+ * capability regardless of unrelated parse errors elsewhere (r12 acceptance:
169
+ * 重复 req_id MUST 对每个涉事 capability 判 ERROR — the 前代 "structural error
170
+ * aborts the index scan" guard is intentionally dropped; a parse-failed spec
171
+ * merely omits the scenarios it could not decode, never invents ids).
172
+ */
173
+ export function buildDuplicatesFor(entries: readonly SpecEntry[]): (reqId: string) => boolean {
206
174
  const registry = buildReqRegistry(entries);
207
175
  const duplicateIds = new Set(registry.duplicates.flatMap((d) => d.reqId));
208
- // v1 parity: a structural error anywhere aborts the req_id index scan, so
209
- // the duplicate gate only fires when every spec parses cleanly.
210
- const structurallyClean = entries.every((e) => e.doc.errors.length === 0);
211
- const duplicatesFor = (reqId: string): boolean => structurallyClean && duplicateIds.has(reqId);
176
+ return (reqId: string): boolean => duplicateIds.has(reqId);
177
+ }
178
+
179
+ /** `Totals:` report line — single wording source for the engine and the CLI. */
180
+ export function formatTotals(passed: number, failed: number, total: number): string {
181
+ return `Totals: ${passed} passed, ${failed} failed (${total} items)`;
182
+ }
183
+
184
+ export function validateAllSpecs(entries: readonly SpecEntry[], io: SpecIo): ValidationReport {
185
+ const duplicatesFor = buildDuplicatesFor(entries);
212
186
 
213
187
  const verdicts = entries.map((e) => validateCapability(e, duplicatesFor, io));
214
188
  const failed = verdicts.some((v) => !v.ok);
@@ -221,14 +195,12 @@ export function validateAllSpecs(entries: readonly SpecEntry[], io: SpecIo): Val
221
195
  lines.push(` [${item.level}] ${item.id}: ${item.message}`);
222
196
  }
223
197
  }
224
- lines.push(
225
- `Totals: ${passed} passed, ${verdicts.length - passed} failed (${verdicts.length} items)`,
226
- );
198
+ lines.push(formatTotals(passed, verdicts.length - passed, verdicts.length));
227
199
 
228
200
  return { verdicts, lines, failed };
229
201
  }
230
202
 
231
- /** v1 `apply_strict`: WARNING issues escalate to ERROR when --strict. */
203
+ /** predecessor `apply_strict`: WARNING issues escalate to ERROR when --strict. */
232
204
  export function applyStrict<T extends { level: ValidationLevel }>(items: readonly T[]): T[] {
233
205
  return items.map((i) => (i.level === 'WARNING' ? { ...i, level: 'ERROR' as const } : i));
234
206
  }
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: "llman-sdd-apply-cycle"
3
- description: "Single closed-loop for one change: gate→implement→test→validate→verify→archive→commit. Manual trigger only. Agent MUST NOT auto-invoke."
3
+ description: "End-to-end closed loop for one change: implement→test→validate→verify→archive. Manual trigger only; agent must not auto-invoke."
4
4
  metadata:
5
5
  version: "{{ llman_version }}"
6
6
  disable-model-invocation: true
@@ -8,7 +8,7 @@ disable-model-invocation: true
8
8
 
9
9
  # LLMAN SDD Apply Cycle
10
10
 
11
- End-to-end closed loop for one change (manual). Requires Branch binding and `readyToImplement=true`.
11
+ End-to-end closed loop for one change (manual). Requires a bound branch and a green specs-landed gate (`specsLanded ∨ needsSpecsChange=false`); `readyToImplement=true` (the completion signal) closes the cycle.
12
12
 
13
13
  **Manual trigger only**: `/skill:llman-sdd-apply-cycle <change-id>`
14
14
 
@@ -16,60 +16,52 @@ End-to-end closed loop for one change (manual). Requires Branch binding and `rea
16
16
 
17
17
  ### 0) Gate + status
18
18
  ```bash
19
- llman-sdd show <change-id> --json --type change
19
+ llman-sdd show <change-id> --output json --type change
20
20
  ```
21
- > Stage gate: decide from `stage` / `readyToImplement` in `llman-sdd show <id> --json --type change`; full decision table lives in llman-sdd-apply.
21
+ > Stage decisions use the `stage` / `readyToImplement` fields; the full decision table lives in llman-sdd-apply.
22
22
 
23
23
  - Must be on the bound non-default branch.
24
- - If `readyToImplement` is not true → STOP (finish Specs landing or `needs_specs_change: false`); **do not** finalize yet.
25
- - Track progress via `tasks.md` checkboxes (or `llman-sdd list` task counts); still read `tasks.md`, proposal/design, and live `llmanspec/specs/**` on the bound branch (SSOT).
24
+ - specs-landed gate failing → STOP (land specs or `needs_specs_change: false`). Green but `readyToImplement=false` → normal: tasks pending, keep implementing; **finalize only when `readyToImplement=true`**.
25
+ - Track progress via `tasks.md` checkboxes (or `llman-sdd list` task counts); still read `tasks.md`, proposal/design, and `llmanspec/specs/**` on the bound branch (the single source of truth).
26
26
 
27
27
  ### 1) Loop: implement → test
28
28
  For each incomplete task:
29
- 1. Implement per task + live specs (minimal diff)
30
- 2. Run `tasks[].test` if present
29
+ 1. Implement per task + specs (minimal diff)
30
+ 2. Run the task's stated verification command when the task text names one
31
31
  3. On failure, fix and retry (same self-repair budget as `llman-sdd-apply`: cap 8 rounds)
32
32
  4. Check off `tasks.md` as `[x]`
33
33
 
34
34
  ### 2) Validate
35
35
  ```bash
36
- llman-sdd validate <change-id> --strict --no-interactive
36
+ llman-sdd validate <change-id> --strict
37
37
  ```
38
- On failure, fix and retry (same self-repair budget as `llman-sdd-apply`: cap 8 rounds).
38
+ On failure, fix and retry (cap 8 rounds).
39
39
 
40
40
  ### 3) Verify (recommended)
41
- Prefer `llman-sdd-verify` (or equivalent dual-axis self-check). CRITICAL → STOP; do not archive.
41
+ Prefer `llman-sdd-verify` (or an equivalent dual-axis self-check). CRITICAL → STOP; do not archive.
42
42
 
43
- ### 4) Archive
43
+ ### 4) Archive + commit
44
44
  ```bash
45
45
  llman-sdd change finalize <change-id>
46
46
  ```
47
- (dirty tree OK; auto merge (squash default) + docs rename + **auto commit** `archive(sdd): <change-id>` in one process. `--no-commit` skips the auto commit for manual/CI histories — then commit with `git add -A && git commit -m "archive(sdd): <change-id>"`.)
47
+ Dirty tree OK; auto merge (squash default) + rename + **auto commit** `archive(sdd): <change-id>` in one process. `--no-commit` skips the auto commit (manual/CI histories) — then commit with `git add -A && git commit -m "archive(sdd): <change-id>"`. Plain `change archive` stays as a fallback.
48
48
 
49
- `change checkpoint` is removed; the plain `change archive` command stays as a fallback (no checkpointed field required).
50
-
51
- ### 5) Commit (see step 4)
52
- Finalize already auto-committed unless `--no-commit` was passed.
53
-
54
- ### 6) Optional cleanup
49
+ ### 5) Optional cleanup
55
50
  ```bash
56
51
  git branch -D <feature-branch> # after squash the branch is no longer an ancestor of main; -d gets refused
57
52
  ```
58
- push / hosting PR only when the user explicitly asks.
53
+ Push / PR only when the user explicitly asks.
59
54
 
60
55
  ## Hard constraints
61
56
  - **Never ask** "should I continue" unless blocked.
62
57
  - **Never switch** changes until this one is archived and committed.
63
- - **Retry cap**: self-repair follows `llman-sdd-apply`'s 8-round budget (including the diagnose escalation path).
64
- - **Do not** author `changes/<id>/specs/` or use `change delta`.
65
- - **No default push/PR**.
58
+ - **Do not** author `changes/<id>/specs/`; **no default push/PR**.
66
59
 
67
60
  ## Ethics Governance
68
61
  - `ethics.risk_level`: medium
69
- - `ethics.prohibited_actions`: implement/archive without `readyToImplement`, switching changes early, writing `changes/<id>/specs/`, commit without validation, default push/PR
62
+ - `ethics.prohibited_actions`: implementing without a bound branch / a green specs-landed gate, archiving without `readyToImplement=true`, switching changes early, writing `changes/<id>/specs/`, committing without validation, default push/PR
70
63
  - `ethics.required_evidence`: `readyToImplement=true`, validate --strict pass, all tasks checked, finalize/archive success
71
- - `ethics.refusal_contract`: after 3 gate/validation failures, report blocker; do not force-archive
72
- - `ethics.escalation_policy`: if changing SDD workflow specs/templates, pause for user confirm before archive
64
+ - `ethics.refusal_contract`: after 8 self-repair rounds still failing, report a blocker; never force-archive
65
+ - `ethics.escalation_policy`: if changing SDD workflow specs/templates, pause for user confirmation before archive
73
66
 
74
- > For command details run `llman-sdd <cmd> --help`; the CLI is the command reference — skills embed no command tables.
75
- > "Spec" here = a `.feature` file under this project's `llmanspec/specs/`; run `llman-sdd list --specs` or `llman-sdd show <capability>`.
67
+ {{ unit("skills/cli-footer") }}