@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.
- package/package.json +2 -1
- package/src/archive/freeze.ts +86 -18
- package/src/archive/frozenCard.ts +105 -0
- package/src/archive/sevenzip.ts +15 -13
- package/src/change/closeOutHarness.ts +29 -0
- package/src/change/collect.ts +140 -0
- package/src/change/frontmatter.ts +48 -6
- package/src/change/id.ts +2 -6
- package/src/change/lifecycle.ts +285 -86
- package/src/change/nextId.ts +63 -2
- package/src/change/resolve.ts +2 -2
- package/src/change/tasks.ts +59 -0
- package/src/config/changeId.ts +14 -12
- package/src/config/load.ts +14 -0
- package/src/config/schema.ts +4 -41
- package/src/config/surface.ts +6 -36
- package/src/context/indexStore.ts +7 -3
- package/src/context/retrieve.ts +8 -10
- package/src/context/tree.ts +28 -24
- package/src/git/spawnGit.ts +90 -2
- package/src/index.ts +81 -54
- package/src/init/defaultConfig.ts +1 -5
- package/src/init/init.ts +19 -4
- package/src/ports.ts +1 -7
- package/src/project/migrateNotes.ts +104 -0
- package/src/render/machine.ts +30 -0
- package/src/report/collect.ts +11 -127
- package/src/report/graph/analysis.ts +152 -0
- package/src/report/graph/deps.ts +30 -0
- package/src/report/graph/graphData.ts +53 -0
- package/src/report/graph/nodes.ts +130 -0
- package/src/report/graph/render.ts +83 -0
- package/src/report/graph/types.ts +47 -0
- package/src/report/graph.ts +9 -381
- package/src/report/show.ts +20 -22
- package/src/report/specHelpers.ts +45 -22
- package/src/report/specs.ts +23 -25
- package/src/review/review.ts +45 -30
- package/src/spec/authoring.ts +147 -71
- package/src/spec/ir.ts +43 -15
- package/src/spec/keywords.ts +147 -0
- package/src/spec/migrateNative.ts +201 -0
- package/src/spec/parser.ts +95 -83
- package/src/spec/reqRegistry.ts +31 -15
- package/src/templates/embedded.ts +10 -16
- package/src/templates/engine.ts +10 -5
- package/src/templates/locale.ts +1 -1
- package/src/templates/skills.ts +4 -5
- package/src/validation/changeCheck.ts +128 -105
- package/src/validation/harness.ts +161 -0
- package/src/validation/staleness.ts +9 -5
- package/src/validation/validate.ts +60 -88
- package/templates/en/skills/llman-sdd-apply-cycle.md +20 -28
- package/templates/en/skills/llman-sdd-apply.md +58 -76
- package/templates/en/skills/llman-sdd-arch-review.md +12 -19
- package/templates/en/skills/llman-sdd-archive.md +27 -42
- package/templates/en/skills/llman-sdd-continue.md +17 -24
- package/templates/en/skills/llman-sdd-draft.md +17 -28
- package/templates/en/skills/llman-sdd-explore.md +29 -43
- package/templates/en/skills/llman-sdd-ff.md +12 -17
- package/templates/en/skills/llman-sdd-graph.md +14 -32
- package/templates/en/skills/llman-sdd-propose.md +48 -63
- package/templates/en/skills/llman-sdd-quick.md +12 -27
- package/templates/en/skills/llman-sdd-research.md +13 -24
- package/templates/en/skills/llman-sdd-specs-compact.md +14 -39
- package/templates/en/skills/llman-sdd-validate.md +11 -15
- package/templates/en/skills/llman-sdd-verify.md +23 -44
- package/templates/en/skills/llman-sdd-wayfinder.md +18 -22
- package/templates/en/units/skills/cli-footer.md +2 -0
- package/templates/en/units/skills/git-native-flow-brief.md +7 -6
- package/templates/en/units/skills/git-native-flow.md +21 -11
- package/templates/en/units/skills/human-readable-summary.md +2 -3
- package/templates/en/units/skills/stage-guard.md +7 -7
- package/templates/en/units/skills/structured-protocol.md +5 -8
- package/templates/en/units/skills/validation-hints.md +10 -14
- package/templates/en/units/spec/feature-contract.md +27 -16
- package/templates/en/units/workflow/archive-freeze-guidance.md +6 -3
- package/templates/zh-Hans/skills/llman-sdd-apply-cycle.md +23 -31
- package/templates/zh-Hans/skills/llman-sdd-apply.md +63 -81
- package/templates/zh-Hans/skills/llman-sdd-arch-review.md +21 -28
- package/templates/zh-Hans/skills/llman-sdd-archive.md +29 -44
- package/templates/zh-Hans/skills/llman-sdd-continue.md +17 -24
- package/templates/zh-Hans/skills/llman-sdd-draft.md +18 -29
- package/templates/zh-Hans/skills/llman-sdd-explore.md +34 -48
- package/templates/zh-Hans/skills/llman-sdd-ff.md +13 -18
- package/templates/zh-Hans/skills/llman-sdd-graph.md +16 -34
- package/templates/zh-Hans/skills/llman-sdd-propose.md +51 -65
- package/templates/zh-Hans/skills/llman-sdd-quick.md +15 -30
- package/templates/zh-Hans/skills/llman-sdd-research.md +17 -28
- package/templates/zh-Hans/skills/llman-sdd-specs-compact.md +15 -40
- package/templates/zh-Hans/skills/llman-sdd-validate.md +11 -15
- package/templates/zh-Hans/skills/llman-sdd-verify.md +26 -47
- package/templates/zh-Hans/skills/llman-sdd-wayfinder.md +25 -29
- package/templates/zh-Hans/units/skills/cli-footer.md +2 -0
- package/templates/zh-Hans/units/skills/git-native-flow-brief.md +7 -6
- package/templates/zh-Hans/units/skills/git-native-flow.md +22 -12
- package/templates/zh-Hans/units/skills/human-readable-summary.md +4 -5
- package/templates/zh-Hans/units/skills/stage-guard.md +9 -9
- package/templates/zh-Hans/units/skills/structured-protocol.md +5 -8
- package/templates/zh-Hans/units/skills/validation-hints.md +10 -14
- package/templates/zh-Hans/units/spec/feature-contract.md +25 -16
- package/templates/zh-Hans/units/workflow/archive-freeze-guidance.md +6 -2
- package/templates/en/skills/llman-sdd-onboard.md +0 -34
- package/templates/en/skills/llman-sdd-show.md +0 -24
- package/templates/en/units/migrate-prompt.md +0 -28
- package/templates/zh-Hans/skills/llman-sdd-onboard.md +0 -34
- package/templates/zh-Hans/skills/llman-sdd-show.md +0 -24
- 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):
|
|
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 —
|
|
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
|
|
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 (
|
|
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 =
|
|
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
|
|
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 {
|
|
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` (
|
|
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 =
|
|
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 /
|
|
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
|
|
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 (
|
|
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
|
|
116
|
-
//
|
|
117
|
-
|
|
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
|
-
//
|
|
125
|
-
|
|
126
|
-
const
|
|
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 (
|
|
129
|
-
push('ERROR', rulesPath(cap), 'spec must define at least one
|
|
122
|
+
if (rules.length === 0) {
|
|
123
|
+
push('ERROR', rulesPath(cap), 'spec must define at least one rule');
|
|
130
124
|
}
|
|
131
125
|
|
|
132
|
-
for (const
|
|
133
|
-
const anchor = `${cap}/rule/${
|
|
134
|
-
if (
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
'
|
|
146
|
-
`${cap}/
|
|
147
|
-
`
|
|
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
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
//
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
/**
|
|
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: "
|
|
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
|
|
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
|
|
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
|
-
-
|
|
25
|
-
- Track progress via `tasks.md` checkboxes (or `llman-sdd list` task counts); still read `tasks.md`, proposal/design, and
|
|
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 +
|
|
30
|
-
2. Run
|
|
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
|
|
36
|
+
llman-sdd validate <change-id> --strict
|
|
37
37
|
```
|
|
38
|
-
On failure, fix and retry (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
- **
|
|
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`:
|
|
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
|
|
72
|
-
- `ethics.escalation_policy`: if changing SDD workflow specs/templates, pause for user
|
|
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
|
-
|
|
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") }}
|