@llman-sdd/core 0.3.0 → 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.
Files changed (107) hide show
  1. package/package.json +2 -1
  2. package/src/archive/freeze.ts +86 -18
  3. package/src/archive/frozenCard.ts +105 -0
  4. package/src/archive/sevenzip.ts +15 -13
  5. package/src/change/closeOutHarness.ts +29 -0
  6. package/src/change/collect.ts +140 -0
  7. package/src/change/frontmatter.ts +48 -6
  8. package/src/change/id.ts +2 -6
  9. package/src/change/lifecycle.ts +285 -86
  10. package/src/change/nextId.ts +63 -2
  11. package/src/change/resolve.ts +2 -2
  12. package/src/change/tasks.ts +59 -0
  13. package/src/config/changeId.ts +14 -12
  14. package/src/config/load.ts +14 -0
  15. package/src/config/schema.ts +4 -41
  16. package/src/config/surface.ts +6 -36
  17. package/src/context/indexStore.ts +7 -3
  18. package/src/context/retrieve.ts +8 -10
  19. package/src/context/tree.ts +28 -24
  20. package/src/git/spawnGit.ts +90 -2
  21. package/src/index.ts +67 -59
  22. package/src/init/defaultConfig.ts +1 -5
  23. package/src/init/init.ts +19 -4
  24. package/src/ports.ts +1 -7
  25. package/src/project/migrateNotes.ts +104 -0
  26. package/src/render/machine.ts +30 -0
  27. package/src/report/collect.ts +11 -127
  28. package/src/report/graph/analysis.ts +152 -0
  29. package/src/report/graph/deps.ts +30 -0
  30. package/src/report/graph/graphData.ts +53 -0
  31. package/src/report/graph/nodes.ts +130 -0
  32. package/src/report/graph/render.ts +83 -0
  33. package/src/report/graph/types.ts +47 -0
  34. package/src/report/graph.ts +9 -381
  35. package/src/report/show.ts +20 -22
  36. package/src/report/specHelpers.ts +42 -21
  37. package/src/report/specs.ts +23 -25
  38. package/src/review/review.ts +45 -30
  39. package/src/spec/authoring.ts +91 -63
  40. package/src/spec/ir.ts +43 -15
  41. package/src/spec/migrateNative.ts +167 -0
  42. package/src/spec/parser.ts +73 -77
  43. package/src/spec/reqRegistry.ts +8 -9
  44. package/src/templates/embedded.ts +10 -16
  45. package/src/templates/engine.ts +10 -5
  46. package/src/templates/locale.ts +1 -1
  47. package/src/templates/skills.ts +4 -5
  48. package/src/validation/changeCheck.ts +128 -105
  49. package/src/validation/harness.ts +161 -0
  50. package/src/validation/staleness.ts +9 -5
  51. package/src/validation/validate.ts +60 -88
  52. package/templates/en/skills/llman-sdd-apply-cycle.md +20 -28
  53. package/templates/en/skills/llman-sdd-apply.md +58 -76
  54. package/templates/en/skills/llman-sdd-arch-review.md +12 -19
  55. package/templates/en/skills/llman-sdd-archive.md +27 -42
  56. package/templates/en/skills/llman-sdd-continue.md +17 -24
  57. package/templates/en/skills/llman-sdd-draft.md +17 -28
  58. package/templates/en/skills/llman-sdd-explore.md +29 -43
  59. package/templates/en/skills/llman-sdd-ff.md +12 -17
  60. package/templates/en/skills/llman-sdd-graph.md +14 -32
  61. package/templates/en/skills/llman-sdd-propose.md +48 -63
  62. package/templates/en/skills/llman-sdd-quick.md +12 -27
  63. package/templates/en/skills/llman-sdd-research.md +13 -24
  64. package/templates/en/skills/llman-sdd-specs-compact.md +14 -39
  65. package/templates/en/skills/llman-sdd-validate.md +11 -15
  66. package/templates/en/skills/llman-sdd-verify.md +23 -44
  67. package/templates/en/skills/llman-sdd-wayfinder.md +18 -22
  68. package/templates/en/units/skills/cli-footer.md +2 -0
  69. package/templates/en/units/skills/git-native-flow-brief.md +7 -6
  70. package/templates/en/units/skills/git-native-flow.md +21 -11
  71. package/templates/en/units/skills/human-readable-summary.md +2 -3
  72. package/templates/en/units/skills/stage-guard.md +7 -7
  73. package/templates/en/units/skills/structured-protocol.md +5 -8
  74. package/templates/en/units/skills/validation-hints.md +10 -14
  75. package/templates/en/units/spec/feature-contract.md +27 -16
  76. package/templates/en/units/workflow/archive-freeze-guidance.md +6 -3
  77. package/templates/zh-Hans/skills/llman-sdd-apply-cycle.md +23 -31
  78. package/templates/zh-Hans/skills/llman-sdd-apply.md +63 -81
  79. package/templates/zh-Hans/skills/llman-sdd-arch-review.md +21 -28
  80. package/templates/zh-Hans/skills/llman-sdd-archive.md +29 -44
  81. package/templates/zh-Hans/skills/llman-sdd-continue.md +17 -24
  82. package/templates/zh-Hans/skills/llman-sdd-draft.md +18 -29
  83. package/templates/zh-Hans/skills/llman-sdd-explore.md +34 -48
  84. package/templates/zh-Hans/skills/llman-sdd-ff.md +13 -18
  85. package/templates/zh-Hans/skills/llman-sdd-graph.md +16 -34
  86. package/templates/zh-Hans/skills/llman-sdd-propose.md +51 -65
  87. package/templates/zh-Hans/skills/llman-sdd-quick.md +15 -30
  88. package/templates/zh-Hans/skills/llman-sdd-research.md +17 -28
  89. package/templates/zh-Hans/skills/llman-sdd-specs-compact.md +15 -40
  90. package/templates/zh-Hans/skills/llman-sdd-validate.md +11 -15
  91. package/templates/zh-Hans/skills/llman-sdd-verify.md +26 -47
  92. package/templates/zh-Hans/skills/llman-sdd-wayfinder.md +25 -29
  93. package/templates/zh-Hans/units/skills/cli-footer.md +2 -0
  94. package/templates/zh-Hans/units/skills/git-native-flow-brief.md +7 -6
  95. package/templates/zh-Hans/units/skills/git-native-flow.md +22 -12
  96. package/templates/zh-Hans/units/skills/human-readable-summary.md +4 -5
  97. package/templates/zh-Hans/units/skills/stage-guard.md +9 -9
  98. package/templates/zh-Hans/units/skills/structured-protocol.md +5 -8
  99. package/templates/zh-Hans/units/skills/validation-hints.md +10 -14
  100. package/templates/zh-Hans/units/spec/feature-contract.md +25 -16
  101. package/templates/zh-Hans/units/workflow/archive-freeze-guidance.md +6 -2
  102. package/templates/en/skills/llman-sdd-onboard.md +0 -34
  103. package/templates/en/skills/llman-sdd-show.md +0 -24
  104. package/templates/en/units/migrate-prompt.md +0 -28
  105. package/templates/zh-Hans/skills/llman-sdd-onboard.md +0 -34
  106. package/templates/zh-Hans/skills/llman-sdd-show.md +0 -24
  107. 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
