@llman-sdd/core 0.5.1 → 0.7.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.
- package/package.json +1 -1
- package/src/change/closeOutHarness.ts +10 -6
- package/src/config/load.ts +53 -3
- package/src/config/schema.ts +6 -6
- package/src/config/surface.ts +1 -1
- package/src/index.ts +27 -4
- package/src/init/defaultConfig.ts +8 -8
- package/src/init/init.ts +45 -16
- package/src/report/specHelpers.ts +9 -10
- package/src/report/specs.ts +16 -16
- package/src/report/unbound.ts +70 -0
- package/src/review/review.ts +9 -9
- package/src/spec/ir.ts +10 -0
- package/src/templates/skills.ts +10 -10
- package/src/validation/harness.ts +12 -12
- package/src/validation/roots.ts +123 -0
- package/src/validation/validate.ts +9 -8
- package/templates/en/skills/llman-sdd-apply.md +1 -1
- package/templates/en/skills/llman-sdd-explore.md +1 -1
- package/templates/en/skills/llman-sdd-propose.md +5 -5
- package/templates/en/skills/llman-sdd-validate.md +3 -3
- package/templates/en/skills/llman-sdd-verify.md +6 -6
- package/templates/zh-Hans/skills/llman-sdd-apply.md +1 -1
- package/templates/zh-Hans/skills/llman-sdd-explore.md +1 -1
- package/templates/zh-Hans/skills/llman-sdd-propose.md +5 -5
- package/templates/zh-Hans/skills/llman-sdd-validate.md +4 -4
- package/templates/zh-Hans/skills/llman-sdd-verify.md +6 -6
package/package.json
CHANGED
|
@@ -1,29 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Whether a close-out (finalize / archive) must run
|
|
2
|
+
* Whether a close-out (finalize / archive) must run specs.check_command before
|
|
3
3
|
* any merge or rename. Pure: the CLI supplies the flags, the spec scan, and
|
|
4
4
|
* the nested-invocation bit.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
export type CloseOutHarnessDecision =
|
|
8
|
-
| { kind: 'skip'; announce: boolean }
|
|
8
|
+
| { kind: 'skip'; announce: boolean; warning?: string }
|
|
9
9
|
| { kind: 'abort'; message: string }
|
|
10
10
|
| { kind: 'run'; command: string };
|
|
11
11
|
|
|
12
12
|
export function decideCloseOutHarness(input: {
|
|
13
13
|
noCheck: boolean;
|
|
14
14
|
needsSpecsChange: boolean;
|
|
15
|
-
hasExecutable: boolean;
|
|
16
15
|
runCommand: string | null;
|
|
17
16
|
nested: boolean;
|
|
18
17
|
}): CloseOutHarnessDecision {
|
|
19
18
|
if (input.noCheck) return { kind: 'skip', announce: true };
|
|
20
|
-
if (!input.needsSpecsChange
|
|
19
|
+
if (!input.needsSpecsChange) return { kind: 'skip', announce: false };
|
|
21
20
|
const command = input.runCommand?.trim() ?? '';
|
|
22
21
|
if (command === '') {
|
|
23
|
-
return {
|
|
22
|
+
return {
|
|
23
|
+
kind: 'skip',
|
|
24
|
+
announce: false,
|
|
25
|
+
warning:
|
|
26
|
+
'specs.check_command is not configured — close-out skips spec verification. Configure it to gate close-out (see migrations/v0.5-v0.6/README.md).',
|
|
27
|
+
};
|
|
24
28
|
}
|
|
25
29
|
if (input.nested) {
|
|
26
|
-
return { kind: 'abort', message: '
|
|
30
|
+
return { kind: 'abort', message: 'spec check skipped: nested invocation' };
|
|
27
31
|
}
|
|
28
32
|
return { kind: 'run', command };
|
|
29
33
|
}
|
package/src/config/load.ts
CHANGED
|
@@ -28,7 +28,52 @@ export class ConfigValidationError extends Error {
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Legacy `bdd:` section elevation (specs-check-config-and-unbound-feed):
|
|
33
|
+
* a present `bdd` block is recognised at load time and elevated onto the new
|
|
34
|
+
* `specs` semantics (`run_command` → `check_command`, `framework`/`verify_prompt`
|
|
35
|
+
* carried under the same names). The new-form `specs` block wins on key conflicts;
|
|
36
|
+
* the legacy block only fills gaps. Any undeclared legacy subkeys (bindings,
|
|
37
|
+
* default_language, feature_dir …) are dropped. Pure — never mutates the input;
|
|
38
|
+
* the caller decides how to surface the migration warning.
|
|
39
|
+
*/
|
|
40
|
+
export function elevateLegacyBdd(data: unknown): { data: unknown; legacyBddElevated: boolean } {
|
|
41
|
+
if (typeof data !== 'object' || data === null || Array.isArray(data)) {
|
|
42
|
+
return { data, legacyBddElevated: false };
|
|
43
|
+
}
|
|
44
|
+
const rec: Record<string, unknown> = { ...(data as Record<string, unknown>) };
|
|
45
|
+
const bdd = rec['bdd'];
|
|
46
|
+
if (bdd === undefined || bdd === null || typeof bdd !== 'object' || Array.isArray(bdd)) {
|
|
47
|
+
return { data, legacyBddElevated: false };
|
|
48
|
+
}
|
|
49
|
+
const legacy = bdd as Record<string, unknown>;
|
|
50
|
+
const specs =
|
|
51
|
+
typeof rec['specs'] === 'object' && rec['specs'] !== null && !Array.isArray(rec['specs'])
|
|
52
|
+
? (rec['specs'] as Record<string, unknown>)
|
|
53
|
+
: {};
|
|
54
|
+
const merged: Record<string, unknown> = { ...specs };
|
|
55
|
+
if (merged['check_command'] === undefined && typeof legacy['run_command'] === 'string') {
|
|
56
|
+
merged['check_command'] = legacy['run_command'];
|
|
57
|
+
}
|
|
58
|
+
if (merged['framework'] === undefined && typeof legacy['framework'] === 'string') {
|
|
59
|
+
merged['framework'] = legacy['framework'];
|
|
60
|
+
}
|
|
61
|
+
if (merged['verify_prompt'] === undefined && typeof legacy['verify_prompt'] === 'string') {
|
|
62
|
+
merged['verify_prompt'] = legacy['verify_prompt'];
|
|
63
|
+
}
|
|
64
|
+
delete rec['bdd'];
|
|
65
|
+
rec['specs'] = merged;
|
|
66
|
+
return { data: rec, legacyBddElevated: true };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Load + validate, returning the parsed config and whether a legacy `bdd:`
|
|
71
|
+
* section was elevated. The CLI surfaces the migration warning from the flag.
|
|
72
|
+
*/
|
|
73
|
+
export function loadConfigDetail(source: string): {
|
|
74
|
+
config: SddConfig;
|
|
75
|
+
legacyBddElevated: boolean;
|
|
76
|
+
} {
|
|
32
77
|
let data: unknown;
|
|
33
78
|
try {
|
|
34
79
|
data = parse(source);
|
|
@@ -37,7 +82,8 @@ export function loadConfig(source: string): SddConfig {
|
|
|
37
82
|
`YAML parse error: ${error instanceof Error ? error.message : String(error)}`,
|
|
38
83
|
]);
|
|
39
84
|
}
|
|
40
|
-
const
|
|
85
|
+
const elevated = elevateLegacyBdd(data);
|
|
86
|
+
const result = sddConfigSchema.safeParse(elevated.data);
|
|
41
87
|
if (!result.success) {
|
|
42
88
|
const issues = result.error.issues.map((iss) => {
|
|
43
89
|
const path = iss.path.map(String).join('/');
|
|
@@ -58,5 +104,9 @@ export function loadConfig(source: string): SddConfig {
|
|
|
58
104
|
throw new ConfigValidationError([(error as Error).message]);
|
|
59
105
|
}
|
|
60
106
|
}
|
|
61
|
-
return result.data;
|
|
107
|
+
return { config: result.data, legacyBddElevated: elevated.legacyBddElevated };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function loadConfig(source: string): SddConfig {
|
|
111
|
+
return loadConfigDetail(source).config;
|
|
62
112
|
}
|
package/src/config/schema.ts
CHANGED
|
@@ -15,18 +15,18 @@ export const EXTRA_SKILLS = [
|
|
|
15
15
|
'llman-sdd-research',
|
|
16
16
|
] as const;
|
|
17
17
|
|
|
18
|
-
export const
|
|
18
|
+
export const specsSchema = z.object({
|
|
19
19
|
framework: z
|
|
20
20
|
.string()
|
|
21
21
|
.default('')
|
|
22
22
|
.describe(
|
|
23
|
-
'BDD framework identifier (optional). Only used to derive a default
|
|
23
|
+
'BDD framework identifier (optional). Only used to derive a default check_command when check_command is unset.',
|
|
24
24
|
),
|
|
25
|
-
|
|
25
|
+
check_command: z
|
|
26
26
|
.string()
|
|
27
27
|
.nullish()
|
|
28
28
|
.describe(
|
|
29
|
-
'
|
|
29
|
+
'Spec verification command executed by validate for spec targets (skip with --no-check). Placeholders: {feature_path}, {feature_dir}, {feature_name}; without placeholders it runs once per validate invocation (batch-once). Legacy `bdd.run_command` is elevated onto this key at load time.',
|
|
30
30
|
),
|
|
31
31
|
verify_prompt: z.string().nullish().describe('Extra prompt text injected during verify phase.'),
|
|
32
32
|
});
|
|
@@ -82,10 +82,10 @@ export const sddConfigSchema = z.object({
|
|
|
82
82
|
archive: archiveSchema
|
|
83
83
|
.nullish()
|
|
84
84
|
.describe('Archive behaviour settings (defer tracking, completion gates).'),
|
|
85
|
-
|
|
85
|
+
specs: specsSchema
|
|
86
86
|
.nullish()
|
|
87
87
|
.describe(
|
|
88
|
-
'
|
|
88
|
+
'Spec verification integration settings. When defined, enables feature-as-spec mode and verification-aware verify prompts. Legacy `bdd` section is elevated onto this field at load time.',
|
|
89
89
|
),
|
|
90
90
|
sdd: sddSchema
|
|
91
91
|
.nullish()
|
package/src/config/surface.ts
CHANGED
|
@@ -18,7 +18,7 @@ export function renderConfigOverview(source: string): string[] {
|
|
|
18
18
|
`schema: ${config.schema}`,
|
|
19
19
|
`locale: ${config.locale}`,
|
|
20
20
|
`extra_skills (enabled/total): ${enabled} / ${EXTRA_SKILLS.length}`,
|
|
21
|
-
`
|
|
21
|
+
`specs: ${config.specs ? 'on' : 'off'}`,
|
|
22
22
|
`archive: ${archiveConfigured ? 'configured' : 'default'}`,
|
|
23
23
|
];
|
|
24
24
|
}
|
package/src/index.ts
CHANGED
|
@@ -3,14 +3,20 @@ export * from './ports.ts';
|
|
|
3
3
|
export {
|
|
4
4
|
EXTRA_SKILLS,
|
|
5
5
|
archiveSchema,
|
|
6
|
-
bddSchema,
|
|
7
6
|
changeIdSchema,
|
|
8
7
|
sddConfigSchema,
|
|
8
|
+
specsSchema,
|
|
9
9
|
sddSchema,
|
|
10
10
|
type SddConfig,
|
|
11
11
|
type SddConfigInput,
|
|
12
12
|
} from './config/schema.ts';
|
|
13
|
-
export {
|
|
13
|
+
export {
|
|
14
|
+
ConfigValidationError,
|
|
15
|
+
MAX_REPORTED_ISSUES,
|
|
16
|
+
elevateLegacyBdd,
|
|
17
|
+
loadConfig,
|
|
18
|
+
loadConfigDetail,
|
|
19
|
+
} from './config/load.ts';
|
|
14
20
|
export {
|
|
15
21
|
ChangeIdError,
|
|
16
22
|
compileChangeIdPattern,
|
|
@@ -27,7 +33,7 @@ export type {
|
|
|
27
33
|
ScenarioStep,
|
|
28
34
|
SpecStructuralError,
|
|
29
35
|
} from './spec/ir.ts';
|
|
30
|
-
export { specIdOf } from './spec/ir.ts';
|
|
36
|
+
export { ruleHasRunnableScenario, specIdOf } from './spec/ir.ts';
|
|
31
37
|
export {
|
|
32
38
|
SpecParseError,
|
|
33
39
|
localeToGherkinLang,
|
|
@@ -101,6 +107,16 @@ export {
|
|
|
101
107
|
type StageGate,
|
|
102
108
|
} from './validation/changeCheck.ts';
|
|
103
109
|
export { discoverSpecs, type DiscoveryIo } from './validation/discover.ts';
|
|
110
|
+
export {
|
|
111
|
+
discoverRoots,
|
|
112
|
+
isValidRoot,
|
|
113
|
+
resolveInstanceRoot,
|
|
114
|
+
scopeCrossings,
|
|
115
|
+
ROOT_EXCLUDED_DIRS,
|
|
116
|
+
type RootEntry,
|
|
117
|
+
type RootsIo,
|
|
118
|
+
type ScopeCrossing,
|
|
119
|
+
} from './validation/roots.ts';
|
|
104
120
|
export {
|
|
105
121
|
expandRunCommand,
|
|
106
122
|
runHarnessForSpecs,
|
|
@@ -164,7 +180,7 @@ export {
|
|
|
164
180
|
export {
|
|
165
181
|
ETHICS_KEYS,
|
|
166
182
|
buildTemplateVars,
|
|
167
|
-
|
|
183
|
+
effectiveCheckCommand,
|
|
168
184
|
enforceEthicsGovernance,
|
|
169
185
|
loadLocaleResource,
|
|
170
186
|
loadSkillTemplates,
|
|
@@ -181,6 +197,7 @@ export {
|
|
|
181
197
|
} from './templates/embedded.ts';
|
|
182
198
|
export {
|
|
183
199
|
TEMPLATES_ROOT,
|
|
200
|
+
refreshSubRootBlocks,
|
|
184
201
|
runInit,
|
|
185
202
|
updateFileWithMarkers,
|
|
186
203
|
type InitIo,
|
|
@@ -197,6 +214,12 @@ export {
|
|
|
197
214
|
type ChangeSummary,
|
|
198
215
|
} from './change/collect.ts';
|
|
199
216
|
export { renderChangesJson, renderChangesList } from './report/collect.ts';
|
|
217
|
+
export {
|
|
218
|
+
buildUnboundFeed,
|
|
219
|
+
collectUnboundRequirements,
|
|
220
|
+
type UnboundFeed,
|
|
221
|
+
type UnboundRequirement,
|
|
222
|
+
} from './report/unbound.ts';
|
|
200
223
|
export { graphData, graphMermaid, type GraphDataIr, type GraphFsIo } from './report/graph.ts';
|
|
201
224
|
export { parseDeps } from './report/graph.ts';
|
|
202
225
|
export { renderMachine, type MachineFormat } from './render/machine.ts';
|
|
@@ -23,13 +23,13 @@ locale: en
|
|
|
23
23
|
# - llman-sdd-wayfinder
|
|
24
24
|
# - llman-sdd-research
|
|
25
25
|
|
|
26
|
-
#
|
|
27
|
-
#
|
|
26
|
+
# Spec verification (optional, uncomment to enable)
|
|
27
|
+
# specs:
|
|
28
28
|
# framework: pytest-bdd
|
|
29
29
|
# # Filtered runners: include {feature_*} so validate --all/--specs runs per capability.
|
|
30
|
-
# #
|
|
30
|
+
# # check_command: "pytest {feature_dir} -k {feature_name} -v"
|
|
31
31
|
# # Project-wide runners (no placeholders): validate --all/--specs runs the suite once (batch-once).
|
|
32
|
-
# #
|
|
32
|
+
# # check_command: "cargo test --features bdd"
|
|
33
33
|
# # verify_prompt: |
|
|
34
34
|
# # Map test failures to requirement IDs.
|
|
35
35
|
`;
|
|
@@ -51,13 +51,13 @@ locale: zh-Hans
|
|
|
51
51
|
# - llman-sdd-wayfinder
|
|
52
52
|
# - llman-sdd-research
|
|
53
53
|
|
|
54
|
-
#
|
|
55
|
-
#
|
|
54
|
+
# Spec 验证(可选,取消注释以启用)
|
|
55
|
+
# specs:
|
|
56
56
|
# framework: pytest-bdd
|
|
57
57
|
# # 过滤型 runner:写 {feature_*},validate --all/--specs 按 capability 分别执行。
|
|
58
|
-
# #
|
|
58
|
+
# # check_command: "pytest {feature_dir} -k {feature_name} -v"
|
|
59
59
|
# # 项目级 runner(无占位符):validate --all/--specs 整批只跑一次(batch-once)。
|
|
60
|
-
# #
|
|
60
|
+
# # check_command: "cargo test --features bdd"
|
|
61
61
|
# # verify_prompt: |
|
|
62
62
|
# # 将测试失败映射到对应的 requirement ID。
|
|
63
63
|
`;
|
package/src/init/init.ts
CHANGED
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
prependSchemaHeader,
|
|
27
27
|
} from './defaultConfig.ts';
|
|
28
28
|
|
|
29
|
-
export {
|
|
29
|
+
export { effectiveCheckCommand } from '../templates/skills.ts';
|
|
30
30
|
|
|
31
31
|
const MARKER_START = '<!-- LLMANSPEC:START -->';
|
|
32
32
|
const MARKER_END = '<!-- LLMANSPEC:END -->';
|
|
@@ -80,7 +80,7 @@ function writeDefaultConfig(io: InitIo, locale: string): void {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
export interface InitResult {
|
|
83
|
-
/** Skill dirs written, in render order. */
|
|
83
|
+
/** Skill dirs written, in render order ([] when skills injection is off). */
|
|
84
84
|
skills: string[];
|
|
85
85
|
/** llman-sdd-* directories removed by --update namespace cleanup. */
|
|
86
86
|
removed: string[];
|
|
@@ -89,10 +89,31 @@ export interface InitResult {
|
|
|
89
89
|
|
|
90
90
|
const SKILLS_BASE = '.agents/skills';
|
|
91
91
|
|
|
92
|
+
/** Managed AGENTS.md marker blocks (root + llmanspec), content-preserving. */
|
|
93
|
+
function writeManagedBlocks(
|
|
94
|
+
io: InitIo,
|
|
95
|
+
templates: TemplateIo,
|
|
96
|
+
config: ReturnType<typeof loadConfig>,
|
|
97
|
+
version: string,
|
|
98
|
+
): void {
|
|
99
|
+
const vars = buildTemplateVars(config, version);
|
|
100
|
+
const locales = localeFallbacks(config.locale);
|
|
101
|
+
for (const [stubPath, agentsPath] of [
|
|
102
|
+
['agents-root-stub.md', 'AGENTS.md'],
|
|
103
|
+
['llmanspec-agents-stub.md', 'llmanspec/AGENTS.md'],
|
|
104
|
+
] as const) {
|
|
105
|
+
const stubRaw = loadLocaleResource(templates, TEMPLATES_ROOT, locales, stubPath);
|
|
106
|
+
if (stubRaw === null) continue;
|
|
107
|
+
const existing = io.exists(agentsPath) ? io.readText(agentsPath) : '';
|
|
108
|
+
const body = renderTemplate(stubRaw, new Map(), vars);
|
|
109
|
+
io.writeText(agentsPath, updateFileWithMarkers(existing, body));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
92
113
|
export function runInit(
|
|
93
114
|
io: InitIo,
|
|
94
115
|
templates: TemplateIo,
|
|
95
|
-
opts: { update: boolean; locale?: string; version: string },
|
|
116
|
+
opts: { update: boolean; locale?: string; version: string; skills: boolean },
|
|
96
117
|
): InitResult {
|
|
97
118
|
io.mkdirp('llmanspec');
|
|
98
119
|
|
|
@@ -110,26 +131,15 @@ export function runInit(
|
|
|
110
131
|
}
|
|
111
132
|
|
|
112
133
|
// 3) AGENTS.md managed blocks (root + llmanspec), preserving content.
|
|
134
|
+
writeManagedBlocks(io, templates, config, opts.version);
|
|
113
135
|
const vars = buildTemplateVars(config, opts.version);
|
|
114
|
-
const locales = localeFallbacks(config.locale);
|
|
115
136
|
const skillTemplates = loadSkillTemplates(templates, TEMPLATES_ROOT, config, vars);
|
|
116
137
|
enforceEthicsGovernance(skillTemplates);
|
|
117
138
|
|
|
118
|
-
for (const [stubPath, agentsPath] of [
|
|
119
|
-
['agents-root-stub.md', 'AGENTS.md'],
|
|
120
|
-
['llmanspec-agents-stub.md', 'llmanspec/AGENTS.md'],
|
|
121
|
-
] as const) {
|
|
122
|
-
const stubRaw = loadLocaleResource(templates, TEMPLATES_ROOT, locales, stubPath);
|
|
123
|
-
if (stubRaw === null) continue;
|
|
124
|
-
const existing = io.exists(agentsPath) ? io.readText(agentsPath) : '';
|
|
125
|
-
const body = renderTemplate(stubRaw, new Map(), vars);
|
|
126
|
-
io.writeText(agentsPath, updateFileWithMarkers(existing, body));
|
|
127
|
-
}
|
|
128
|
-
|
|
129
139
|
// 4) skills namespace cleanup (--update only): remove llman-sdd-* dirs
|
|
130
140
|
// outside the candidate set; un-prefixed custom skills stay untouched.
|
|
131
141
|
const removed: string[] = [];
|
|
132
|
-
if (opts.update && io.exists(SKILLS_BASE)) {
|
|
142
|
+
if (opts.skills && opts.update && io.exists(SKILLS_BASE)) {
|
|
133
143
|
const candidates = new Set(skillCandidates(config).map((f) => f.replace(/\.md$/u, '')));
|
|
134
144
|
for (const entry of io.listDir(SKILLS_BASE)) {
|
|
135
145
|
if (entry.startsWith('llman-sdd-') && !candidates.has(entry)) {
|
|
@@ -140,6 +150,12 @@ export function runInit(
|
|
|
140
150
|
}
|
|
141
151
|
|
|
142
152
|
// 5) write candidates: rendered product trimmed + single trailing newline.
|
|
153
|
+
// Sub-root instances default to no skills injection (r93): the agent skill
|
|
154
|
+
// surface stays at the repo root and sub-root navigation lives in the
|
|
155
|
+
// managed blocks; --skills opts a sub-root in.
|
|
156
|
+
if (!opts.skills) {
|
|
157
|
+
return { skills: [], removed, configPath };
|
|
158
|
+
}
|
|
143
159
|
for (const t of skillTemplates) {
|
|
144
160
|
const dirName = t.name.replace(/\.md$/u, '');
|
|
145
161
|
io.mkdirp(`${SKILLS_BASE}/${dirName}`);
|
|
@@ -148,3 +164,16 @@ export function runInit(
|
|
|
148
164
|
|
|
149
165
|
return { skills: skillTemplates.map((t) => t.name.replace(/\.md$/u, '')), removed, configPath };
|
|
150
166
|
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* r93 --update sweep: refresh the managed blocks of one discovered sub-root
|
|
170
|
+
* (blocks only — no scaffold, no config touch, no skills). Returns false for
|
|
171
|
+
* a missing config (discovery guarantees validity; a race is not fatal).
|
|
172
|
+
*/
|
|
173
|
+
export function refreshSubRootBlocks(io: InitIo, templates: TemplateIo, version: string): boolean {
|
|
174
|
+
const configPath = 'llmanspec/config.yaml';
|
|
175
|
+
if (!io.exists(configPath)) return false;
|
|
176
|
+
const config = loadConfig(io.readText(configPath));
|
|
177
|
+
writeManagedBlocks(io, templates, config, version);
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
@@ -39,20 +39,19 @@ function collectSpecEntries(io: SpecHelperIo, specsDir: string): ParsedEntry[] {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
42
|
+
* Max+1 over the requirement handles (`@req` on `规则:` headers) only — the id
|
|
43
|
+
* set comes from the global req registry fed with the parsed specs. Deliberately
|
|
44
|
+
* divergent from the predecessor's smallest-free semantics
|
|
45
|
+
* (`req_registry.rs::next_req_id_from_index`): handing out freed ids aliases
|
|
46
|
+
* references archived in past changes (issue #5) — retired ranges are never
|
|
47
|
+
* reused. See change `align-next-req-id-max-plus-one` design for the trade-off.
|
|
45
48
|
*/
|
|
46
49
|
export function nextReqId(io: SpecHelperIo, specsDir: string): string {
|
|
47
50
|
const entries = collectSpecEntries(io, specsDir);
|
|
48
|
-
const used =
|
|
49
|
-
|
|
50
|
-
Math.trunc(Number(reqId.replace(/^r/u, ''))),
|
|
51
|
-
),
|
|
51
|
+
const used = [...buildReqRegistry(entries).byId.keys()].map((reqId) =>
|
|
52
|
+
Math.trunc(Number(reqId.replace(/^r/u, ''))),
|
|
52
53
|
);
|
|
53
|
-
|
|
54
|
-
while (used.has(n)) n += 1;
|
|
55
|
-
return `r${n}`;
|
|
54
|
+
return `r${Math.max(0, ...used) + 1}`;
|
|
56
55
|
}
|
|
57
56
|
|
|
58
57
|
export function skeletonContent(capability: string, reqId: string, locale: string): string {
|
package/src/report/specs.ts
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Specs listing (peripheral-commands capability, r20/r21): morphology
|
|
3
|
-
* Native model — rules = `规则:` blocks;
|
|
4
|
-
* nested executable scenario;
|
|
5
|
-
*
|
|
2
|
+
* Specs listing (peripheral-commands capability, r20/r21/r90): morphology
|
|
3
|
+
* counts. Native model — rules = `规则:` blocks; bound = rules with at least
|
|
4
|
+
* one runnable nested executable scenario; unbound = rules without any runnable
|
|
5
|
+
* nested scenario (@skip/@experimental-only or stepless scenarios do not bind);
|
|
6
|
+
* acceptance = nested scenarios; featureScenarioCount = top-level examples.
|
|
7
|
+
* Machine field names follow the requirement terminology (r90).
|
|
6
8
|
*/
|
|
7
9
|
import { renderMachine } from '../render/machine.ts';
|
|
8
|
-
import { specIdOf } from '../spec/ir.ts';
|
|
10
|
+
import { ruleHasRunnableScenario, specIdOf } from '../spec/ir.ts';
|
|
9
11
|
import type { CapabilityDoc } from '../spec/ir.ts';
|
|
10
12
|
import type { SpecEntry } from '../validation/validate.ts';
|
|
11
13
|
import { pad } from './collect.ts';
|
|
12
14
|
|
|
13
15
|
export interface SpecMorphology {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
rulePendingCount: number;
|
|
16
|
+
requirementBoundCount: number;
|
|
17
|
+
requirementUnboundCount: number;
|
|
17
18
|
acceptanceCount: number;
|
|
18
19
|
featureScenarioCount: number;
|
|
19
20
|
}
|
|
@@ -30,16 +31,14 @@ export interface SpecSummary {
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
/** Morphology counts shared by `list --specs`, `show <spec> --json`, and the
|
|
33
|
-
* CLI text render —
|
|
34
|
-
*
|
|
35
|
-
* separately with no rule accounting. */
|
|
34
|
+
* CLI text render — bound/unbound follow the runnable-scenario definition; top-level
|
|
35
|
+
* examples are counted separately with no rule accounting. */
|
|
36
36
|
export function morphologyOf(doc: CapabilityDoc): SpecMorphology {
|
|
37
37
|
const rules = doc.rules;
|
|
38
38
|
const acceptance = rules.flatMap((r) => r.scenarios);
|
|
39
39
|
return {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
rulePendingCount: rules.filter((r) => r.scenarios.length === 0).length,
|
|
40
|
+
requirementBoundCount: rules.filter(ruleHasRunnableScenario).length,
|
|
41
|
+
requirementUnboundCount: rules.filter((r) => !ruleHasRunnableScenario(r)).length,
|
|
43
42
|
acceptanceCount: acceptance.length,
|
|
44
43
|
featureScenarioCount: doc.orphans.length,
|
|
45
44
|
};
|
|
@@ -48,6 +47,7 @@ export function morphologyOf(doc: CapabilityDoc): SpecMorphology {
|
|
|
48
47
|
export function collectSpecs(entries: readonly SpecEntry[]): SpecSummary[] {
|
|
49
48
|
return entries.map((e) => {
|
|
50
49
|
const morphology = morphologyOf(e.doc);
|
|
50
|
+
const total = morphology.requirementBoundCount + morphology.requirementUnboundCount;
|
|
51
51
|
return {
|
|
52
52
|
id: specIdOf(e),
|
|
53
53
|
title: specIdOf(e),
|
|
@@ -56,7 +56,7 @@ export function collectSpecs(entries: readonly SpecEntry[]): SpecSummary[] {
|
|
|
56
56
|
.split(',')
|
|
57
57
|
.map((s) => s.trim())
|
|
58
58
|
.filter((s) => s !== ''),
|
|
59
|
-
requirementCount:
|
|
59
|
+
requirementCount: total,
|
|
60
60
|
health: null,
|
|
61
61
|
staleness: null,
|
|
62
62
|
morphology,
|
|
@@ -70,7 +70,7 @@ export function renderSpecsList(summaries: readonly SpecSummary[]): string[] {
|
|
|
70
70
|
for (const s of summaries) {
|
|
71
71
|
const m = s.morphology;
|
|
72
72
|
lines.push(
|
|
73
|
-
` ${pad(s.id, idWidth)}
|
|
73
|
+
` ${pad(s.id, idWidth)}requirements ${s.requirementCount} bound ${m.requirementBoundCount} unbound ${m.requirementUnboundCount} acceptance ${m.acceptanceCount}`,
|
|
74
74
|
);
|
|
75
75
|
}
|
|
76
76
|
return lines;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `spec unbound` retrieval (peripheral-commands capability, r89): the
|
|
3
|
+
* implementation-readiness feed of unbound requirements. "Unbound" follows the
|
|
4
|
+
* single global definition — a requirement without any runnable nested scenario
|
|
5
|
+
* (@skip/@experimental-only or stepless included) — same口径 as review's
|
|
6
|
+
* `unbound` signal, validate's aggregate INFO and list/show morphology (r90).
|
|
7
|
+
*
|
|
8
|
+
* Ordering is deterministic: capability files in discovery (sorted) order,
|
|
9
|
+
* then rules in document order — agents can consume the feed incrementally.
|
|
10
|
+
* Pure — no filesystem access.
|
|
11
|
+
*/
|
|
12
|
+
import { ruleHasRunnableScenario, specIdOf } from '../spec/ir.ts';
|
|
13
|
+
import type { SpecEntry } from '../validation/validate.ts';
|
|
14
|
+
|
|
15
|
+
export interface UnboundRequirement {
|
|
16
|
+
/** The `@req:<id>` handle (requirement id). */
|
|
17
|
+
reqId: string;
|
|
18
|
+
/** The `规则:` block title. */
|
|
19
|
+
title: string;
|
|
20
|
+
/** Free-form requirement statement (block description, as authored). */
|
|
21
|
+
statement: string;
|
|
22
|
+
/** Capability (spec) id owning the requirement. */
|
|
23
|
+
capability: string;
|
|
24
|
+
/** Repo-root-relative path of the capability's main .feature file. */
|
|
25
|
+
featurePath: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** All unbound requirements across the given specs, in deterministic order. */
|
|
29
|
+
export function collectUnboundRequirements(entries: readonly SpecEntry[]): UnboundRequirement[] {
|
|
30
|
+
const out: UnboundRequirement[] = [];
|
|
31
|
+
for (const entry of entries) {
|
|
32
|
+
const capability = specIdOf(entry);
|
|
33
|
+
for (const rule of entry.doc.rules) {
|
|
34
|
+
if (ruleHasRunnableScenario(rule)) continue;
|
|
35
|
+
out.push({
|
|
36
|
+
reqId: rule.reqId,
|
|
37
|
+
title: rule.title,
|
|
38
|
+
statement: rule.description,
|
|
39
|
+
capability,
|
|
40
|
+
featurePath: entry.fileName,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface UnboundFeed {
|
|
48
|
+
/** Total unbound requirements before any limit. */
|
|
49
|
+
total: number;
|
|
50
|
+
/** Entries actually returned (limit applied). */
|
|
51
|
+
returned: number;
|
|
52
|
+
/** `total - returned`; 0 means everything was returned. */
|
|
53
|
+
remaining: number;
|
|
54
|
+
/** Self-introspection hint when remaining > 0 (empty otherwise). */
|
|
55
|
+
hint: string;
|
|
56
|
+
requirements: UnboundRequirement[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Build the `spec unbound` feed under a hard limit. `limit` semantics are
|
|
61
|
+
* uniform across every output mode: default 1, `--limit 0` returns all.
|
|
62
|
+
*/
|
|
63
|
+
export function buildUnboundFeed(entries: readonly SpecEntry[], limit: number): UnboundFeed {
|
|
64
|
+
const all = collectUnboundRequirements(entries);
|
|
65
|
+
const shown = limit === 0 ? all : all.slice(0, limit);
|
|
66
|
+
const returned = shown.length;
|
|
67
|
+
const remaining = all.length - returned;
|
|
68
|
+
const hint = remaining > 0 ? `… 还有 ${remaining} 条未绑定需求,用 --limit 0 列出全部` : '';
|
|
69
|
+
return { total: all.length, returned, remaining, hint, requirements: shown };
|
|
70
|
+
}
|
package/src/review/review.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { GitLike } from '../git/spawnGit.ts';
|
|
2
|
-
import { specIdOf } from '../spec/ir.ts';
|
|
2
|
+
import { ruleHasRunnableScenario, specIdOf } from '../spec/ir.ts';
|
|
3
3
|
import { evaluateStaleness, notApplicableStaleness } from '../validation/staleness.ts';
|
|
4
4
|
/**
|
|
5
5
|
* Review aggregation (review-freeze capability, r23): five-signal review over
|
|
@@ -7,7 +7,7 @@ import { evaluateStaleness, notApplicableStaleness } from '../validation/stalene
|
|
|
7
7
|
*/
|
|
8
8
|
import { validateAllSpecs, type SpecEntry, type SpecIo } from '../validation/validate.ts';
|
|
9
9
|
|
|
10
|
-
export type ReviewKind = '
|
|
10
|
+
export type ReviewKind = 'unbound' | 'stale' | 'locked' | 'validate';
|
|
11
11
|
|
|
12
12
|
export interface ReviewSignal {
|
|
13
13
|
kind: ReviewKind;
|
|
@@ -66,13 +66,13 @@ export function buildReview(input: ReviewInput, io: SpecIo): ReviewResult {
|
|
|
66
66
|
// r33: per-capability signals honor the --capability filter; locked and
|
|
67
67
|
// validate stay global regardless.
|
|
68
68
|
if (input.capability !== undefined && cap !== input.capability) continue;
|
|
69
|
-
// Native model: rules are `规则:` blocks; a rule without
|
|
70
|
-
// is
|
|
71
|
-
// scenarios outside any rule
|
|
72
|
-
// special signal.
|
|
73
|
-
const
|
|
69
|
+
// Native model: rules are `规则:` blocks; a rule without any runnable
|
|
70
|
+
// nested scenario is unbound (not bound to real logic — 0 scenarios or
|
|
71
|
+
// all @skip/@experimental/stepless). Top-level scenarios outside any rule
|
|
72
|
+
// are plain feature-level examples with no special signal.
|
|
73
|
+
const unbound = entry.doc.rules.filter((r) => !ruleHasRunnableScenario(r)).length;
|
|
74
74
|
|
|
75
|
-
push('
|
|
75
|
+
push('unbound', cap, unbound);
|
|
76
76
|
|
|
77
77
|
// staleness (predecessor evaluate): real base-ref/scope evaluation.
|
|
78
78
|
let staleInfo = notApplicableStaleness();
|
|
@@ -143,7 +143,7 @@ export function buildReview(input: ReviewInput, io: SpecIo): ReviewResult {
|
|
|
143
143
|
|
|
144
144
|
const lines: string[] = [`Review: critical=${criticalCount} warning=${warningCount}`];
|
|
145
145
|
for (const cap of sorted.map((e) => specIdOf(e))) {
|
|
146
|
-
for (const kind of ['
|
|
146
|
+
for (const kind of ['unbound', 'stale'] as const) {
|
|
147
147
|
const s = signals.find((x) => x.kind === kind && x.capability === cap);
|
|
148
148
|
if (!s) continue;
|
|
149
149
|
lines.push(`${kind}: ${cap} (${s.count})`);
|
package/src/spec/ir.ts
CHANGED
|
@@ -66,6 +66,16 @@ export interface CapabilityDoc {
|
|
|
66
66
|
errors: SpecStructuralError[];
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Whether a requirement is bound to real logic — it has at least one runnable
|
|
71
|
+
* nested scenario (not `@skip`/`@experimental` and carrying steps). Single
|
|
72
|
+
* authority for the `bound`/`unbound` definition shared by review's `unbound`
|
|
73
|
+
* signal, validate's aggregate INFO, list/show morphology and `spec unbound`.
|
|
74
|
+
*/
|
|
75
|
+
export function ruleHasRunnableScenario(rule: RuleIR): boolean {
|
|
76
|
+
return rule.scenarios.some((s) => s.runnable && s.stepCount > 0);
|
|
77
|
+
}
|
|
78
|
+
|
|
69
79
|
/**
|
|
70
80
|
* Single spec-id caliber (r25) for every consumer that labels a discovered
|
|
71
81
|
* spec entry: the `# capability:` header wins, else the fileName minus the
|
package/src/templates/skills.ts
CHANGED
|
@@ -49,10 +49,10 @@ export const ETHICS_KEYS: readonly string[] = [
|
|
|
49
49
|
'ethics.escalation_policy',
|
|
50
50
|
];
|
|
51
51
|
|
|
52
|
-
/** Framework-derived
|
|
53
|
-
export function
|
|
54
|
-
if (
|
|
55
|
-
switch (
|
|
52
|
+
/** Framework-derived check_command (predecessor config.rs effective_run_command). */
|
|
53
|
+
export function effectiveCheckCommand(specs: NonNullable<SddConfig['specs']>): string {
|
|
54
|
+
if (specs.check_command) return specs.check_command;
|
|
55
|
+
switch (specs.framework ?? '') {
|
|
56
56
|
case 'pytest-bdd':
|
|
57
57
|
return 'pytest {feature_dir} -k {feature_name} -v';
|
|
58
58
|
case 'rstest-bdd':
|
|
@@ -62,18 +62,18 @@ export function effectiveRunCommand(bdd: NonNullable<SddConfig['bdd']>): string
|
|
|
62
62
|
case 'behave':
|
|
63
63
|
return 'behave {feature_path}';
|
|
64
64
|
default:
|
|
65
|
-
return "echo 'No
|
|
65
|
+
return "echo 'No check_command configured. Set specs.check_command in config.yaml'";
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
/** All-string globals (predecessor BTreeMap<String, String> semantics). */
|
|
70
70
|
export function buildTemplateVars(config: SddConfig, version: string): Record<string, string> {
|
|
71
71
|
const vars: Record<string, string> = { llman_version: version };
|
|
72
|
-
if (config.
|
|
73
|
-
vars['
|
|
74
|
-
vars['
|
|
75
|
-
vars['
|
|
76
|
-
if (config.
|
|
72
|
+
if (config.specs) {
|
|
73
|
+
vars['specs_enabled'] = 'true';
|
|
74
|
+
vars['specs_framework'] = config.specs.framework ?? '';
|
|
75
|
+
vars['specs_check_command'] = effectiveCheckCommand(config.specs);
|
|
76
|
+
if (config.specs.verify_prompt) vars['specs_verify_prompt'] = config.specs.verify_prompt;
|
|
77
77
|
}
|
|
78
78
|
const extras = new Set<string>(config.extra_skills ?? []);
|
|
79
79
|
for (const name of OPTIONAL_SKILL_FILES) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* expands
|
|
2
|
+
* Spec verification check execution for validate (validation capability, r13/r48):
|
|
3
|
+
* expands specs.check_command per capability, executes each expanded command
|
|
4
4
|
* string at most once per validate invocation (batch-once), and maps results
|
|
5
5
|
* to per-capability issues. Pure — the subprocess runs through the injected
|
|
6
6
|
* HarnessRunner; this module never reads the environment or the wall clock
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import type { ValidationItem } from './validate.ts';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* Harness runner port (r13/r48): executes one expanded
|
|
13
|
+
* Harness runner port (r13/r48): executes one expanded specs.check_command
|
|
14
14
|
* string; the CLI adapter owns the subprocess (and the nested-invocation
|
|
15
15
|
* guard env it exports). exitCode is null only when the command could not
|
|
16
16
|
* start (spawnError then carries the reason). Lives here rather than in
|
|
@@ -39,7 +39,7 @@ export interface HarnessGate {
|
|
|
39
39
|
/** LLMAN_SDD_HARNESS_ACTIVE=1 was set by an enclosing validate invocation. */
|
|
40
40
|
nested: boolean;
|
|
41
41
|
check: 'default' | 'on' | 'off';
|
|
42
|
-
/** Undefined when
|
|
42
|
+
/** Undefined when specs.check_command is not configured (non-empty string). */
|
|
43
43
|
runner: HarnessRunner | undefined;
|
|
44
44
|
runCommand: string | null;
|
|
45
45
|
/** Project root — the cwd every harness subprocess runs in. */
|
|
@@ -75,7 +75,7 @@ const OUTPUT_TAIL = 200;
|
|
|
75
75
|
|
|
76
76
|
/**
|
|
77
77
|
* Trigger matrix (r13): --no-check skips silently; a nested invocation skips
|
|
78
|
-
* with a per-spec INFO; an explicit --check without a configured
|
|
78
|
+
* with a per-spec INFO; an explicit --check without a configured check_command
|
|
79
79
|
* yields a single INFO on the first spec; otherwise every expanded command
|
|
80
80
|
* executes at most once (cache keyed by the expanded string).
|
|
81
81
|
*/
|
|
@@ -91,7 +91,7 @@ export function runHarnessForSpecs(
|
|
|
91
91
|
{
|
|
92
92
|
level: 'INFO',
|
|
93
93
|
id: target.featurePath,
|
|
94
|
-
message: '
|
|
94
|
+
message: 'spec check skipped: nested invocation',
|
|
95
95
|
},
|
|
96
96
|
]);
|
|
97
97
|
}
|
|
@@ -104,7 +104,7 @@ export function runHarnessForSpecs(
|
|
|
104
104
|
{
|
|
105
105
|
level: 'INFO',
|
|
106
106
|
id: first.featurePath,
|
|
107
|
-
message: '--check has no effect:
|
|
107
|
+
message: '--check has no effect: specs.check_command is not configured',
|
|
108
108
|
},
|
|
109
109
|
]);
|
|
110
110
|
}
|
|
@@ -123,14 +123,14 @@ export function runHarnessForSpecs(
|
|
|
123
123
|
{
|
|
124
124
|
level: 'INFO',
|
|
125
125
|
id: target.featurePath,
|
|
126
|
-
message: `
|
|
126
|
+
message: `spec check passed (cached): ${expanded}`,
|
|
127
127
|
},
|
|
128
128
|
]
|
|
129
129
|
: [
|
|
130
130
|
{
|
|
131
131
|
level: 'ERROR',
|
|
132
132
|
id: target.featurePath,
|
|
133
|
-
message: `
|
|
133
|
+
message: `spec check failed (cached result of ${expanded}): ${cached.failureSummary}`,
|
|
134
134
|
},
|
|
135
135
|
];
|
|
136
136
|
} else {
|
|
@@ -142,13 +142,13 @@ export function runHarnessForSpecs(
|
|
|
142
142
|
const result = gate.runner.run(expanded, gate.cwd);
|
|
143
143
|
const firstError =
|
|
144
144
|
result.spawnError !== undefined
|
|
145
|
-
? `
|
|
145
|
+
? `spec check could not start: ${expanded}: ${result.spawnError}`
|
|
146
146
|
: result.exitCode === 0
|
|
147
147
|
? null
|
|
148
|
-
: `
|
|
148
|
+
: `spec check failed (exit ${result.exitCode}): ${expanded}: ${result.output.slice(-OUTPUT_TAIL)}`;
|
|
149
149
|
issues =
|
|
150
150
|
firstError === null
|
|
151
|
-
? [{ level: 'INFO', id: target.featurePath, message: `
|
|
151
|
+
? [{ level: 'INFO', id: target.featurePath, message: `spec check passed: ${expanded}` }]
|
|
152
152
|
: [{ level: 'ERROR', id: target.featurePath, message: firstError }];
|
|
153
153
|
cache.set(expanded, {
|
|
154
154
|
success: firstError === null,
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Instance-root discovery (subproject-llmanspec-discovery, r91-r92): the
|
|
3
|
+
* unified-root axiom — every llman-sdd operation targets one llmanspec/
|
|
4
|
+
* directory; the repo root is just the default instance and a workspace
|
|
5
|
+
* subpackage carrying its own llmanspec/ is another instance of the same
|
|
6
|
+
* shape. Pure: filesystem access only through the injected IO.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface RootsIo {
|
|
10
|
+
exists(path: string): boolean;
|
|
11
|
+
isDirectory(path: string): boolean;
|
|
12
|
+
listDir(path: string): string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface RootEntry {
|
|
16
|
+
/** Directory containing the llmanspec/ instance (the instance root). */
|
|
17
|
+
rootDir: string;
|
|
18
|
+
/** The llmanspec directory itself. */
|
|
19
|
+
llmanspecDir: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Directory names never descended into during discovery. */
|
|
23
|
+
export const ROOT_EXCLUDED_DIRS: ReadonlySet<string> = new Set(['node_modules', 'target', '.git']);
|
|
24
|
+
|
|
25
|
+
const joinPath = (dir: string, name: string): string =>
|
|
26
|
+
dir.endsWith('/') ? `${dir}${name}` : `${dir}/${name}`;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A directory counts as an llmanspec root when it carries `config.yaml` or a
|
|
30
|
+
* `specs/` subdir — bare `llmanspec/` scaffolds in flight are not instances.
|
|
31
|
+
*/
|
|
32
|
+
export function isValidRoot(llmanspecDir: string, io: RootsIo): boolean {
|
|
33
|
+
if (!io.isDirectory(llmanspecDir)) return false;
|
|
34
|
+
return io.exists(`${llmanspecDir}/config.yaml`) || io.isDirectory(`${llmanspecDir}/specs`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Discover instance roots under `startDir` (convention scan). The start dir
|
|
39
|
+
* itself is checked first (depth 0 — the git-root instance), then subdirs
|
|
40
|
+
* depth-first in stable sort order; `llmanspec/` contents are never descended
|
|
41
|
+
* into and excluded names are pruned. `maxDepth` mirrors the global
|
|
42
|
+
* --max-scan-depth knob (default 8).
|
|
43
|
+
*/
|
|
44
|
+
export function discoverRoots(
|
|
45
|
+
startDir: string,
|
|
46
|
+
io: RootsIo,
|
|
47
|
+
opts: { maxDepth?: number } = {},
|
|
48
|
+
): RootEntry[] {
|
|
49
|
+
const maxDepth = opts.maxDepth ?? 8;
|
|
50
|
+
const roots: RootEntry[] = [];
|
|
51
|
+
const walk = (dir: string, depth: number): void => {
|
|
52
|
+
if (depth > maxDepth) return;
|
|
53
|
+
const llmanspecDir = joinPath(dir, 'llmanspec');
|
|
54
|
+
if (isValidRoot(llmanspecDir, io)) roots.push({ rootDir: dir, llmanspecDir });
|
|
55
|
+
let names: string[];
|
|
56
|
+
try {
|
|
57
|
+
names = io.listDir(dir);
|
|
58
|
+
} catch {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
for (const name of names.toSorted()) {
|
|
62
|
+
if (name.startsWith('.') || ROOT_EXCLUDED_DIRS.has(name)) continue;
|
|
63
|
+
const full = joinPath(dir, name);
|
|
64
|
+
if (!io.isDirectory(full)) continue;
|
|
65
|
+
if (name === 'llmanspec') continue;
|
|
66
|
+
walk(full, depth + 1);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
walk(startDir, 0);
|
|
70
|
+
return roots;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface ScopeCrossing {
|
|
74
|
+
scope: string;
|
|
75
|
+
rootDir: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Single-ownership rule (r92): a file path belongs to exactly one llmanspec
|
|
80
|
+
* root. Scope entries are resolved against the owning instance root; a scope
|
|
81
|
+
* that reaches into another root's instance directory crosses the boundary
|
|
82
|
+
* and is reported with the offending scope and the other root. Ancestor roots
|
|
83
|
+
* are exempt: a sub-root scoping its own subtree always resolves under the
|
|
84
|
+
* ancestor's directory, but ownership there belongs to the descendant.
|
|
85
|
+
*/
|
|
86
|
+
export function scopeCrossings(
|
|
87
|
+
scopePaths: readonly string[],
|
|
88
|
+
instanceRootDir: string,
|
|
89
|
+
otherRoots: readonly RootEntry[],
|
|
90
|
+
): ScopeCrossing[] {
|
|
91
|
+
const crossings: ScopeCrossing[] = [];
|
|
92
|
+
for (const raw of scopePaths) {
|
|
93
|
+
const scope = raw.trim().replace(/^\.\//u, '').replace(/\/+$/u, '');
|
|
94
|
+
if (scope === '') continue;
|
|
95
|
+
const abs = joinPath(instanceRootDir, scope);
|
|
96
|
+
for (const other of otherRoots) {
|
|
97
|
+
if (other.rootDir === instanceRootDir) continue;
|
|
98
|
+
// Ancestor exemption: our own subtree necessarily sits inside the
|
|
99
|
+
// ancestor's directory — the ancestor does not own it.
|
|
100
|
+
if (instanceRootDir.startsWith(`${other.rootDir}/`)) continue;
|
|
101
|
+
if (abs === other.rootDir || abs.startsWith(`${other.rootDir}/`)) {
|
|
102
|
+
crossings.push({ scope: raw.trim(), rootDir: other.rootDir });
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return crossings;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Nearest instance root at or above `startDir` (inclusive) — the cwd
|
|
112
|
+
* resolution behind "cd into the subpackage and run" (r94). Returns null when
|
|
113
|
+
* no ancestor carries a valid llmanspec root.
|
|
114
|
+
*/
|
|
115
|
+
export function resolveInstanceRoot(startDir: string, io: RootsIo): string | null {
|
|
116
|
+
let dir = startDir;
|
|
117
|
+
for (;;) {
|
|
118
|
+
if (isValidRoot(joinPath(dir, 'llmanspec'), io)) return dir;
|
|
119
|
+
const parent = dir.replace(/\/+$/u, '').replace(/\/[^/]+$/u, '');
|
|
120
|
+
if (parent === '' || parent === dir) return null;
|
|
121
|
+
dir = parent;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* injected via SpecIo.
|
|
6
6
|
*/
|
|
7
7
|
import type { CapabilityDoc } from '../spec/ir.ts';
|
|
8
|
-
import { specIdOf } from '../spec/ir.ts';
|
|
8
|
+
import { ruleHasRunnableScenario, specIdOf } from '../spec/ir.ts';
|
|
9
9
|
import { buildReqRegistry } from '../spec/reqRegistry.ts';
|
|
10
10
|
|
|
11
11
|
export type ValidationLevel = 'ERROR' | 'WARNING' | 'INFO';
|
|
@@ -140,16 +140,17 @@ export function validateCapability(
|
|
|
140
140
|
// (native Gherkin); they carry no rule handle, so no warning or signal —
|
|
141
141
|
// they simply aren't part of rule accounting.
|
|
142
142
|
|
|
143
|
-
//
|
|
144
|
-
// scenario
|
|
145
|
-
// never
|
|
146
|
-
// `
|
|
147
|
-
|
|
148
|
-
|
|
143
|
+
// Unbound-requirement aggregate (r12/r90): rules without any runnable nested
|
|
144
|
+
// scenario (@skip/@experimental-only or stepless included), aggregated per
|
|
145
|
+
// capability (never one issue per rule), INFO so it never blocks anything.
|
|
146
|
+
// The real accountability lives in the review `unbound` signal, the
|
|
147
|
+
// `spec unbound` feed and the specs-compact workflow.
|
|
148
|
+
const unbound = rules.filter((r) => !ruleHasRunnableScenario(r)).length;
|
|
149
|
+
if (unbound > 0) {
|
|
149
150
|
push(
|
|
150
151
|
'INFO',
|
|
151
152
|
coveragePath(cap),
|
|
152
|
-
`${
|
|
153
|
+
`${unbound} unbound requirement(s) without any runnable scenario — bind via 场景: or compact`,
|
|
153
154
|
);
|
|
154
155
|
}
|
|
155
156
|
|
|
@@ -76,7 +76,7 @@ Run the project gates as appropriate:
|
|
|
76
76
|
- SDD validation: `llman-sdd validate <id> --strict`
|
|
77
77
|
|
|
78
78
|
**Gate evidence**:
|
|
79
|
-
- Close-out runs the configured `
|
|
79
|
+
- Close-out runs the configured `specs.check_command`, so do not run that command again just before close-out; the skip line printed by `--no-check` is not a pass.
|
|
80
80
|
- Gate verdicts MUST come from the real harness: MUST NOT obtain a "pass" via `--no-check`; on harness failure, find the root cause first (leaked env vars, nested-invocation guards, wrong cwd …) — MUST NOT label it an "inherent/self-referential property" and bypass it.
|
|
81
81
|
- Before/after criteria (counts, baselines) MUST be measured on the change branch (against the freshly computed merge-base); a value measured on the default branch is usually trivially the baseline and proves nothing.
|
|
82
82
|
- Refactors and bulk replacements: MUST compare the test count before and after; all-green gates with fewer tests is a failure.
|
|
@@ -46,7 +46,7 @@ flowchart LR
|
|
|
46
46
|
- Write decisions back: resolved decisions go into the change's `proposal.md` "Open Questions" section.
|
|
47
47
|
- Completion criterion: every pending decision is resolved or explicitly deferred. When not triggered, the default (ask 1–3 questions) behavior is unchanged.
|
|
48
48
|
4. If a change id is relevant, read its artifacts under `llmanspec/changes/<id>/`.
|
|
49
|
-
- When diagnosing validation errors, run `llman-sdd validate <spec> --strict` first for the structural gates (Gherkin / `@req` linkage / dual-write / req_id uniqueness); when `
|
|
49
|
+
- When diagnosing validation errors, run `llman-sdd validate <spec> --strict` first for the structural gates (Gherkin / `@req` linkage / dual-write / req_id uniqueness); when `specs.check_command` is configured, validate executes that harness by default (`--no-check` skips it). Failing items are pinned down in the default TOON output's `items[].issues[]`; `--output human` prints `FAIL <item_type>/<id>` lines.
|
|
50
50
|
5. Explore options and tradeoffs (2–3 options).
|
|
51
51
|
6. Assess change scale to determine if full SDD is needed.
|
|
52
52
|
7. When something crystallizes, offer to capture it (don't auto-write):
|
|
@@ -83,11 +83,11 @@ llman-sdd validate <change-id> --strict
|
|
|
83
83
|
```
|
|
84
84
|
This MUST pass before proceeding; failing items are listed one by one in the validate output's `items[].issues[]` — fix each and re-run.
|
|
85
85
|
|
|
86
|
-
### 4a) Optional BDD runner (`
|
|
87
|
-
- Read `llmanspec/config.yaml`. Is there a `
|
|
88
|
-
- **Yes**: `
|
|
89
|
-
- **No**: if this change involves executable behavior scenarios (Given/When/Then the user will want to run), ask **once, up front** whether to enable a `
|
|
90
|
-
- **Do NOT silently add the `
|
|
86
|
+
### 4a) Optional BDD runner (`specs:` block)
|
|
87
|
+
- Read `llmanspec/config.yaml`. Is there a `specs:` block?
|
|
88
|
+
- **Yes**: `specs.check_command` declares the project's BDD execution entry; validate executes it by default when its target set includes specs (`--no-check` skips). Authoring follows 4b regardless.
|
|
89
|
+
- **No**: if this change involves executable behavior scenarios (Given/When/Then the user will want to run), ask **once, up front** whether to enable a `specs:` verification runner block (adds a `specs:` block to `config.yaml` — runner only, does not change the lifecycle). If **yes**: show the exact `specs:` block to add (pick a `check_command` matching the project's test framework — `cargo test --features bdd` for rstest-bdd, `pytest {feature_dir} -k {feature_name} -v` for pytest-bdd), let the user confirm or edit, write it to `config.yaml`, then proceed with 4b. If **no**: features still validate structurally; BDD execution responsibility stays with the project test suite.
|
|
90
|
+
- **Do NOT silently add the `specs:` block** — always ask first. Adding it declares the project-wide BDD execution entry.
|
|
91
91
|
|
|
92
92
|
### 4b) Single-track feature authoring
|
|
93
93
|
- Planning docs may briefly live on the default branch; **do not** edit `llmanspec/specs/**` on the default branch. After binding, landing specs and implementation happen on the bound branch.
|
|
@@ -12,12 +12,12 @@ Validate change/spec format and staleness.
|
|
|
12
12
|
## Steps
|
|
13
13
|
1. Single item: `llman-sdd validate <id>`; batch: `llman-sdd validate --all` (or `--changes` / `--specs`); use `--strict` in CI/automation.
|
|
14
14
|
2. On failure, summarize the errors and propose minimal, concrete fixes.
|
|
15
|
-
{% if
|
|
15
|
+
{% if specs_enabled %}
|
|
16
16
|
3. **BDD checks**:
|
|
17
17
|
- Validate `.feature` Gherkin and `@req` / dual-write gates on the **bound branch**; `.feature` is the harness authority — executable GWT lives only there.
|
|
18
18
|
- Lifecycle gates: `change start` / `attach` (bind branch), `finalize` (close-out; auto commit `archive(sdd): <id>`, `--no-commit` to skip) / `diff` (read-only).
|
|
19
|
-
- `llman-sdd validate --specs` enforces structural and contract gates; when `
|
|
20
|
-
- `list --specs --json` shows `morphology` (
|
|
19
|
+
- `llman-sdd validate --specs` enforces structural and contract gates; when `specs.check_command` is configured it also executes that harness by default (`--no-check` skips it, `--check` is a compat alias); a placeholder-free command runs at most once per invocation.
|
|
20
|
+
- `list --specs --json` shows `morphology` (requirementCount / requirementBoundCount / requirementUnboundCount / acceptanceCount / featureScenarioCount).
|
|
21
21
|
- Change JSON status fields: `stage` (draft/designed/planned/full) / `specsLanded` / `needsSpecsChange` / `readyToImplement` (`show --output json`).
|
|
22
22
|
{% endif %}
|
|
23
23
|
|
|
@@ -26,7 +26,7 @@ flowchart LR
|
|
|
26
26
|
- **Apply must be all-green first**: don't verify unimplemented changes.
|
|
27
27
|
- **CRITICAL must be fixed**: zero CRITICAL before archive.
|
|
28
28
|
- **Rerun the gates yourself**: MUST rerun `llman-sdd validate <id> --strict` (real harness) and the project gates; MUST NOT trust gate verdicts in the implementer's report — a mismatch is CRITICAL.
|
|
29
|
-
- **`--no-check` is not evidence**: gate evidence obtained with `--no-check` → CRITICAL. Close-out runs the configured `
|
|
29
|
+
- **`--no-check` is not evidence**: gate evidence obtained with `--no-check` → CRITICAL. Close-out runs the configured `specs.check_command`, so do not run that command again just before close-out; the skip line printed by `--no-check` is not a pass.
|
|
30
30
|
- **Don't ask "should I continue?"**: run the full verification flow and output a complete report.
|
|
31
31
|
|
|
32
32
|
{{ unit("skills/stage-guard") }}
|
|
@@ -34,7 +34,7 @@ flowchart LR
|
|
|
34
34
|
## Steps
|
|
35
35
|
1. Select the change id (or ask the user to pick from `llman-sdd list --json`).
|
|
36
36
|
2. Fast validation gate: `llman-sdd validate <id> --strict`.
|
|
37
|
-
- When diagnosing structural issues (Gherkin parse / `@req` linkage / dual-write / req_id uniqueness), run the structural validation first (when `
|
|
37
|
+
- When diagnosing structural issues (Gherkin parse / `@req` linkage / dual-write / req_id uniqueness), run the structural validation first (when `specs.check_command` is configured, validate executes that harness by default — `--no-check` skips it; a harness failure lands as an ERROR on its spec item). Failing items are listed one by one in the default TOON output's `items[].issues[]` (`--output human` prints `FAIL <item_type>/<id>` lines above the `Totals` line).
|
|
38
38
|
3. Read: `llmanspec/specs/**` (`<capability>.feature`, the single source of truth) on the branch, `proposal.md` and `design.md` (if present), `tasks.md`; ignore residual old docs under `changes/<id>/specs/`.
|
|
39
39
|
4. **Dual-axis review (kept separate so neither masks the other)** — diff against `git diff <merge-base>...HEAD` (merge-base is COMPUTED via `git merge-base <local-default> HEAD`; the stored base_sha is audit-only and MUST NOT feed range math):
|
|
40
40
|
- **Spec axis**: does the implementation satisfy the `规则:` block requirement statement (free-text description, judged by its semantics) and the nested `场景:` GWT steps? Missing/partial behaviors, wrong implementations, and scope creep not asked for by the spec → suggest minimal fixes or artifact updates. Check where before/after evidence (counts, baselines) was taken: it MUST be measured on the change branch (against the freshly computed merge-base); a value measured on the default branch is usually trivially the baseline and proves nothing.
|
|
@@ -55,13 +55,13 @@ flowchart LR
|
|
|
55
55
|
| Middle Man (just delegates) | cut it, call direct |
|
|
56
56
|
| Refused Bequest (subclass rejects most inheritance) | use composition |
|
|
57
57
|
- The two axes may be reviewed in parallel (sub-agents); the report MUST present them separately, MUST NOT merge or cross-rerank (one axis passing must not mask the other failing).
|
|
58
|
-
5. **BDD verification** — only when `config.yaml` has a `
|
|
58
|
+
5. **BDD verification** — only when `config.yaml` has a `specs:` block:
|
|
59
59
|
- Confirm the change is branch-bound and you are on that branch.
|
|
60
|
-
- `llman-sdd validate --specs`: Gherkin + `@req`/dual-write gates; when `
|
|
60
|
+
- `llman-sdd validate --specs`: Gherkin + `@req`/dual-write gates; when `specs.check_command` is configured the harness runs by default (`--no-check` skips it) and a failure maps to an ERROR on the matching spec item.
|
|
61
61
|
- Optional read-only review: `llman-sdd change diff <id>` (or `--export-patch <path>`) — review/export only, never an apply step.
|
|
62
62
|
- Next step after verify passes: `llman-sdd-archive` (not inline finalize here).
|
|
63
|
-
{% if
|
|
64
|
-
- Extra requirement: {{
|
|
63
|
+
{% if specs_verify_prompt %}
|
|
64
|
+
- Extra requirement: {{ specs_verify_prompt }}
|
|
65
65
|
{% endif %}
|
|
66
66
|
6. Produce a short report: **CRITICAL** (must fix before archive) / **WARNING** (should fix) / **SUGGESTION** (nice to have).
|
|
67
67
|
7. **Human review gate**: once the report has no CRITICAL findings and before suggesting archive, run `llman-sdd review`: exit code zero → suggest `llman-sdd-archive`; non-zero = CRITICAL → fix via `llman-sdd-apply`, then re-run review; MUST NOT enter finalize/archive with CRITICAL findings open.
|
|
@@ -76,7 +76,7 @@ flowchart LR
|
|
|
76
76
|
- SDD 校验:`llman-sdd validate <id> --strict`
|
|
77
77
|
|
|
78
78
|
**门禁证据**:
|
|
79
|
-
- 收口会执行已配置的 `
|
|
79
|
+
- 收口会执行已配置的 `specs.check_command`,收口前不必再跑一遍;`--no-check` 打出的跳过说明不是通过。
|
|
80
80
|
- 门禁结论 MUST 来自真实 harness:MUST NOT 以 `--no-check` 取得「通过」;harness 失败 MUST 先查根因(环境变量泄漏、嵌套调用守卫、工作目录错误等),MUST NOT 以「固有/自指属性」定性后绕过。
|
|
81
81
|
- 前后对比类判据(计数、基线)MUST 在 change 分支上测量(相对现算 merge-base);默认分支测得的值通常恒为基线,不构成证据。
|
|
82
82
|
- 重构或批量替换类 task:MUST 对比改动前后测试用例数;门禁全绿但用例数下降视为失败。
|
|
@@ -46,7 +46,7 @@ flowchart LR
|
|
|
46
46
|
- 决策回写:已解决的决策写进该 change 的 `proposal.md`「Open Questions」段。
|
|
47
47
|
- 完成判据:每个待定决策都已解决或显式推迟。未触发时保持默认(问 1–3 个问题)。
|
|
48
48
|
4. 涉及某个 change id 时,读 `llmanspec/changes/<id>/` 下的工件。
|
|
49
|
-
- 诊断校验错误先跑 `llman-sdd validate <spec> --strict` 过结构门禁(Gherkin / `@req` 链接 / 双写 / req_id 唯一性);配置了 `
|
|
49
|
+
- 诊断校验错误先跑 `llman-sdd validate <spec> --strict` 过结构门禁(Gherkin / `@req` 链接 / 双写 / req_id 唯一性);配置了 `specs.check_command` 时 validate 缺省执行该 harness(`--no-check` 跳过)。失败项在缺省 TOON 输出的 `items[].issues[]` 逐条指明;`--output human` 输出人读 `FAIL <item_type>/<id>` 行。
|
|
50
50
|
5. 探索 2–3 个选项与权衡。
|
|
51
51
|
6. 判断变更规模,确定是否走完整 SDD。
|
|
52
52
|
7. 结论清晰时建议用户记录(勿自动写):
|
|
@@ -83,11 +83,11 @@ llman-sdd validate <change-id> --strict
|
|
|
83
83
|
```
|
|
84
84
|
MUST 通过才能继续;失败项在 validate 输出的 `items[].issues[]` 逐条指明,按条修复后重跑。
|
|
85
85
|
|
|
86
|
-
### 4a) 可选 BDD runner(`
|
|
87
|
-
- 读 `llmanspec/config.yaml` 是否含 `
|
|
88
|
-
- **有**:`
|
|
89
|
-
- **无**:若本次 change 含可执行行为场景(用户会想运行的 Given/When/Then),**一次性前置**询问是否启用 `
|
|
90
|
-
- **MUST NOT 静默添加 `
|
|
86
|
+
### 4a) 可选 BDD runner(`specs:` 段)
|
|
87
|
+
- 读 `llmanspec/config.yaml` 是否含 `specs:` 段:
|
|
88
|
+
- **有**:`specs.check_command` 是项目的 BDD 执行入口;validate 在目标集含 spec 时缺省执行它(`--no-check` 跳过)。撰写仍按 4b。
|
|
89
|
+
- **无**:若本次 change 含可执行行为场景(用户会想运行的 Given/When/Then),**一次性前置**询问是否启用 `specs:` 验证 runner 段(会向 `config.yaml` 加一个 `specs:` 段——仅 runner,不改生命周期)。**是**:展示要加的精确 `specs:` 段(`check_command` 选匹配项目测试框架的——rstest-bdd 用 `cargo test --features bdd`,pytest-bdd 用 `pytest {feature_dir} -k {feature_name} -v`),用户确认或修改后写入 `config.yaml`,再按 4b 继续。**否**:feature 仍做结构校验;BDD 执行责任始终在项目测试套件。
|
|
90
|
+
- **MUST NOT 静默添加 `specs:` 段**——总是先问。添加它会向全项目声明 BDD 执行入口。
|
|
91
91
|
|
|
92
92
|
### 4b) 单轨 feature 撰写
|
|
93
93
|
- 规划文档可短暂留在默认分支;**不要**在默认分支编辑 `llmanspec/specs/**`。绑定分支后,落地 specs 与实现都在绑定分支上。
|
|
@@ -12,12 +12,12 @@ metadata:
|
|
|
12
12
|
## 步骤
|
|
13
13
|
1. 单个:`llman-sdd validate <id>`;批量:`llman-sdd validate --all`(或 `--changes` / `--specs`);CI/自动化用 `--strict`。
|
|
14
14
|
2. 校验失败时汇总错误,给出最小可执行的修复建议。
|
|
15
|
-
{% if
|
|
16
|
-
3. **
|
|
15
|
+
{% if specs_enabled %}
|
|
16
|
+
3. **Spec 校验**:
|
|
17
17
|
- 在**绑定分支**上验证 `.feature` Gherkin 与 `@req` / 双写门禁;`.feature` 是 harness 权威——可执行 GWT 只在其中维护。
|
|
18
18
|
- 生命周期门禁:`change start` / `attach`(绑定分支)、`finalize`(收口;自动提交 `archive(sdd): <id>`,`--no-commit` 跳过)/ `diff`(只读)。
|
|
19
|
-
- `llman-sdd validate --specs` 做结构与合约门禁;配置 `
|
|
20
|
-
- `list --specs --json` 查看 `morphology`(
|
|
19
|
+
- `llman-sdd validate --specs` 做结构与合约门禁;配置 `specs.check_command` 时缺省执行该 harness(`--no-check` 跳过,`--check` 为兼容别名),无占位符的命令每次调用至多执行一次。
|
|
20
|
+
- `list --specs --json` 查看 `morphology`(requirementCount / requirementBoundCount / requirementUnboundCount / acceptanceCount / featureScenarioCount)。
|
|
21
21
|
- change JSON 状态字段:`stage`(draft/designed/planned/full)/ `specsLanded` / `needsSpecsChange` / `readyToImplement`(`show --output json`)。
|
|
22
22
|
{% endif %}
|
|
23
23
|
|
|
@@ -26,7 +26,7 @@ flowchart LR
|
|
|
26
26
|
- **必须先 apply 全绿**:未完成实现的 change 跳过验证。
|
|
27
27
|
- **CRITICAL 必须修复**:归档前清零。
|
|
28
28
|
- **亲自复跑门禁**:MUST 亲自重跑 `llman-sdd validate <id> --strict`(真实 harness)与项目门禁,MUST NOT 采信实现者报告的门禁结论;复跑结果与报告不符 → CRITICAL。
|
|
29
|
-
- **`--no-check` 不是证据**:以 `--no-check` 取得的门禁证据 → CRITICAL。收口会执行已配置的 `
|
|
29
|
+
- **`--no-check` 不是证据**:以 `--no-check` 取得的门禁证据 → CRITICAL。收口会执行已配置的 `specs.check_command`,收口前不必再跑一遍;`--no-check` 打出的跳过说明不是通过。
|
|
30
30
|
- **不要问「要不要继续」**:跑完整验证流程,输出完整报告。
|
|
31
31
|
|
|
32
32
|
{{ unit("skills/stage-guard") }}
|
|
@@ -34,7 +34,7 @@ flowchart LR
|
|
|
34
34
|
## 步骤
|
|
35
35
|
1. 确定 change id(不明确时让用户从 `llman-sdd list --json` 选)。
|
|
36
36
|
2. 快速校验门禁:`llman-sdd validate <id> --strict`。
|
|
37
|
-
- 诊断结构问题(Gherkin 解析 / `@req` 链接 / 双写 / req_id 唯一性)先跑结构校验(配置 `
|
|
37
|
+
- 诊断结构问题(Gherkin 解析 / `@req` 链接 / 双写 / req_id 唯一性)先跑结构校验(配置 `specs.check_command` 时 validate 缺省执行该 harness,`--no-check` 跳过;harness 失败以 ERROR 落在对应 spec 条目)。失败项在缺省 TOON 输出的 `items[].issues[]` 逐条列出(`--output human` 输出 `FAIL <item_type>/<id>` 行,位于 `Totals` 上方)。
|
|
38
38
|
3. 阅读:分支上的 `llmanspec/specs/**`(`<capability>.feature`,唯一事实来源)、`proposal.md` 与 `design.md`(如有)、`tasks.md`;`changes/<id>/specs/` 若有残留旧文档可忽略。
|
|
39
39
|
4. **双轴审查(两轴分离,互不掩盖)**——对比 diff(`git diff <merge-base>...HEAD`,merge-base 现算 `git merge-base <本地默认分支> HEAD`;存储的 base_sha 仅审计、MUST NOT 参与范围计算):
|
|
40
40
|
- **合约轴**:实现是否满足 `规则:` 块的需求表述(描述为自由文本,以其语义为准)与嵌套 `场景:` 的 GWT 步骤?缺失/部分实现、错误实现、spec 未要求的超范围改动 → 给最小修复建议或建议更新工件。前后对比类证据(计数、基线)核对测量位置:MUST 在 change 分支上测量(相对现算 merge-base);默认分支测得的值通常恒为基线,不构成证据。
|
|
@@ -55,13 +55,13 @@ flowchart LR
|
|
|
55
55
|
| Middle Man(只转发) | 删掉直连 |
|
|
56
56
|
| Refused Bequest(子类拒绝大部分继承) | 改组合 |
|
|
57
57
|
- 两轴可并行(sub-agent)审查;报告 MUST 分离呈现,MUST NOT 合并或交叉重排(一轴通过不能掩盖另一轴失败)。
|
|
58
|
-
5. **
|
|
58
|
+
5. **Spec 验证**——仅当 `config.yaml` 含 `specs:` 段:
|
|
59
59
|
- 确认 change 已绑定分支且当前在该分支上。
|
|
60
|
-
- `llman-sdd validate --specs`:Gherkin + `@req`/双写门禁;配置 `
|
|
60
|
+
- `llman-sdd validate --specs`:Gherkin + `@req`/双写门禁;配置 `specs.check_command` 时缺省执行该 harness(`--no-check` 跳过),失败映射为对应 spec 条目的 ERROR。
|
|
61
61
|
- 可选只读审查:`llman-sdd change diff <id>`(或 `--export-patch <path>`)——仅审查/导出,绝不当作 apply 步骤。
|
|
62
62
|
- verify 通过后下一步 `llman-sdd-archive`(勿在此 inline finalize)。
|
|
63
|
-
{% if
|
|
64
|
-
- 额外要求: {{
|
|
63
|
+
{% if specs_verify_prompt %}
|
|
64
|
+
- 额外要求: {{ specs_verify_prompt }}
|
|
65
65
|
{% endif %}
|
|
66
66
|
6. 输出简短报告:**CRITICAL**(归档前必须修复)/ **WARNING**(建议修复)/ **SUGGESTION**(可选优化)。
|
|
67
67
|
7. **人审关卡**:报告无 CRITICAL 后、建议归档前跑 `llman-sdd review`:退出码零 → 建议 `llman-sdd-archive`;非零 = CRITICAL → 用 `llman-sdd-apply` 修复后重跑 review;MUST NOT 带 CRITICAL 进入 finalize/archive。
|