@llman-sdd/core 0.3.1 → 0.5.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 -1
- package/src/archive/freeze.ts +86 -18
- package/src/archive/frozenCard.ts +105 -0
- package/src/archive/sevenzip.ts +15 -13
- package/src/change/closeOutHarness.ts +29 -0
- package/src/change/collect.ts +140 -0
- package/src/change/frontmatter.ts +48 -6
- package/src/change/id.ts +2 -6
- package/src/change/lifecycle.ts +285 -86
- package/src/change/nextId.ts +63 -2
- package/src/change/resolve.ts +2 -2
- package/src/change/tasks.ts +59 -0
- package/src/config/changeId.ts +14 -12
- package/src/config/load.ts +14 -0
- package/src/config/schema.ts +4 -41
- package/src/config/surface.ts +6 -36
- package/src/context/indexStore.ts +7 -3
- package/src/context/retrieve.ts +8 -10
- package/src/context/tree.ts +28 -24
- package/src/git/spawnGit.ts +90 -2
- package/src/index.ts +67 -59
- package/src/init/defaultConfig.ts +1 -5
- package/src/init/init.ts +19 -4
- package/src/ports.ts +1 -7
- package/src/project/migrateNotes.ts +104 -0
- package/src/render/machine.ts +30 -0
- package/src/report/collect.ts +11 -127
- package/src/report/graph/analysis.ts +152 -0
- package/src/report/graph/deps.ts +30 -0
- package/src/report/graph/graphData.ts +53 -0
- package/src/report/graph/nodes.ts +130 -0
- package/src/report/graph/render.ts +83 -0
- package/src/report/graph/types.ts +47 -0
- package/src/report/graph.ts +9 -381
- package/src/report/show.ts +20 -22
- package/src/report/specHelpers.ts +42 -21
- package/src/report/specs.ts +23 -25
- package/src/review/review.ts +45 -30
- package/src/spec/authoring.ts +91 -63
- package/src/spec/ir.ts +43 -15
- package/src/spec/migrateNative.ts +167 -0
- package/src/spec/parser.ts +73 -77
- package/src/spec/reqRegistry.ts +8 -9
- package/src/templates/embedded.ts +10 -16
- package/src/templates/engine.ts +10 -5
- package/src/templates/locale.ts +1 -1
- package/src/templates/skills.ts +4 -5
- package/src/validation/changeCheck.ts +128 -105
- package/src/validation/harness.ts +161 -0
- package/src/validation/staleness.ts +9 -5
- package/src/validation/validate.ts +60 -88
- package/templates/en/skills/llman-sdd-apply-cycle.md +20 -28
- package/templates/en/skills/llman-sdd-apply.md +58 -76
- package/templates/en/skills/llman-sdd-arch-review.md +12 -19
- package/templates/en/skills/llman-sdd-archive.md +27 -42
- package/templates/en/skills/llman-sdd-continue.md +17 -24
- package/templates/en/skills/llman-sdd-draft.md +17 -28
- package/templates/en/skills/llman-sdd-explore.md +29 -43
- package/templates/en/skills/llman-sdd-ff.md +12 -17
- package/templates/en/skills/llman-sdd-graph.md +14 -32
- package/templates/en/skills/llman-sdd-propose.md +48 -63
- package/templates/en/skills/llman-sdd-quick.md +12 -27
- package/templates/en/skills/llman-sdd-research.md +13 -24
- package/templates/en/skills/llman-sdd-specs-compact.md +14 -39
- package/templates/en/skills/llman-sdd-validate.md +11 -15
- package/templates/en/skills/llman-sdd-verify.md +23 -44
- package/templates/en/skills/llman-sdd-wayfinder.md +18 -22
- package/templates/en/units/skills/cli-footer.md +2 -0
- package/templates/en/units/skills/git-native-flow-brief.md +7 -6
- package/templates/en/units/skills/git-native-flow.md +21 -11
- package/templates/en/units/skills/human-readable-summary.md +2 -3
- package/templates/en/units/skills/stage-guard.md +7 -7
- package/templates/en/units/skills/structured-protocol.md +5 -8
- package/templates/en/units/skills/validation-hints.md +10 -14
- package/templates/en/units/spec/feature-contract.md +27 -16
- package/templates/en/units/workflow/archive-freeze-guidance.md +6 -3
- package/templates/zh-Hans/skills/llman-sdd-apply-cycle.md +23 -31
- package/templates/zh-Hans/skills/llman-sdd-apply.md +63 -81
- package/templates/zh-Hans/skills/llman-sdd-arch-review.md +21 -28
- package/templates/zh-Hans/skills/llman-sdd-archive.md +29 -44
- package/templates/zh-Hans/skills/llman-sdd-continue.md +17 -24
- package/templates/zh-Hans/skills/llman-sdd-draft.md +18 -29
- package/templates/zh-Hans/skills/llman-sdd-explore.md +34 -48
- package/templates/zh-Hans/skills/llman-sdd-ff.md +13 -18
- package/templates/zh-Hans/skills/llman-sdd-graph.md +16 -34
- package/templates/zh-Hans/skills/llman-sdd-propose.md +51 -65
- package/templates/zh-Hans/skills/llman-sdd-quick.md +15 -30
- package/templates/zh-Hans/skills/llman-sdd-research.md +17 -28
- package/templates/zh-Hans/skills/llman-sdd-specs-compact.md +15 -40
- package/templates/zh-Hans/skills/llman-sdd-validate.md +11 -15
- package/templates/zh-Hans/skills/llman-sdd-verify.md +26 -47
- package/templates/zh-Hans/skills/llman-sdd-wayfinder.md +25 -29
- package/templates/zh-Hans/units/skills/cli-footer.md +2 -0
- package/templates/zh-Hans/units/skills/git-native-flow-brief.md +7 -6
- package/templates/zh-Hans/units/skills/git-native-flow.md +22 -12
- package/templates/zh-Hans/units/skills/human-readable-summary.md +4 -5
- package/templates/zh-Hans/units/skills/stage-guard.md +9 -9
- package/templates/zh-Hans/units/skills/structured-protocol.md +5 -8
- package/templates/zh-Hans/units/skills/validation-hints.md +10 -14
- package/templates/zh-Hans/units/spec/feature-contract.md +25 -16
- package/templates/zh-Hans/units/workflow/archive-freeze-guidance.md +6 -2
- package/templates/en/skills/llman-sdd-onboard.md +0 -34
- package/templates/en/skills/llman-sdd-show.md +0 -24
- package/templates/en/units/migrate-prompt.md +0 -28
- package/templates/zh-Hans/skills/llman-sdd-onboard.md +0 -34
- package/templates/zh-Hans/skills/llman-sdd-show.md +0 -24
- package/templates/zh-Hans/units/migrate-prompt.md +0 -28
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Legacy → native spec migration (v1 tag-based flat layout → native `规则:`
|
|
3
|
+
* blocks with nested `场景:`). Pure text transform with IO injected by the
|
|
4
|
+
* caller. Used by `spec migrate-native` (interactive + --dry-run) and the
|
|
5
|
+
* in-repo one-shot migration.
|
|
6
|
+
*
|
|
7
|
+
* Parsing ALWAYS goes through the official @cucumber/gherkin parser
|
|
8
|
+
* (parseFeatureSource) — no hand-rolled line scanning; the legacy ROLE is
|
|
9
|
+
* derived from the tag set the official parser exposes per scenario.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { parseFeatureSource } from './parser.ts';
|
|
13
|
+
|
|
14
|
+
export interface MigrateBlock {
|
|
15
|
+
reqIds: string[];
|
|
16
|
+
isRule: boolean;
|
|
17
|
+
skip: boolean;
|
|
18
|
+
title: string;
|
|
19
|
+
/** Statement description lines for rules; step [keyword,text] pairs for acceptances. */
|
|
20
|
+
descriptionLines: string[];
|
|
21
|
+
steps: { keyword: string; text: string }[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface MigrateAnalysis {
|
|
25
|
+
blocks: MigrateBlock[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type MigrateResult =
|
|
29
|
+
| { ok: true; content: string; rules: number; scenarios: number }
|
|
30
|
+
| { ok: false; message: string };
|
|
31
|
+
|
|
32
|
+
const REQ_TAG_RE = /^@?req:(r\d+)$/u;
|
|
33
|
+
|
|
34
|
+
function tagsOf(tags: readonly { name: string }[]): {
|
|
35
|
+
reqIds: string[];
|
|
36
|
+
isRule: boolean;
|
|
37
|
+
skip: boolean;
|
|
38
|
+
} {
|
|
39
|
+
const names = tags.map((t) => t.name);
|
|
40
|
+
const reqIds = names
|
|
41
|
+
.map((n) => n.match(REQ_TAG_RE)?.[1])
|
|
42
|
+
.filter((v): v is string => v !== undefined);
|
|
43
|
+
const isRule = names.some((n) => n === '@rule' || n === '@human');
|
|
44
|
+
const skip = names.some((n) => n === '@skip' || n === '@experimental');
|
|
45
|
+
return { reqIds, isRule, skip };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* True when the source already uses the native layout (has at least one
|
|
50
|
+
* top-level `规则:` block). Uses the official parser.
|
|
51
|
+
*/
|
|
52
|
+
export function hasNativeRules(source: string): boolean {
|
|
53
|
+
try {
|
|
54
|
+
const { doc } = parseFeatureSource(source);
|
|
55
|
+
return (doc.feature?.children ?? []).some((c) => c.rule !== undefined);
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Analyze a legacy source through the official parser: every top-level
|
|
63
|
+
* scenario becomes a block; its role comes from the tags, its body from the
|
|
64
|
+
* official description/step fields.
|
|
65
|
+
*/
|
|
66
|
+
export function analyzeLegacy(source: string): MigrateAnalysis | { ok: false; message: string } {
|
|
67
|
+
const { doc } = parseFeatureSource(source);
|
|
68
|
+
const blocks: MigrateBlock[] = [];
|
|
69
|
+
for (const child of doc.feature?.children ?? []) {
|
|
70
|
+
if (child.rule) {
|
|
71
|
+
// native source — nothing to migrate here; caller should skip
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const sc = child.scenario;
|
|
75
|
+
if (!sc) continue;
|
|
76
|
+
const { reqIds, isRule, skip } = tagsOf(sc.tags);
|
|
77
|
+
blocks.push({
|
|
78
|
+
reqIds,
|
|
79
|
+
isRule,
|
|
80
|
+
skip,
|
|
81
|
+
title: sc.name,
|
|
82
|
+
descriptionLines: (sc.description ?? '')
|
|
83
|
+
.split('\n')
|
|
84
|
+
.map((l) => l.trim())
|
|
85
|
+
.filter((l) => l !== ''),
|
|
86
|
+
steps: sc.steps.map((s) => ({ keyword: s.keyword.trim(), text: s.text.trim() })),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return { blocks };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Strip `- ` list markers from a rule-statement line (legacy prose residue). */
|
|
93
|
+
function stripBullet(line: string): string {
|
|
94
|
+
return line.replace(/^-\s+/u, '');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Migrate one legacy feature source to the native layout. Structural rules:
|
|
99
|
+
* rules keep file order; acceptances nest under the rule carrying their
|
|
100
|
+
* @req id (file order); acceptances without a matching rule stay top-level
|
|
101
|
+
* orphans. Non-scenario lines (headers/feature/comments) are NOT touched —
|
|
102
|
+
* the native output only emits tag/rule/scenario lines, so the preamble is
|
|
103
|
+
* reconstructed by the caller from the original file.
|
|
104
|
+
*/
|
|
105
|
+
export function migrateNativeSource(source: string): MigrateResult {
|
|
106
|
+
const analysis = analyzeLegacy(source);
|
|
107
|
+
if ('ok' in analysis) return { ok: false, message: analysis.message };
|
|
108
|
+
|
|
109
|
+
const { blocks } = analysis as MigrateAnalysis;
|
|
110
|
+
if (blocks.length === 0) {
|
|
111
|
+
return { ok: false, message: 'no legacy scenarios found (already native?)' };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// preamble: everything before the first top-level tag line (headers,
|
|
115
|
+
// feature line, comments) — cut textually, preserved verbatim.
|
|
116
|
+
const lines = source.split('\n');
|
|
117
|
+
let preEnd = 0;
|
|
118
|
+
for (let i = 0; i < lines.length; i++) {
|
|
119
|
+
if ((lines[i] ?? '').startsWith(' @')) break;
|
|
120
|
+
preEnd = i + 1;
|
|
121
|
+
}
|
|
122
|
+
const preamble = lines.slice(0, preEnd);
|
|
123
|
+
|
|
124
|
+
const out: string[] = [...preamble];
|
|
125
|
+
let rules = 0;
|
|
126
|
+
let scenarios = 0;
|
|
127
|
+
const consumed = new Set<number>();
|
|
128
|
+
|
|
129
|
+
for (const b of blocks) {
|
|
130
|
+
if (!b.isRule) continue;
|
|
131
|
+
rules++;
|
|
132
|
+
out.push(` @req:${b.reqIds[0] ?? ''}`);
|
|
133
|
+
out.push(` 规则: ${b.title}`);
|
|
134
|
+
for (const line of b.descriptionLines) {
|
|
135
|
+
const text = stripBullet(line);
|
|
136
|
+
if (text !== '') out.push(` ${text}`);
|
|
137
|
+
}
|
|
138
|
+
for (const [ai, a] of blocks.entries()) {
|
|
139
|
+
if (a.isRule || consumed.has(ai)) continue;
|
|
140
|
+
if (!a.reqIds.some((rid) => b.reqIds.includes(rid))) continue;
|
|
141
|
+
consumed.add(ai);
|
|
142
|
+
scenarios++;
|
|
143
|
+
out.push('');
|
|
144
|
+
if (a.skip) out.push(' @skip');
|
|
145
|
+
out.push(` 场景: ${a.title}`);
|
|
146
|
+
for (const s of a.steps) out.push(` ${s.keyword} ${s.text}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Unbound acceptance scenarios (no matching rule) are emitted last at
|
|
150
|
+
// top level — native feature-level examples with no requirement handle.
|
|
151
|
+
// Under official Gherkin parsing they are absorbed into the preceding rule,
|
|
152
|
+
// which is their natural functional home; no special signal exists for them.
|
|
153
|
+
for (const [ai, a] of blocks.entries()) {
|
|
154
|
+
if (a.isRule || consumed.has(ai)) continue;
|
|
155
|
+
consumed.add(ai);
|
|
156
|
+
scenarios++;
|
|
157
|
+
out.push('');
|
|
158
|
+
if (a.skip) out.push(' @skip');
|
|
159
|
+
out.push(` 场景: ${a.title}`);
|
|
160
|
+
for (const s of a.steps) out.push(` ${s.keyword} ${s.text}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (rules === 0) {
|
|
164
|
+
return { ok: false, message: 'no legacy rule scenarios found (@req + @rule/@human)' };
|
|
165
|
+
}
|
|
166
|
+
return { ok: true, content: `${out.join('\n')}\n`, rules, scenarios };
|
|
167
|
+
}
|
package/src/spec/parser.ts
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Single-track capability .feature parsing (spec-parsing capability).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Native v2 model: `功能:` → `规则:` blocks (requirement: title + free-form
|
|
5
|
+
* description + `@req:<id>` handle) → nested `场景:` (executable examples).
|
|
6
|
+
* Top-level `场景:` outside any rule become orphans. Legacy role tags
|
|
7
|
+
* (@human/@rule/@executable/@manual) are inert — the parser only reads
|
|
8
|
+
* `@req` (handle) and `@skip`/`@experimental` (runner opt-out). Files in the
|
|
9
|
+
* legacy flat layout must be migrated with `spec migrate-native`.
|
|
8
10
|
*/
|
|
9
11
|
import { AstBuilder, GherkinClassicTokenMatcher, Parser } from '@cucumber/gherkin';
|
|
10
12
|
import type { GherkinDocument } from '@cucumber/messages';
|
|
11
13
|
|
|
12
14
|
import {
|
|
13
|
-
MUST_WORD_RE,
|
|
14
15
|
type CapabilityDoc,
|
|
15
16
|
type CapabilityHeader,
|
|
17
|
+
type RuleIR,
|
|
16
18
|
type ScenarioIR,
|
|
19
|
+
type ScenarioStepKind,
|
|
17
20
|
type SpecStructuralError,
|
|
18
21
|
} from './ir.ts';
|
|
19
22
|
|
|
@@ -52,13 +55,14 @@ export function parseFeatureSource(source: string): { doc: GherkinDocument; lang
|
|
|
52
55
|
const REQ_TAG_RE = /^@?req:(r\d+)$/u;
|
|
53
56
|
|
|
54
57
|
/** Gherkin keyword (zh-CN + en) → step kind; And/But/* inherit via fallback. */
|
|
55
|
-
function stepKeywordToKind(keyword: string):
|
|
58
|
+
function stepKeywordToKind(keyword: string): ScenarioStepKind {
|
|
56
59
|
const kw = keyword.trim();
|
|
57
60
|
if (/^(假如|Given)/iu.test(kw)) return 'given';
|
|
58
61
|
if (/^(当|When)/iu.test(kw)) return 'when';
|
|
59
62
|
if (/^(那么|Then)/iu.test(kw)) return 'then';
|
|
60
63
|
return 'given';
|
|
61
64
|
}
|
|
65
|
+
|
|
62
66
|
const HEADER_RE = /^#\s*(capability|purpose|scope):\s*(.*)$/u;
|
|
63
67
|
|
|
64
68
|
function extractHeader(source: string): CapabilityHeader {
|
|
@@ -75,37 +79,47 @@ function extractHeader(source: string): CapabilityHeader {
|
|
|
75
79
|
return header;
|
|
76
80
|
}
|
|
77
81
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
:
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
82
|
+
interface RawTags {
|
|
83
|
+
names: string[]; // without leading '@'
|
|
84
|
+
reqId: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function parseTags(tags: readonly { name: string }[]): RawTags {
|
|
88
|
+
const names = tags.map((t) => t.name.replace(/^@/u, ''));
|
|
89
|
+
const req = tags.map((t) => t.name.match(REQ_TAG_RE)?.[1]).find((v): v is string => !!v);
|
|
90
|
+
return { names, reqId: req ?? '' };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function collectScenario(sc: {
|
|
94
|
+
tags: readonly { name: string }[];
|
|
95
|
+
name: string;
|
|
96
|
+
steps: readonly { keyword: string; text: string }[];
|
|
97
|
+
}): ScenarioIR {
|
|
98
|
+
const { names, reqId: _ignored } = parseTags(sc.tags);
|
|
99
|
+
const skipTokens = new Set(['skip', 'experimental']);
|
|
100
|
+
const runnable = !names.some((n) => skipTokens.has(n));
|
|
101
|
+
const steps = sc.steps.map((s) => ({
|
|
102
|
+
kind: stepKeywordToKind(s.keyword),
|
|
103
|
+
text: s.text.trim(),
|
|
104
|
+
}));
|
|
105
|
+
return {
|
|
106
|
+
name: sc.name,
|
|
107
|
+
tags: names.filter(
|
|
108
|
+
(n) =>
|
|
109
|
+
!n.startsWith('req') &&
|
|
110
|
+
!skipTokens.has(n) &&
|
|
111
|
+
!n.startsWith('rule') &&
|
|
112
|
+
!n.startsWith('human') &&
|
|
113
|
+
!n.startsWith('executable'),
|
|
114
|
+
),
|
|
115
|
+
runnable,
|
|
116
|
+
stepCount: steps.length,
|
|
117
|
+
steps,
|
|
118
|
+
statement: steps.map((s) => s.text).join('\n'),
|
|
119
|
+
};
|
|
106
120
|
}
|
|
107
121
|
|
|
108
|
-
/** Parse one capability .feature source into the single-track IR. */
|
|
122
|
+
/** Parse one capability .feature source into the native single-track IR. */
|
|
109
123
|
export function parseCapability(source: string, fileName = '<inline>'): CapabilityDoc {
|
|
110
124
|
const errors: SpecStructuralError[] = [];
|
|
111
125
|
const header = extractHeader(source);
|
|
@@ -118,56 +132,38 @@ export function parseCapability(source: string, fileName = '<inline>'): Capabili
|
|
|
118
132
|
const { doc, language } = parseFeatureSource(source);
|
|
119
133
|
const feature = doc.feature;
|
|
120
134
|
const featureName = feature?.name ?? '';
|
|
121
|
-
const
|
|
135
|
+
const rules: RuleIR[] = [];
|
|
136
|
+
const orphans: ScenarioIR[] = [];
|
|
122
137
|
|
|
123
138
|
for (const child of feature?.children ?? []) {
|
|
124
139
|
const rule = child.rule;
|
|
125
|
-
if (rule
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
140
|
+
if (rule) {
|
|
141
|
+
const { names, reqId } = parseTags(rule.tags);
|
|
142
|
+
const description = (rule.description ?? '')
|
|
143
|
+
.split('\n')
|
|
144
|
+
.map((l) => l.trim())
|
|
145
|
+
.filter((l) => l !== '')
|
|
146
|
+
.join('\n');
|
|
147
|
+
const scenarios: ScenarioIR[] = [];
|
|
148
|
+
for (const rc of rule.children) {
|
|
149
|
+
if (rc.scenario) scenarios.push(collectScenario(rc.scenario));
|
|
150
|
+
}
|
|
151
|
+
rules.push({
|
|
152
|
+
reqId,
|
|
153
|
+
title: rule.name,
|
|
154
|
+
description,
|
|
155
|
+
scenarios,
|
|
156
|
+
tags: names.filter(
|
|
157
|
+
(n) => !n.startsWith('req') && !n.startsWith('rule') && !n.startsWith('human'),
|
|
158
|
+
),
|
|
129
159
|
});
|
|
130
160
|
continue;
|
|
131
161
|
}
|
|
132
162
|
const scenario = child.scenario;
|
|
133
|
-
if (
|
|
134
|
-
|
|
135
|
-
const tags = scenario.tags.map((t) => t.name.replace(/^@/u, ''));
|
|
136
|
-
const reqIds = scenario.tags
|
|
137
|
-
.map((t) => t.name.match(REQ_TAG_RE)?.[1])
|
|
138
|
-
.filter((v): v is string => v !== undefined);
|
|
139
|
-
|
|
140
|
-
const kind = classify(tags);
|
|
141
|
-
errors.push(...kind.errors);
|
|
142
|
-
|
|
143
|
-
const description = (scenario.description ?? '')
|
|
144
|
-
.split('\n')
|
|
145
|
-
.map((l) => l.trim())
|
|
146
|
-
.filter((l) => l !== '');
|
|
147
|
-
const stepTexts = scenario.steps.map((s) => s.text.trim());
|
|
148
|
-
const statement = [...description, ...stepTexts].join('\n');
|
|
149
|
-
const steps = scenario.steps.map((s) => ({
|
|
150
|
-
kind: stepKeywordToKind(s.keyword),
|
|
151
|
-
text: s.text.trim(),
|
|
152
|
-
}));
|
|
153
|
-
|
|
154
|
-
if (kind.classification === 'human' && !MUST_WORD_RE.test(statement)) {
|
|
155
|
-
errors.push({
|
|
156
|
-
code: 'rule:missing-must-word',
|
|
157
|
-
message: `@human 规则场景描述必须含 MUST/SHALL(scenario: ${scenario.name})`,
|
|
158
|
-
});
|
|
163
|
+
if (scenario) {
|
|
164
|
+
orphans.push(collectScenario(scenario));
|
|
159
165
|
}
|
|
160
|
-
|
|
161
|
-
scenarios.push({
|
|
162
|
-
name: scenario.name,
|
|
163
|
-
tags,
|
|
164
|
-
reqIds,
|
|
165
|
-
classification: kind.classification,
|
|
166
|
-
statement,
|
|
167
|
-
stepCount: stepTexts.length,
|
|
168
|
-
steps,
|
|
169
|
-
});
|
|
170
166
|
}
|
|
171
167
|
|
|
172
|
-
return { fileName, header, featureName, language,
|
|
168
|
+
return { fileName, header, featureName, language, rules, orphans, errors };
|
|
173
169
|
}
|
package/src/spec/reqRegistry.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Global rN registry (spec-parsing capability): @req:rN ids
|
|
3
|
-
* global namespace across all capability specs; duplicates are
|
|
4
|
-
* the conflicting file pairs.
|
|
2
|
+
* Global rN registry (spec-parsing capability): @req:rN ids on `规则:` blocks
|
|
3
|
+
* form a single global namespace across all capability specs; duplicates are
|
|
4
|
+
* reported with the conflicting file pairs.
|
|
5
5
|
*/
|
|
6
6
|
import type { CapabilityDoc } from './ir.ts';
|
|
7
7
|
|
|
@@ -21,12 +21,11 @@ export function buildReqRegistry(
|
|
|
21
21
|
): ReqRegistry {
|
|
22
22
|
const byId = new Map<string, string[]>();
|
|
23
23
|
for (const { fileName, doc } of docs) {
|
|
24
|
-
for (const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
24
|
+
for (const rule of doc.rules) {
|
|
25
|
+
if (rule.reqId === '') continue;
|
|
26
|
+
const files = byId.get(rule.reqId) ?? [];
|
|
27
|
+
if (!files.includes(fileName)) files.push(fileName);
|
|
28
|
+
byId.set(rule.reqId, files);
|
|
30
29
|
}
|
|
31
30
|
}
|
|
32
31
|
const duplicates: RegistryDuplicate[] = [];
|
|
@@ -1,19 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Embedded template table for single-file binaries.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* define values as raw expressions — the JSON is a valid object literal, so
|
|
12
|
-
* in the compiled binary the expression IS the table object itself, while a
|
|
13
|
-
* JSON string reaches these callers through the same expression in other
|
|
14
|
-
* engines or when force-set via env. Both forms are accepted below; every
|
|
15
|
-
* non-compiled run (source, npm package, Node) leaves the define unset and
|
|
16
|
-
* falls back to the real filesystem via TEMPLATES_ROOT.
|
|
4
|
+
* Compiled binaries receive the table through the same build-time define
|
|
5
|
+
* mechanism as the version (see apps/cli/src/cli-shared.ts — the define read
|
|
6
|
+
* lives at the CLI seam, core only receives already-resolved values). Bun
|
|
7
|
+
* inlines define values as raw expressions; both the JSON-string form and a
|
|
8
|
+
* pre-parsed object literal are accepted below. Every non-compiled run
|
|
9
|
+
* (source, npm package, Node) leaves the value unset and falls back to the
|
|
10
|
+
* real filesystem via TEMPLATES_ROOT.
|
|
17
11
|
*/
|
|
18
12
|
import type { TemplateIo } from './skills.ts';
|
|
19
13
|
|
|
@@ -37,9 +31,9 @@ export function resolveEmbeddedTable(value: unknown): Record<string, string> | u
|
|
|
37
31
|
return undefined;
|
|
38
32
|
}
|
|
39
33
|
|
|
40
|
-
/**
|
|
41
|
-
export function embeddedTemplates(): Record<string, string> | undefined {
|
|
42
|
-
return resolveEmbeddedTable(
|
|
34
|
+
/** Resolve the caller-passed define value; undefined when absent or malformed. */
|
|
35
|
+
export function embeddedTemplates(raw?: unknown): Record<string, string> | undefined {
|
|
36
|
+
return resolveEmbeddedTable(raw);
|
|
43
37
|
}
|
|
44
38
|
|
|
45
39
|
/**
|
package/src/templates/engine.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Template engine (init-generators capability, r17): nunjucks adapter with
|
|
3
|
-
* minijinja-compatible semantics used by
|
|
3
|
+
* minijinja-compatible semantics used by predecessor — string-only globals, undefined
|
|
4
4
|
* renders empty (Lenient), nested `unit(id)` expansion capped at 32, and
|
|
5
5
|
* trailing whitespace trimmed from the final product.
|
|
6
6
|
*/
|
|
@@ -22,22 +22,26 @@ export function renderWithUnits(
|
|
|
22
22
|
units: UnitRegistry,
|
|
23
23
|
vars: Record<string, string>,
|
|
24
24
|
depth = 0,
|
|
25
|
+
opts: { throwOnUndefined?: boolean } = {},
|
|
25
26
|
): string {
|
|
26
27
|
if (depth > MAX_UNIT_NESTING_DEPTH) {
|
|
27
28
|
throw new Error(`unit nesting exceeded ${MAX_UNIT_NESTING_DEPTH}`);
|
|
28
29
|
}
|
|
29
30
|
// minijinja keep_trailing_newline=false: a single trailing newline of the
|
|
30
|
-
// template SOURCE is stripped before rendering (
|
|
31
|
+
// template SOURCE is stripped before rendering (predecessor parity). \r? first —
|
|
31
32
|
// stripping \n alone would strand a trailing \r for CRLF sources.
|
|
32
33
|
const source = raw.replace(/\r?\n$/u, '');
|
|
33
|
-
const env = new nunjucks.Environment(null, {
|
|
34
|
+
const env = new nunjucks.Environment(null, {
|
|
35
|
+
autoescape: false,
|
|
36
|
+
throwOnUndefined: opts.throwOnUndefined === true,
|
|
37
|
+
});
|
|
34
38
|
for (const [key, value] of Object.entries(vars)) {
|
|
35
39
|
env.addGlobal(key, value);
|
|
36
40
|
}
|
|
37
41
|
env.addGlobal('unit', (id: string): string => {
|
|
38
42
|
const content = units.get(id);
|
|
39
43
|
if (content === undefined) throw new MissingUnitError(id);
|
|
40
|
-
return renderWithUnits(content, units, vars, depth + 1);
|
|
44
|
+
return renderWithUnits(content, units, vars, depth + 1, opts);
|
|
41
45
|
});
|
|
42
46
|
return env.renderString(source, {});
|
|
43
47
|
}
|
|
@@ -47,6 +51,7 @@ export function renderTemplate(
|
|
|
47
51
|
raw: string,
|
|
48
52
|
units: UnitRegistry,
|
|
49
53
|
vars: Record<string, string>,
|
|
54
|
+
opts: { throwOnUndefined?: boolean } = {},
|
|
50
55
|
): string {
|
|
51
|
-
return renderWithUnits(raw, units, vars, 0).trimEnd();
|
|
56
|
+
return renderWithUnits(raw, units, vars, 0, opts).trimEnd();
|
|
52
57
|
}
|
package/src/templates/locale.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Locale normalization + fallback chain (init-generators capability, r18).
|
|
3
|
-
* Port of
|
|
3
|
+
* Port of predecessor config.rs normalize_locale / locale_fallbacks.
|
|
4
4
|
*/
|
|
5
5
|
export function normalizeLocale(value: string): string {
|
|
6
6
|
const trimmed = value.trim();
|
package/src/templates/skills.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { SddConfig } from '../config/schema.ts';
|
|
|
2
2
|
import { renderTemplate, type UnitRegistry } from './engine.ts';
|
|
3
3
|
/**
|
|
4
4
|
* Skills rendering + init orchestration (init-generators capability, r19).
|
|
5
|
-
* Port of
|
|
5
|
+
* Port of predecessor templates.rs / update_skills.rs / init.rs semantics.
|
|
6
6
|
*/
|
|
7
7
|
import { localeFallbacks } from './locale.ts';
|
|
8
8
|
|
|
@@ -37,6 +37,7 @@ export const UNIT_FILES: readonly string[] = [
|
|
|
37
37
|
'skills/ethics-governance.md',
|
|
38
38
|
'spec/feature-contract.md',
|
|
39
39
|
'skills/structured-protocol.md',
|
|
40
|
+
'skills/cli-footer.md',
|
|
40
41
|
'workflow/archive-freeze-guidance.md',
|
|
41
42
|
];
|
|
42
43
|
|
|
@@ -48,7 +49,7 @@ export const ETHICS_KEYS: readonly string[] = [
|
|
|
48
49
|
'ethics.escalation_policy',
|
|
49
50
|
];
|
|
50
51
|
|
|
51
|
-
/** Framework-derived run_command (
|
|
52
|
+
/** Framework-derived run_command (predecessor config.rs effective_run_command). */
|
|
52
53
|
export function effectiveRunCommand(bdd: NonNullable<SddConfig['bdd']>): string {
|
|
53
54
|
if (bdd.run_command) return bdd.run_command;
|
|
54
55
|
switch (bdd.framework ?? '') {
|
|
@@ -65,15 +66,13 @@ export function effectiveRunCommand(bdd: NonNullable<SddConfig['bdd']>): string
|
|
|
65
66
|
}
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
/** All-string globals (
|
|
69
|
+
/** All-string globals (predecessor BTreeMap<String, String> semantics). */
|
|
69
70
|
export function buildTemplateVars(config: SddConfig, version: string): Record<string, string> {
|
|
70
71
|
const vars: Record<string, string> = { llman_version: version };
|
|
71
72
|
if (config.bdd) {
|
|
72
73
|
vars['bdd_enabled'] = 'true';
|
|
73
74
|
vars['bdd_framework'] = config.bdd.framework ?? '';
|
|
74
|
-
if (config.bdd.feature_dir) vars['bdd_feature_dir'] = config.bdd.feature_dir;
|
|
75
75
|
vars['bdd_run_command'] = effectiveRunCommand(config.bdd);
|
|
76
|
-
if (config.bdd.default_language) vars['bdd_default_language'] = config.bdd.default_language;
|
|
77
76
|
if (config.bdd.verify_prompt) vars['bdd_verify_prompt'] = config.bdd.verify_prompt;
|
|
78
77
|
}
|
|
79
78
|
const extras = new Set<string>(config.extra_skills ?? []);
|