+ }
@@ -1,19 +1,22 @@
1
1
  /**
2
2
  * Single-track capability .feature parsing (spec-parsing capability).
3
3
  *
4
- * Language fallback chain (r7): start with the `en` matcher — a
5
- * `# language:` header switches dialect automatically mid-scan; on failure
6
- * retry with the zh-CN matcher (Chinese keywords without a header); only then
7
- * surface a parse error. locale zh-Hans maps to gherkin zh-CN.
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): 'given' | 'when' | 'then' {
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
- function classify(tags: string[]): {
79
- classification: ScenarioIR['classification'];
80
- errors: SpecStructuralError[];
81
- } {
82
- const errors: SpecStructuralError[] = [];
83
- const has = (t: string): boolean => tags.includes(t);
84
- const human = has('human');
85
- const executable = has('executable');
86
- const label = tags.join(',');
87
-
88
- if (has('manual')) {
89
- errors.push({
90
- code: 'tag:manual-removed',
91
- message: `@manual was removed in 0.3.0 — drop the tag (@human already carries the human-judgement semantics) (tags: ${label})`,
92
- });
93
- }
94
- if (human && executable) {
95
- errors.push({
96
- code: 'tag:mutually-exclusive',
97
- message: `@human 与 @executable 互斥(tags: ${label})`,
98
- });
99
- }
100
- const classification: ScenarioIR['classification'] = human
101
- ? 'human'
102
- : executable
103
- ? 'executable'
104
- : 'unclassified';
105
- return { classification, errors };
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 scenarios: ScenarioIR[] = [];
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 && rule.children.some((c) => c.scenario)) {
126
- errors.push({
127
- code: 'rule:nested-scenario',
128
- message: `Rule 块内嵌场景被拒绝(rule: ${rule.name})`,
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 (!scenario) continue;
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, scenarios, errors };
168
+ return { fileName, header, featureName, language, rules, orphans, errors };
173
169
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Global rN registry (spec-parsing capability): @req:rN ids form a single
3
- * global namespace across all capability specs; duplicates are reported with
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 scenario of doc.scenarios) {
25
- for (const reqId of scenario.reqIds) {
26
- const files = byId.get(reqId) ?? [];
27
- if (!files.includes(fileName)) files.push(fileName);
28
- byId.set(reqId, files);
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
- * Bun 1.4.x offers no embedding that survives our Node >= 24 dual-runtime
5
- * constraint: `with { type: "text" }` works in compiled output (probed on
6
- * 1.4.2) but Node cannot load that attribute, and `assets`/`?raw`/`?asset`
7
- * never land in the bundle — so templates are injected through the same
8
- * build-time `define` mechanism as LLMAN_SDD_VERSION: build-binary.ts
9
- * collects packages/core/templates into a root-relative path→content map and
10
- * sets process.env.LLMAN_SDD_EMBEDDED_TEMPLATES to that JSON. Bun inlines
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
- /** Read the build-injected table; undefined when absent or malformed. */
41
- export function embeddedTemplates(): Record<string, string> | undefined {
42
- return resolveEmbeddedTable(process.env.LLMAN_SDD_EMBEDDED_TEMPLATES);
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
  /**
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Template engine (init-generators capability, r17): nunjucks adapter with
3
- * minijinja-compatible semantics used by v1 — string-only globals, undefined
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 (v1 parity). \r? first —
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, { autoescape: false });
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
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Locale normalization + fallback chain (init-generators capability, r18).
3
- * Port of v1 config.rs normalize_locale / locale_fallbacks.
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();
@@ -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 v1 templates.rs / update_skills.rs / init.rs semantics.
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 (v1 config.rs effective_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 (v1 BTreeMap<String, String> semantics). */
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 ?? []);