@llman-sdd/core 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 +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 +37 -6
- package/src/init/defaultConfig.ts +8 -8
- package/src/init/init.ts +1 -1
- package/src/report/specHelpers.ts +13 -11
- package/src/report/specs.ts +16 -16
- package/src/report/unbound.ts +70 -0
- package/src/review/review.ts +9 -9
- package/src/spec/authoring.ts +67 -19
- package/src/spec/ir.ts +10 -0
- package/src/spec/keywords.ts +147 -0
- package/src/spec/migrateNative.ts +41 -7
- package/src/spec/parser.ts +22 -6
- package/src/spec/reqRegistry.ts +30 -13
- package/src/templates/skills.ts +10 -10
- package/src/validation/harness.ts +12 -12
- 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,14 +33,28 @@ 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,
|
|
34
40
|
parseCapability,
|
|
35
41
|
parseFeatureSource,
|
|
42
|
+
sourceDialect,
|
|
36
43
|
} from './spec/parser.ts';
|
|
37
|
-
export {
|
|
44
|
+
export {
|
|
45
|
+
BLOCK_KEYWORD_LINE_RE,
|
|
46
|
+
officialKeywords,
|
|
47
|
+
officialKeywordsOrEn,
|
|
48
|
+
stepKeywordToOfficialKind,
|
|
49
|
+
STEP_KIND_BY_KEYWORD,
|
|
50
|
+
type GherkinKeywords,
|
|
51
|
+
} from './spec/keywords.ts';
|
|
52
|
+
export {
|
|
53
|
+
buildReqRegistry,
|
|
54
|
+
type RegistryDuplicate,
|
|
55
|
+
type RegistryOccurrence,
|
|
56
|
+
type ReqRegistry,
|
|
57
|
+
} from './spec/reqRegistry.ts';
|
|
38
58
|
export {
|
|
39
59
|
analyzeLegacy,
|
|
40
60
|
hasNativeRules,
|
|
@@ -150,7 +170,7 @@ export {
|
|
|
150
170
|
export {
|
|
151
171
|
ETHICS_KEYS,
|
|
152
172
|
buildTemplateVars,
|
|
153
|
-
|
|
173
|
+
effectiveCheckCommand,
|
|
154
174
|
enforceEthicsGovernance,
|
|
155
175
|
loadLocaleResource,
|
|
156
176
|
loadSkillTemplates,
|
|
@@ -183,11 +203,22 @@ export {
|
|
|
183
203
|
type ChangeSummary,
|
|
184
204
|
} from './change/collect.ts';
|
|
185
205
|
export { renderChangesJson, renderChangesList } from './report/collect.ts';
|
|
206
|
+
export {
|
|
207
|
+
buildUnboundFeed,
|
|
208
|
+
collectUnboundRequirements,
|
|
209
|
+
type UnboundFeed,
|
|
210
|
+
type UnboundRequirement,
|
|
211
|
+
} from './report/unbound.ts';
|
|
186
212
|
export { graphData, graphMermaid, type GraphDataIr, type GraphFsIo } from './report/graph.ts';
|
|
187
213
|
export { parseDeps } from './report/graph.ts';
|
|
188
214
|
export { renderMachine, type MachineFormat } from './render/machine.ts';
|
|
189
215
|
export { showChangeJson, type ShowDeps, type ShowFsIo } from './report/show.ts';
|
|
190
|
-
export {
|
|
216
|
+
export {
|
|
217
|
+
nextReqId,
|
|
218
|
+
scaffoldSpec,
|
|
219
|
+
skeletonContent,
|
|
220
|
+
type SpecHelperIo,
|
|
221
|
+
} from './report/specHelpers.ts';
|
|
191
222
|
export {
|
|
192
223
|
collectSpecs,
|
|
193
224
|
morphologyOf,
|
|
@@ -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 -->';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { officialKeywords, officialKeywordsOrEn } from '../spec/keywords.ts';
|
|
1
2
|
import { localeToGherkinLang, parseCapability } from '../spec/parser.ts';
|
|
2
3
|
import { buildReqRegistry } from '../spec/reqRegistry.ts';
|
|
3
4
|
/**
|
|
@@ -57,22 +58,23 @@ export function nextReqId(io: SpecHelperIo, specsDir: string): string {
|
|
|
57
58
|
export function skeletonContent(capability: string, reqId: string, locale: string): string {
|
|
58
59
|
const zh = localeFallbacks(locale)[0] === 'zh-Hans';
|
|
59
60
|
// r7: the `# language:` header derives from the locale mapping — never a
|
|
60
|
-
// second hardcoded caliber.
|
|
61
|
-
|
|
61
|
+
// second hardcoded caliber. Table-less locales fall back to en wholesale
|
|
62
|
+
// (header + keywords same-source) so the skeleton stays parseable.
|
|
63
|
+
const mapped = localeToGherkinLang(locale);
|
|
64
|
+
const language = officialKeywords(mapped) !== null ? mapped : 'en';
|
|
65
|
+
const kw = officialKeywordsOrEn(language);
|
|
62
66
|
const header = zh
|
|
63
67
|
? `# language: ${language}\n# capability: ${capability}\n# purpose: TODO: 一句话描述该能力与其目的。\n# scope: llmanspec/`
|
|
64
68
|
: `# language: ${language}\n# capability: ${capability}\n# purpose: TODO: Describe this capability and its purpose.\n# scope: llmanspec/`;
|
|
65
|
-
const feature = zh ? `功能: ${capability}` : `Feature: ${capability}`;
|
|
66
|
-
const ruleKw = zh ? '规则' : 'Rule';
|
|
67
|
-
const scenarioKw = zh ? '场景' : 'Scenario';
|
|
68
|
-
const ruleTitle = 'TODO-rule';
|
|
69
69
|
const ruleDesc = zh ? 'TODO: 需求描述(自由文本)。' : 'TODO: requirement statement (free text).';
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
// Skeleton placeholder prose is not vocabulary — the zh/en copy stays; keywords do not.
|
|
71
|
+
const steps = zh
|
|
72
|
+
? ` ${kw.given} TODO 前置\n ${kw.when} TODO 动作\n ${kw.thenText} TODO 断言`
|
|
73
|
+
: ` ${kw.given} TODO precondition\n ${kw.when} TODO action\n ${kw.thenText} TODO assertion`;
|
|
74
|
+
// native v2 skeleton: a rule block (with @req handle) + one nested example
|
|
72
75
|
return (
|
|
73
|
-
`${header}\n\n${feature}\n\n @req:${reqId}\n ${
|
|
74
|
-
` ${ruleDesc}\n\n ${
|
|
75
|
-
` 假如 TODO 前置\n 当 TODO 动作\n 那么 TODO 断言\n`
|
|
76
|
+
`${header}\n\n${kw.feature}: ${capability}\n\n @req:${reqId}\n ${kw.rule}: TODO-rule\n` +
|
|
77
|
+
` ${ruleDesc}\n\n ${kw.scenario}: TODO-acceptance\n${steps}\n`
|
|
76
78
|
);
|
|
77
79
|
}
|
|
78
80
|
|
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})`);
|