@llman-sdd/cli 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/cli-shared.ts +23 -3
- package/src/commands/change.ts +5 -10
- package/src/commands/config.ts +10 -2
- package/src/commands/project.ts +4 -14
- package/src/commands/review.ts +1 -1
- package/src/commands/show.ts +1 -1
- package/src/commands/spec.ts +38 -1
- package/src/commands/validate.ts +4 -4
- package/src/harness.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llman-sdd/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Spec-driven development workflow CLI (the `llman-sdd` command)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"build": "bun run ./scripts/build-binary.ts"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@llman-sdd/core": "0.
|
|
30
|
+
"@llman-sdd/core": "0.6.0",
|
|
31
31
|
"commander": "^15.0.0"
|
|
32
32
|
},
|
|
33
33
|
"engines": {
|
package/src/cli-shared.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
discoverSpecs,
|
|
10
10
|
embeddedTemplates,
|
|
11
11
|
loadConfig,
|
|
12
|
+
loadConfigDetail,
|
|
12
13
|
makeEmbeddedTemplateIo,
|
|
13
14
|
resolveChangeId,
|
|
14
15
|
type TemplateIo,
|
|
@@ -74,10 +75,29 @@ export function loadSpecEntries(): ReturnType<typeof discoverSpecs> {
|
|
|
74
75
|
return discoverSpecs('llmanspec/specs', newIo());
|
|
75
76
|
}
|
|
76
77
|
|
|
78
|
+
/** One-time legacy `bdd:` migration warning — printed to stderr by the first
|
|
79
|
+
* config-reading command of a process (specs-check-config-and-unbound-feed). */
|
|
80
|
+
let legacyBddWarningShown = false;
|
|
81
|
+
|
|
82
|
+
function warnLegacyBdd(elevated: boolean): void {
|
|
83
|
+
if (!elevated || legacyBddWarningShown) return;
|
|
84
|
+
legacyBddWarningShown = true;
|
|
85
|
+
console.error(
|
|
86
|
+
'[WARNING] llmanspec/config.yaml uses the legacy `bdd:` section — migrate to `specs:` (`bdd.run_command` → `specs.check_command`). See migrations/v0.5-v0.6/README.md.',
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Surface the legacy-section warning from a raw config source (config command). */
|
|
91
|
+
export function warnLegacyBddOnce(source: string): void {
|
|
92
|
+
if (legacyBddWarningShown) return;
|
|
93
|
+
warnLegacyBdd(loadConfigDetail(source).legacyBddElevated);
|
|
94
|
+
}
|
|
95
|
+
|
|
77
96
|
export function loadCliConfig(): ReturnType<typeof loadConfig> | null {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
97
|
+
if (!existsSync('llmanspec/config.yaml')) return null;
|
|
98
|
+
const detail = loadConfigDetail(readFileSync('llmanspec/config.yaml', 'utf8'));
|
|
99
|
+
warnLegacyBdd(detail.legacyBddElevated);
|
|
100
|
+
return detail.config;
|
|
81
101
|
}
|
|
82
102
|
|
|
83
103
|
export function cliMaxScanDepth(program: Command): number {
|
package/src/commands/change.ts
CHANGED
|
@@ -40,21 +40,15 @@ function runValidateSweep(): boolean {
|
|
|
40
40
|
return report.verdicts.some((v) => !v.ok);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
/** r81: run or refuse the
|
|
43
|
+
/** r81: run or refuse the spec verification command before any merge or rename. */
|
|
44
44
|
function enforceCloseOutHarness(id: string, noCheck: boolean): void {
|
|
45
45
|
const proposalPath = `llmanspec/changes/${id}/proposal.md`;
|
|
46
46
|
const proposal = existsSync(proposalPath) ? readFileSync(proposalPath, 'utf8') : '';
|
|
47
47
|
const needsSpecsChange = readNeedsSpecsChange(extractFrontmatter(proposal));
|
|
48
|
-
const
|
|
49
|
-
(entry) =>
|
|
50
|
-
entry.doc.rules.some((r) => r.scenarios.some((s) => s.runnable && s.stepCount > 0)) ||
|
|
51
|
-
entry.doc.orphans.some((s) => s.runnable && s.stepCount > 0),
|
|
52
|
-
);
|
|
53
|
-
const runCommand = loadCliConfig()?.bdd?.run_command ?? null;
|
|
48
|
+
const runCommand = loadCliConfig()?.specs?.check_command ?? null;
|
|
54
49
|
const decision = decideCloseOutHarness({
|
|
55
50
|
noCheck,
|
|
56
51
|
needsSpecsChange,
|
|
57
|
-
hasExecutable,
|
|
58
52
|
runCommand,
|
|
59
53
|
nested: process.env.LLMAN_SDD_HARNESS_ACTIVE === '1',
|
|
60
54
|
});
|
|
@@ -62,13 +56,14 @@ function enforceCloseOutHarness(id: string, noCheck: boolean): void {
|
|
|
62
56
|
throw new CliError(`close-out aborted: ${decision.message}`);
|
|
63
57
|
}
|
|
64
58
|
if (decision.kind === 'skip') {
|
|
65
|
-
if (decision.announce) console.error('
|
|
59
|
+
if (decision.announce) console.error('spec check skipped: --no-check');
|
|
60
|
+
if (decision.warning !== undefined) console.error(`[WARNING] ${decision.warning}`);
|
|
66
61
|
return;
|
|
67
62
|
}
|
|
68
63
|
const result = makeCliHarnessRunner().run(decision.command, process.cwd());
|
|
69
64
|
if (result.spawnError !== undefined || result.exitCode !== 0) {
|
|
70
65
|
const why = result.spawnError ?? `exit ${result.exitCode}`;
|
|
71
|
-
throw new CliError(`close-out aborted:
|
|
66
|
+
throw new CliError(`close-out aborted: spec check failed (${why})`);
|
|
72
67
|
}
|
|
73
68
|
}
|
|
74
69
|
|
package/src/commands/config.ts
CHANGED
|
@@ -3,7 +3,12 @@ import { readFileSync } from 'node:fs';
|
|
|
3
3
|
import { renderConfigOverview, renderMachine, skillsJson } from '@llman-sdd/core';
|
|
4
4
|
import type { Command } from 'commander';
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
addReportOutputOptions,
|
|
8
|
+
resolveOutMode,
|
|
9
|
+
skillDesc,
|
|
10
|
+
warnLegacyBddOnce,
|
|
11
|
+
} from '../cli-shared.ts';
|
|
7
12
|
|
|
8
13
|
export function registerConfig(program: Command): void {
|
|
9
14
|
const configCmd = program
|
|
@@ -12,6 +17,7 @@ export function registerConfig(program: Command): void {
|
|
|
12
17
|
|
|
13
18
|
configCmd.description('Print a read-only llmanspec/config.yaml overview').action(() => {
|
|
14
19
|
const source = readFileSync('llmanspec/config.yaml', 'utf8');
|
|
20
|
+
warnLegacyBddOnce(source);
|
|
15
21
|
console.log(renderConfigOverview(source).join('\n'));
|
|
16
22
|
});
|
|
17
23
|
|
|
@@ -19,7 +25,9 @@ export function registerConfig(program: Command): void {
|
|
|
19
25
|
addReportOutputOptions(skills);
|
|
20
26
|
skills.action((options: { json?: boolean; output?: string; compactJson?: boolean }) => {
|
|
21
27
|
const path = 'llmanspec/config.yaml';
|
|
22
|
-
const
|
|
28
|
+
const source = readFileSync(path, 'utf8');
|
|
29
|
+
warnLegacyBddOnce(source);
|
|
30
|
+
const info = skillsJson(source);
|
|
23
31
|
const mode = resolveOutMode(options.output, options.json, options.compactJson);
|
|
24
32
|
if (mode !== 'human') {
|
|
25
33
|
console.log(renderMachine(info, mode));
|
package/src/commands/project.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { migrateNoteFor, migrateOverviewFor, planDedupe } from '@llman-sdd/core';
|
|
1
|
+
import { buildReqRegistry, migrateNoteFor, migrateOverviewFor, planDedupe } from '@llman-sdd/core';
|
|
2
2
|
import type { Command } from 'commander';
|
|
3
3
|
|
|
4
4
|
import { CliError, loadCliConfig, loadSpecEntries, newIo } from '../cli-shared.ts';
|
|
@@ -11,20 +11,10 @@ export function registerProject(program: Command): void {
|
|
|
11
11
|
.description('Remap globally duplicated req ids (report with --dry-run)')
|
|
12
12
|
.option('--dry-run', 'report the remap plan without writing')
|
|
13
13
|
.action((options: { dryRun?: boolean }) => {
|
|
14
|
-
// dedupe registry covers requirement handles (`@req` on 规则: headers) only
|
|
14
|
+
// dedupe registry covers requirement handles (`@req` on 规则: headers) only;
|
|
15
|
+
// duplicates follow the occurrence model (同文件内共用或跨文件均算)
|
|
15
16
|
const entries = loadSpecEntries();
|
|
16
|
-
const
|
|
17
|
-
for (const e of entries) {
|
|
18
|
-
for (const rule of e.doc.rules) {
|
|
19
|
-
if (rule.reqId === '') continue;
|
|
20
|
-
const list = owners.get(rule.reqId) ?? [];
|
|
21
|
-
if (!list.includes(e.fileName)) list.push(e.fileName);
|
|
22
|
-
owners.set(rule.reqId, list);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
const duplicates = [...owners.entries()]
|
|
26
|
-
.filter(([, files]) => files.length > 1)
|
|
27
|
-
.map(([reqId, files]) => ({ reqId, files }));
|
|
17
|
+
const { duplicates } = buildReqRegistry(entries);
|
|
28
18
|
if (duplicates.length === 0) {
|
|
29
19
|
console.log('No colliding req_id values in llmanspec/specs.');
|
|
30
20
|
return;
|
package/src/commands/review.ts
CHANGED
|
@@ -26,7 +26,7 @@ import { makeCliGit } from '../io.ts';
|
|
|
26
26
|
export function registerReview(program: Command): void {
|
|
27
27
|
const review = program
|
|
28
28
|
.command('review')
|
|
29
|
-
.description('Aggregate review:
|
|
29
|
+
.description('Aggregate review: unbound/stale signals plus a validate sweep');
|
|
30
30
|
|
|
31
31
|
review.option('--capability <capability>', 'restrict the sweep to one capability/spec id');
|
|
32
32
|
addReportOutputOptions(review);
|
package/src/commands/show.ts
CHANGED
|
@@ -209,7 +209,7 @@ export function registerShow(program: Command): void {
|
|
|
209
209
|
const raw = readFileSync(specPath, 'utf8').trimEnd();
|
|
210
210
|
const summary = collectSpecs(entries).find((x) => x.id === item);
|
|
211
211
|
const morphology = summary
|
|
212
|
-
? `\n\n## Morphology\
|
|
212
|
+
? `\n\n## Morphology\nrequirements=${summary.requirementCount} bound=${summary.morphology.requirementBoundCount} unbound=${summary.morphology.requirementUnboundCount} acceptanceCount=${summary.morphology.acceptanceCount}`
|
|
213
213
|
: '';
|
|
214
214
|
console.log(`## Spec\n${raw}${morphology}`);
|
|
215
215
|
return;
|
package/src/commands/spec.ts
CHANGED
|
@@ -5,15 +5,25 @@ import { createInterface } from 'node:readline';
|
|
|
5
5
|
import {
|
|
6
6
|
addReq,
|
|
7
7
|
addScenario,
|
|
8
|
+
buildUnboundFeed,
|
|
8
9
|
hasNativeRules,
|
|
9
10
|
migrateNativeSource,
|
|
10
11
|
nextReqId,
|
|
12
|
+
renderMachine,
|
|
11
13
|
resolveReq,
|
|
12
14
|
scaffoldSpec,
|
|
15
|
+
type UnboundFeed,
|
|
13
16
|
} from '@llman-sdd/core';
|
|
14
17
|
import type { Command } from 'commander';
|
|
15
18
|
|
|
16
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
addReportOutputOptions,
|
|
21
|
+
CliError,
|
|
22
|
+
loadCliConfig,
|
|
23
|
+
loadSpecEntries,
|
|
24
|
+
newIo,
|
|
25
|
+
resolveOutMode,
|
|
26
|
+
} from '../cli-shared.ts';
|
|
17
27
|
|
|
18
28
|
/** Walk paths collecting .feature files (directories recurse). */
|
|
19
29
|
function collectFeatureFiles(paths: string[]): string[] {
|
|
@@ -178,4 +188,31 @@ export function registerSpec(program: Command): void {
|
|
|
178
188
|
console.log('harness:');
|
|
179
189
|
for (const h of resolved.harness) console.log(` - ${h}`);
|
|
180
190
|
});
|
|
191
|
+
|
|
192
|
+
const unbound = spec
|
|
193
|
+
.command('unbound')
|
|
194
|
+
.description('List unbound requirements (no runnable nested scenario) for implementation')
|
|
195
|
+
.option('--limit <N>', 'max entries to return (default 1; 0 = all)');
|
|
196
|
+
addReportOutputOptions(unbound);
|
|
197
|
+
unbound.action(
|
|
198
|
+
(options: { limit?: string; output?: string; json?: boolean; compactJson?: boolean }) => {
|
|
199
|
+
const raw = options.limit ?? '1';
|
|
200
|
+
if (!/^\d+$/u.test(raw)) {
|
|
201
|
+
throw new CliError(`invalid --limit: ${raw} (non-negative integer)`, 2);
|
|
202
|
+
}
|
|
203
|
+
const limit = Number(raw);
|
|
204
|
+
const feed: UnboundFeed = buildUnboundFeed(loadSpecEntries(), limit);
|
|
205
|
+
const mode = resolveOutMode(options.output, options.json, options.compactJson);
|
|
206
|
+
if (mode !== 'human') {
|
|
207
|
+
console.log(renderMachine(feed, mode));
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
console.log(`spec unbound: ${feed.returned} shown of ${feed.total} total`);
|
|
211
|
+
for (const r of feed.requirements) {
|
|
212
|
+
console.log(` - [${r.reqId}] ${r.featurePath} :: ${r.title}`);
|
|
213
|
+
for (const line of r.statement.split('\n')) console.log(` ${line}`);
|
|
214
|
+
}
|
|
215
|
+
if (feed.hint !== '') console.log(feed.hint);
|
|
216
|
+
},
|
|
217
|
+
);
|
|
181
218
|
}
|
package/src/commands/validate.ts
CHANGED
|
@@ -258,7 +258,7 @@ let harnessBannerShown = false;
|
|
|
258
258
|
* --check → check=true, --no-check → check=false, absent → undefined.
|
|
259
259
|
*/
|
|
260
260
|
function makeHarnessGate(options: { check?: boolean }): HarnessGate {
|
|
261
|
-
const runCommand = loadCliConfig()?.
|
|
261
|
+
const runCommand = loadCliConfig()?.specs?.check_command ?? null;
|
|
262
262
|
const configured = runCommand !== null && runCommand !== '';
|
|
263
263
|
return {
|
|
264
264
|
nested: process.env.LLMAN_SDD_HARNESS_ACTIVE === '1',
|
|
@@ -269,7 +269,7 @@ function makeHarnessGate(options: { check?: boolean }): HarnessGate {
|
|
|
269
269
|
onBeforeFirstRun: (expanded: string): void => {
|
|
270
270
|
if (harnessBannerShown) return;
|
|
271
271
|
harnessBannerShown = true;
|
|
272
|
-
console.error(`running
|
|
272
|
+
console.error(`running spec check: ${expanded} (use --no-check to skip)`);
|
|
273
273
|
},
|
|
274
274
|
};
|
|
275
275
|
}
|
|
@@ -286,8 +286,8 @@ export function registerValidate(program: Command): void {
|
|
|
286
286
|
.option('--stage <stage>', 'change stage gate: draft | designed | planned | full')
|
|
287
287
|
.option('--strict', 'warnings also make the exit code non-zero')
|
|
288
288
|
.option('--include-info', 'keep INFO-level issues (default: WARNING and above)')
|
|
289
|
-
.option('--no-check', 'skip the
|
|
290
|
-
.option('--check', 'run the
|
|
289
|
+
.option('--no-check', 'skip the spec check')
|
|
290
|
+
.option('--check', 'run the spec check (default when specs.check_command is configured)');
|
|
291
291
|
addReportOutputOptions(validate);
|
|
292
292
|
validate.action(
|
|
293
293
|
(
|
package/src/harness.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CLI HarnessRunner adapter (validation r13/r48): executes the expanded
|
|
3
|
-
*
|
|
3
|
+
* specs.check_command through `sh -c` in the project root, exporting
|
|
4
4
|
* LLMAN_SDD_HARNESS_ACTIVE=1 so harness-spawned validate invocations skip
|
|
5
5
|
* execution (nested guard). Windows (no sh) is out of scope for this change —
|
|
6
6
|
* the spawnError branch surfaces it as an ERROR. stdout and stderr are merged;
|