@lumenflow/core 2.2.2 → 2.3.1

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 (213) hide show
  1. package/dist/active-wu-detector.d.ts +1 -1
  2. package/dist/active-wu-detector.js +1 -1
  3. package/dist/arg-parser.js +51 -18
  4. package/dist/backlog-generator.d.ts +4 -4
  5. package/dist/backlog-generator.js +4 -4
  6. package/dist/backlog-sync-validator.js +1 -1
  7. package/dist/cleanup-lock.d.ts +9 -2
  8. package/dist/cleanup-lock.js +17 -7
  9. package/dist/code-path-validator.d.ts +3 -3
  10. package/dist/code-path-validator.js +3 -3
  11. package/dist/compliance-parser.d.ts +1 -1
  12. package/dist/compliance-parser.js +1 -1
  13. package/dist/constants/backlog-patterns.d.ts +1 -1
  14. package/dist/constants/backlog-patterns.js +1 -1
  15. package/dist/constants/dora-constants.d.ts +1 -1
  16. package/dist/constants/dora-constants.js +1 -1
  17. package/dist/constants/gate-constants.d.ts +1 -1
  18. package/dist/constants/gate-constants.js +1 -1
  19. package/dist/constants/linter-constants.d.ts +1 -1
  20. package/dist/constants/linter-constants.js +1 -1
  21. package/dist/constants/tokenizer-constants.d.ts +1 -1
  22. package/dist/constants/tokenizer-constants.js +1 -1
  23. package/dist/context/location-resolver.js +2 -1
  24. package/dist/context-validation-integration.d.ts +1 -0
  25. package/dist/core/scope-checker.d.ts +3 -3
  26. package/dist/core/scope-checker.js +3 -3
  27. package/dist/core/tool-runner.d.ts +5 -5
  28. package/dist/core/tool-runner.js +5 -5
  29. package/dist/core/tool.constants.d.ts +1 -1
  30. package/dist/core/tool.constants.js +1 -1
  31. package/dist/core/tool.schemas.d.ts +2 -2
  32. package/dist/core/tool.schemas.js +1 -1
  33. package/dist/core/worktree-guard.d.ts +1 -1
  34. package/dist/core/worktree-guard.js +1 -1
  35. package/dist/coverage-gate.d.ts +12 -3
  36. package/dist/coverage-gate.js +15 -8
  37. package/dist/date-utils.d.ts +4 -4
  38. package/dist/date-utils.js +4 -4
  39. package/dist/dependency-graph.d.ts +6 -0
  40. package/dist/dependency-graph.js +43 -2
  41. package/dist/dependency-guard.d.ts +2 -2
  42. package/dist/dependency-guard.js +3 -3
  43. package/dist/dependency-validator.d.ts +4 -4
  44. package/dist/dependency-validator.js +4 -7
  45. package/dist/domain/orchestration.constants.d.ts +31 -10
  46. package/dist/domain/orchestration.constants.js +45 -16
  47. package/dist/domain/orchestration.schemas.d.ts +54 -28
  48. package/dist/domain/orchestration.schemas.js +2 -2
  49. package/dist/domain/orchestration.types.d.ts +2 -2
  50. package/dist/domain/orchestration.types.js +2 -2
  51. package/dist/error-handler.d.ts +10 -10
  52. package/dist/error-handler.js +10 -10
  53. package/dist/file-classifiers.d.ts +6 -6
  54. package/dist/file-classifiers.js +6 -6
  55. package/dist/gates-config.d.ts +74 -0
  56. package/dist/gates-config.js +209 -2
  57. package/dist/git-adapter.d.ts +11 -11
  58. package/dist/git-adapter.js +11 -11
  59. package/dist/git-context-extractor.d.ts +112 -0
  60. package/dist/git-context-extractor.js +559 -0
  61. package/dist/hardcoded-strings.d.ts +1 -1
  62. package/dist/hardcoded-strings.js +1 -1
  63. package/dist/incremental-lint.d.ts +1 -1
  64. package/dist/incremental-lint.js +2 -2
  65. package/dist/incremental-test.d.ts +1 -1
  66. package/dist/incremental-test.js +1 -1
  67. package/dist/index.d.ts +13 -0
  68. package/dist/index.js +25 -0
  69. package/dist/invariants/check-automated-tests.d.ts +2 -2
  70. package/dist/invariants/check-automated-tests.js +3 -3
  71. package/dist/lane-checker.d.ts +28 -7
  72. package/dist/lane-checker.js +316 -159
  73. package/dist/lane-suggest-prompt.d.ts +108 -0
  74. package/dist/lane-suggest-prompt.js +359 -0
  75. package/dist/lane-validator.d.ts +3 -3
  76. package/dist/lane-validator.js +3 -3
  77. package/dist/logs-lib.d.ts +1 -1
  78. package/dist/logs-lib.js +1 -1
  79. package/dist/lumenflow-config-schema.d.ts +162 -0
  80. package/dist/lumenflow-config-schema.js +180 -0
  81. package/dist/manual-test-validator.d.ts +2 -2
  82. package/dist/manual-test-validator.js +3 -3
  83. package/dist/merge-lock.d.ts +8 -1
  84. package/dist/merge-lock.js +16 -7
  85. package/dist/micro-worktree.d.ts +81 -13
  86. package/dist/micro-worktree.js +98 -17
  87. package/dist/migration-deployer.d.ts +1 -1
  88. package/dist/migration-deployer.js +1 -1
  89. package/dist/orchestration-advisory-loader.d.ts +2 -2
  90. package/dist/orchestration-advisory-loader.js +10 -6
  91. package/dist/orchestration-advisory.d.ts +3 -3
  92. package/dist/orchestration-advisory.js +4 -4
  93. package/dist/orchestration-di.d.ts +4 -4
  94. package/dist/orchestration-di.js +4 -4
  95. package/dist/orchestration-rules.d.ts +4 -4
  96. package/dist/orchestration-rules.js +18 -10
  97. package/dist/orphan-detector.d.ts +3 -3
  98. package/dist/orphan-detector.js +3 -3
  99. package/dist/patrol-loop.d.ts +170 -0
  100. package/dist/patrol-loop.js +186 -0
  101. package/dist/process-detector.d.ts +5 -5
  102. package/dist/process-detector.js +5 -5
  103. package/dist/rebase-artifact-cleanup.d.ts +3 -3
  104. package/dist/rebase-artifact-cleanup.js +3 -3
  105. package/dist/resolve-policy.d.ts +195 -0
  106. package/dist/resolve-policy.js +203 -0
  107. package/dist/risk-detector.d.ts +2 -2
  108. package/dist/risk-detector.js +2 -2
  109. package/dist/rollback-utils.d.ts +1 -1
  110. package/dist/rollback-utils.js +1 -1
  111. package/dist/section-headings.d.ts +1 -1
  112. package/dist/section-headings.js +1 -1
  113. package/dist/spawn-escalation.d.ts +4 -4
  114. package/dist/spawn-escalation.js +3 -3
  115. package/dist/spawn-monitor.d.ts +4 -4
  116. package/dist/spawn-monitor.js +4 -4
  117. package/dist/spawn-recovery.d.ts +3 -3
  118. package/dist/spawn-recovery.js +3 -3
  119. package/dist/spawn-registry-schema.d.ts +2 -2
  120. package/dist/spawn-registry-schema.js +2 -2
  121. package/dist/spawn-registry-store.d.ts +2 -2
  122. package/dist/spawn-registry-store.js +2 -2
  123. package/dist/spawn-strategy.d.ts +17 -11
  124. package/dist/spawn-strategy.js +47 -44
  125. package/dist/spawn-tree.d.ts +3 -3
  126. package/dist/spawn-tree.js +3 -3
  127. package/dist/state-cleanup-core.d.ts +205 -0
  128. package/dist/state-cleanup-core.js +240 -0
  129. package/dist/state-doctor-core.d.ts +168 -0
  130. package/dist/state-doctor-core.js +251 -0
  131. package/dist/stream-error-handler.d.ts +67 -0
  132. package/dist/stream-error-handler.js +94 -0
  133. package/dist/telemetry.d.ts +1 -1
  134. package/dist/telemetry.js +1 -1
  135. package/dist/template-loader.d.ts +162 -0
  136. package/dist/template-loader.js +372 -0
  137. package/dist/test-baseline.d.ts +176 -0
  138. package/dist/test-baseline.js +282 -0
  139. package/dist/usecases/get-suggestions.usecase.d.ts +1 -1
  140. package/dist/validation/command-registry.js +37 -0
  141. package/dist/validators/backlog-sync.js +4 -2
  142. package/dist/worktree-scanner.d.ts +1 -1
  143. package/dist/worktree-scanner.js +1 -1
  144. package/dist/worktree-symlink.d.ts +3 -3
  145. package/dist/worktree-symlink.js +3 -3
  146. package/dist/wu-backlog-updater.d.ts +1 -1
  147. package/dist/wu-backlog-updater.js +1 -1
  148. package/dist/wu-claim-helpers.d.ts +1 -1
  149. package/dist/wu-claim-helpers.js +1 -1
  150. package/dist/wu-claim-resume.d.ts +1 -1
  151. package/dist/wu-claim-resume.js +1 -1
  152. package/dist/wu-consistency-checker.d.ts +1 -1
  153. package/dist/wu-consistency-checker.js +17 -11
  154. package/dist/wu-constants.d.ts +73 -21
  155. package/dist/wu-constants.js +65 -22
  156. package/dist/wu-done-branch-only.d.ts +1 -1
  157. package/dist/wu-done-branch-only.js +1 -1
  158. package/dist/wu-done-docs-generate.d.ts +1 -1
  159. package/dist/wu-done-docs-generate.js +1 -1
  160. package/dist/wu-done-messages.d.ts +2 -2
  161. package/dist/wu-done-messages.js +2 -2
  162. package/dist/wu-done-metadata.d.ts +3 -3
  163. package/dist/wu-done-metadata.js +3 -3
  164. package/dist/wu-done-pr.d.ts +1 -1
  165. package/dist/wu-done-pr.js +4 -2
  166. package/dist/wu-done-preflight.d.ts +8 -0
  167. package/dist/wu-done-preflight.js +18 -2
  168. package/dist/wu-done-ui.d.ts +3 -3
  169. package/dist/wu-done-ui.js +3 -3
  170. package/dist/wu-done-validation.d.ts +30 -0
  171. package/dist/wu-done-validation.js +106 -1
  172. package/dist/wu-done-worktree.d.ts +1 -1
  173. package/dist/wu-done-worktree.js +11 -1
  174. package/dist/wu-events-cleanup.d.ts +148 -0
  175. package/dist/wu-events-cleanup.js +401 -0
  176. package/dist/wu-helpers.d.ts +2 -2
  177. package/dist/wu-helpers.js +2 -2
  178. package/dist/wu-id-generator.d.ts +58 -0
  179. package/dist/wu-id-generator.js +103 -0
  180. package/dist/wu-lint.js +1 -1
  181. package/dist/wu-preflight-validators.d.ts +13 -1
  182. package/dist/wu-preflight-validators.js +56 -1
  183. package/dist/wu-recovery.d.ts +2 -2
  184. package/dist/wu-recovery.js +4 -4
  185. package/dist/wu-repair-core.d.ts +5 -5
  186. package/dist/wu-repair-core.js +6 -6
  187. package/dist/wu-schema-normalization.d.ts +1 -1
  188. package/dist/wu-schema-normalization.js +1 -1
  189. package/dist/wu-schema.d.ts +7 -7
  190. package/dist/wu-schema.js +8 -8
  191. package/dist/wu-spawn-context.d.ts +87 -0
  192. package/dist/wu-spawn-context.js +175 -0
  193. package/dist/wu-spawn-helpers.d.ts +1 -1
  194. package/dist/wu-spawn-helpers.js +1 -1
  195. package/dist/wu-spawn.d.ts +177 -4
  196. package/dist/wu-spawn.js +694 -72
  197. package/dist/wu-state-schema.d.ts +1 -1
  198. package/dist/wu-state-schema.js +1 -1
  199. package/dist/wu-state-store.d.ts +3 -3
  200. package/dist/wu-state-store.js +3 -3
  201. package/dist/wu-status-transition.d.ts +1 -1
  202. package/dist/wu-status-transition.js +1 -1
  203. package/dist/wu-status-updater.d.ts +3 -3
  204. package/dist/wu-status-updater.js +3 -3
  205. package/dist/wu-validation-constants.d.ts +2 -2
  206. package/dist/wu-validation-constants.js +2 -2
  207. package/dist/wu-validation.d.ts +3 -3
  208. package/dist/wu-validation.js +3 -3
  209. package/dist/wu-yaml-fixer.d.ts +2 -2
  210. package/dist/wu-yaml-fixer.js +3 -3
  211. package/dist/wu-yaml.d.ts +23 -0
  212. package/dist/wu-yaml.js +76 -2
  213. package/package.json +5 -2
@@ -0,0 +1,372 @@
1
+ /**
2
+ * Template Loader (WU-1253)
3
+ *
4
+ * Loads, parses, and assembles prompt templates from .lumenflow/templates/
5
+ * with YAML frontmatter support. Enables extraction of hardcoded templates
6
+ * from wu-spawn.ts into maintainable markdown files.
7
+ *
8
+ * Features:
9
+ * - YAML frontmatter parsing via gray-matter
10
+ * - Manifest-driven assembly order
11
+ * - Client-specific overrides (templates.claude/, templates.cursor/)
12
+ * - Token replacement ({WU_ID}, {LANE}, etc.)
13
+ * - Conditional template inclusion
14
+ *
15
+ * @see {@link https://lumenflow.dev/reference/template-system/} - Template documentation
16
+ */
17
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
18
+ import { join } from 'node:path';
19
+ import matter from 'gray-matter';
20
+ import yaml from 'yaml';
21
+ import { createError, ErrorCodes } from './error-handler.js';
22
+ const MANIFEST_PATH = '.lumenflow/templates/manifest.yaml';
23
+ const TEMPLATES_DIR = '.lumenflow/templates/spawn-prompt';
24
+ /**
25
+ * Validate a template entry from the manifest.
26
+ * @throws If entry is missing required fields
27
+ */
28
+ function validateTemplateEntry(entry, manifestPath) {
29
+ if (!entry || typeof entry !== 'object') {
30
+ throw createError(ErrorCodes.VALIDATION_ERROR, `manifest.yaml: Each template entry must be an object`, { path: manifestPath });
31
+ }
32
+ const templateEntry = entry;
33
+ const missingFields = [];
34
+ if (!templateEntry.id)
35
+ missingFields.push('id');
36
+ if (!templateEntry.path)
37
+ missingFields.push('path');
38
+ if (typeof templateEntry.required !== 'boolean')
39
+ missingFields.push('required');
40
+ if (typeof templateEntry.order !== 'number')
41
+ missingFields.push('order');
42
+ if (missingFields.length > 0) {
43
+ throw createError(ErrorCodes.VALIDATION_ERROR, `manifest.yaml: Template entry '${templateEntry.id || 'unknown'}' is missing required fields: ${missingFields.join(', ')}`, { path: manifestPath, entry: templateEntry });
44
+ }
45
+ }
46
+ /**
47
+ * Load and parse the template manifest.
48
+ *
49
+ * @param baseDir - Project root directory containing .lumenflow/
50
+ * @returns Parsed manifest with validated structure
51
+ * @throws If manifest is missing or has invalid structure
52
+ */
53
+ export function loadManifest(baseDir) {
54
+ const manifestPath = join(baseDir, MANIFEST_PATH);
55
+ if (!existsSync(manifestPath)) {
56
+ throw createError(ErrorCodes.FILE_NOT_FOUND, `Template manifest.yaml not found at ${manifestPath}. ` +
57
+ `Create .lumenflow/templates/manifest.yaml to define template assembly order.`, { path: manifestPath });
58
+ }
59
+ const content = readFileSync(manifestPath, 'utf-8');
60
+ let parsed;
61
+ try {
62
+ parsed = yaml.parse(content);
63
+ }
64
+ catch (err) {
65
+ const message = err instanceof Error ? err.message : String(err);
66
+ throw createError(ErrorCodes.YAML_PARSE_ERROR, `Failed to parse manifest.yaml: ${message}`, {
67
+ path: manifestPath,
68
+ originalError: message,
69
+ });
70
+ }
71
+ // Validate manifest structure
72
+ if (!parsed || typeof parsed !== 'object') {
73
+ throw createError(ErrorCodes.VALIDATION_ERROR, `manifest.yaml must be a valid YAML object`, {
74
+ path: manifestPath,
75
+ });
76
+ }
77
+ const manifest = parsed;
78
+ if (!manifest.version || typeof manifest.version !== 'string') {
79
+ throw createError(ErrorCodes.VALIDATION_ERROR, `manifest.yaml: 'version' field is required and must be a string`, { path: manifestPath });
80
+ }
81
+ if (!Array.isArray(manifest.templates)) {
82
+ throw createError(ErrorCodes.VALIDATION_ERROR, `manifest.yaml: 'templates' field is required and must be an array`, { path: manifestPath });
83
+ }
84
+ // Validate each template entry using extracted helper
85
+ for (const entry of manifest.templates) {
86
+ validateTemplateEntry(entry, manifestPath);
87
+ }
88
+ // Build validated manifest
89
+ const defaults = manifest.defaults || {};
90
+ return {
91
+ version: manifest.version,
92
+ defaults: {
93
+ tokenFormat: defaults.tokenFormat || '{TOKEN}',
94
+ },
95
+ templates: manifest.templates.map((entry) => {
96
+ const e = entry;
97
+ return {
98
+ id: e.id,
99
+ path: e.path,
100
+ required: e.required,
101
+ order: e.order,
102
+ condition: e.condition,
103
+ };
104
+ }),
105
+ };
106
+ }
107
+ /**
108
+ * Load a single template file with frontmatter parsing.
109
+ *
110
+ * Uses gray-matter with the yaml engine for robust parsing,
111
+ * matching the pattern established in backlog-parser.ts.
112
+ *
113
+ * @param templatePath - Absolute path to template file
114
+ * @returns Parsed template with frontmatter and content
115
+ * @throws If file is missing or frontmatter is invalid
116
+ */
117
+ export function loadTemplate(templatePath) {
118
+ if (!existsSync(templatePath)) {
119
+ throw createError(ErrorCodes.FILE_NOT_FOUND, `Template not found: ${templatePath}`, {
120
+ path: templatePath,
121
+ });
122
+ }
123
+ const fileContent = readFileSync(templatePath, 'utf-8');
124
+ // Parse frontmatter using gray-matter with modern yaml engine
125
+ // Pattern from backlog-parser.ts (WU-1065)
126
+ const { data, content } = matter(fileContent, {
127
+ engines: {
128
+ yaml: {
129
+ parse: yaml.parse.bind(yaml),
130
+ stringify: yaml.stringify.bind(yaml),
131
+ },
132
+ },
133
+ });
134
+ // Validate required frontmatter fields
135
+ const frontmatter = data;
136
+ if (!frontmatter.id || typeof frontmatter.id !== 'string') {
137
+ throw createError(ErrorCodes.VALIDATION_ERROR, `Template ${templatePath}: 'id' field is required in frontmatter`, { path: templatePath });
138
+ }
139
+ if (!frontmatter.name || typeof frontmatter.name !== 'string') {
140
+ throw createError(ErrorCodes.VALIDATION_ERROR, `Template ${templatePath}: 'name' field is required in frontmatter`, { path: templatePath });
141
+ }
142
+ if (typeof frontmatter.required !== 'boolean') {
143
+ throw createError(ErrorCodes.VALIDATION_ERROR, `Template ${templatePath}: 'required' field must be a boolean`, { path: templatePath });
144
+ }
145
+ if (typeof frontmatter.order !== 'number') {
146
+ throw createError(ErrorCodes.VALIDATION_ERROR, `Template ${templatePath}: 'order' field must be a number`, { path: templatePath });
147
+ }
148
+ return {
149
+ frontmatter: {
150
+ id: frontmatter.id,
151
+ name: frontmatter.name,
152
+ required: frontmatter.required,
153
+ order: frontmatter.order,
154
+ tokens: Array.isArray(frontmatter.tokens) ? frontmatter.tokens : undefined,
155
+ condition: typeof frontmatter.condition === 'string' ? frontmatter.condition : undefined,
156
+ },
157
+ content: content.trim(),
158
+ sourcePath: templatePath,
159
+ };
160
+ }
161
+ /**
162
+ * Load all templates from a directory, respecting client overrides.
163
+ *
164
+ * Override resolution order:
165
+ * 1. .lumenflow/templates.{client}/spawn-prompt/{template}.md (highest priority)
166
+ * 2. .lumenflow/templates/spawn-prompt/{template}.md (fallback)
167
+ *
168
+ * @param baseDir - Project root directory
169
+ * @param clientName - Client name for overrides (e.g., 'claude', 'cursor')
170
+ * @returns Map of template id to loaded template
171
+ */
172
+ export function loadTemplatesWithOverrides(baseDir, clientName) {
173
+ const templates = new Map();
174
+ const baseTemplatesDir = join(baseDir, TEMPLATES_DIR);
175
+ const clientTemplatesDir = join(baseDir, `.lumenflow/templates.${clientName}/spawn-prompt`);
176
+ // Load base templates first
177
+ if (existsSync(baseTemplatesDir)) {
178
+ loadTemplatesFromDir(baseTemplatesDir, templates);
179
+ }
180
+ // Override with client-specific templates
181
+ if (existsSync(clientTemplatesDir)) {
182
+ loadTemplatesFromDir(clientTemplatesDir, templates);
183
+ }
184
+ return templates;
185
+ }
186
+ /**
187
+ * Load all templates from a directory into the provided map.
188
+ * Templates are keyed by their frontmatter id.
189
+ */
190
+ function loadTemplatesFromDir(dirPath, templates) {
191
+ const entries = readdirSync(dirPath, { withFileTypes: true });
192
+ for (const entry of entries) {
193
+ const fullPath = join(dirPath, entry.name);
194
+ if (entry.isDirectory()) {
195
+ // Recursively load from subdirectories (e.g., lane-guidance/)
196
+ loadTemplatesFromDir(fullPath, templates);
197
+ }
198
+ else if (entry.name.endsWith('.md')) {
199
+ try {
200
+ const template = loadTemplate(fullPath);
201
+ templates.set(template.frontmatter.id, template);
202
+ }
203
+ catch {
204
+ // Silently skip templates that fail to load (intentional - optional templates)
205
+ }
206
+ }
207
+ }
208
+ }
209
+ /**
210
+ * Normalize context for condition evaluation.
211
+ *
212
+ * Adds lowercase aliases for common fields so conditions can use
213
+ * natural syntax like `type === 'feature'` instead of `TYPE === 'feature'`.
214
+ *
215
+ * Token replacement still uses uppercase keys (WU_ID, LANE).
216
+ */
217
+ function normalizeContextForConditions(context) {
218
+ const normalized = { ...context };
219
+ // Add lowercase aliases for condition evaluation
220
+ if (context.TYPE !== undefined)
221
+ normalized.type = context.TYPE;
222
+ if (context.LANE !== undefined)
223
+ normalized.lane = context.LANE;
224
+ if (context.WU_ID !== undefined)
225
+ normalized.wuId = context.WU_ID;
226
+ if (context.TITLE !== undefined)
227
+ normalized.title = context.TITLE;
228
+ if (context.DESCRIPTION !== undefined)
229
+ normalized.description = context.DESCRIPTION;
230
+ if (context.WORKTREE_PATH !== undefined)
231
+ normalized.worktreePath = context.WORKTREE_PATH;
232
+ return normalized;
233
+ }
234
+ /**
235
+ * Assemble templates in manifest order with token replacement.
236
+ *
237
+ * @param templates - Map of loaded templates by id
238
+ * @param manifest - Manifest defining assembly order
239
+ * @param context - Token values for replacement
240
+ * @returns Assembled content with all tokens replaced
241
+ * @throws If required template is missing
242
+ */
243
+ export function assembleTemplates(templates, manifest, context) {
244
+ // Sort manifest entries by order (ascending)
245
+ const sortedEntries = [...manifest.templates].sort((a, b) => a.order - b.order);
246
+ // Normalize context for condition evaluation (add lowercase aliases)
247
+ // Conditions use lowercase (type, lane) while tokens use uppercase (WU_ID, LANE)
248
+ const conditionContext = normalizeContextForConditions(context);
249
+ const sections = [];
250
+ for (const entry of sortedEntries) {
251
+ const template = templates.get(entry.id);
252
+ // Handle missing templates
253
+ if (!template) {
254
+ if (entry.required) {
255
+ throw createError(ErrorCodes.VALIDATION_ERROR, `Required template '${entry.id}' is missing. ` + `Expected at: ${entry.path}`, { templateId: entry.id, path: entry.path });
256
+ }
257
+ // Skip optional missing templates
258
+ continue;
259
+ }
260
+ // Evaluate condition if present (using normalized context)
261
+ const condition = entry.condition || template.frontmatter.condition;
262
+ if (condition && !evaluateCondition(condition, conditionContext)) {
263
+ continue;
264
+ }
265
+ // Replace tokens and add to output
266
+ const content = replaceTokens(template.content, context);
267
+ sections.push(content);
268
+ }
269
+ return sections.join('\n\n');
270
+ }
271
+ /**
272
+ * Replace {TOKEN} placeholders with context values.
273
+ *
274
+ * @param content - Template content with placeholders
275
+ * @param tokens - Token name to value mapping
276
+ * @returns Content with tokens replaced
277
+ */
278
+ export function replaceTokens(content, tokens) {
279
+ let result = content;
280
+ for (const [key, value] of Object.entries(tokens)) {
281
+ if (value !== undefined) {
282
+ // Use global replace with escaped regex for safety
283
+ const pattern = new RegExp(`\\{${escapeRegex(key)}\\}`, 'g');
284
+ result = result.replace(pattern, value);
285
+ }
286
+ }
287
+ return result;
288
+ }
289
+ /**
290
+ * Escape special regex characters in a string.
291
+ */
292
+ function escapeRegex(str) {
293
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
294
+ }
295
+ /**
296
+ * Get a value from a nested path in an object.
297
+ * Supports both flat keys (e.g., 'type') and dotted paths (e.g., 'policy.testing').
298
+ *
299
+ * @param obj - Object to get value from
300
+ * @param path - Key path (may contain dots for nested access)
301
+ * @returns Value at path, or undefined if not found
302
+ */
303
+ function getNestedValue(obj, path) {
304
+ // First try direct key lookup (for keys like 'policy.testing' stored as flat keys)
305
+ if (path in obj) {
306
+ return obj[path];
307
+ }
308
+ // Then try nested path traversal
309
+ const parts = path.split('.');
310
+ let current = obj;
311
+ for (const part of parts) {
312
+ if (current === null || current === undefined || typeof current !== 'object') {
313
+ return undefined;
314
+ }
315
+ current = current[part];
316
+ }
317
+ return current;
318
+ }
319
+ /**
320
+ * Evaluate a simple condition expression against context.
321
+ *
322
+ * Supports:
323
+ * - Equality: type === 'feature'
324
+ * - Inequality: type !== 'documentation'
325
+ * - Truthy: worktreePath
326
+ * - AND: type === 'feature' && lane === 'Core'
327
+ * - OR: type === 'feature' || type === 'bug'
328
+ * - Dotted paths: policy.testing === 'tdd' (WU-1260)
329
+ *
330
+ * @param condition - Condition expression string
331
+ * @param context - Context values for evaluation
332
+ * @returns Whether condition evaluates to true
333
+ */
334
+ export function evaluateCondition(condition, context) {
335
+ // Empty/undefined conditions always pass
336
+ if (!condition || condition.trim() === '') {
337
+ return true;
338
+ }
339
+ // Handle OR conditions first (lower precedence)
340
+ if (condition.includes('||')) {
341
+ const parts = condition.split('||').map((p) => p.trim());
342
+ return parts.some((part) => evaluateCondition(part, context));
343
+ }
344
+ // Handle AND conditions
345
+ if (condition.includes('&&')) {
346
+ const parts = condition.split('&&').map((p) => p.trim());
347
+ return parts.every((part) => evaluateCondition(part, context));
348
+ }
349
+ // Handle equality: key === 'value' (supports dotted paths like policy.testing)
350
+ const eqRegex = /^([\w.]+)\s*===\s*['"](.+)['"]$/;
351
+ const eqMatch = eqRegex.exec(condition);
352
+ if (eqMatch) {
353
+ const [, key, value] = eqMatch;
354
+ return getNestedValue(context, key) === value;
355
+ }
356
+ // Handle inequality: key !== 'value' (supports dotted paths like policy.testing)
357
+ const neqRegex = /^([\w.]+)\s*!==\s*['"](.+)['"]$/;
358
+ const neqMatch = neqRegex.exec(condition);
359
+ if (neqMatch) {
360
+ const [, key, value] = neqMatch;
361
+ return getNestedValue(context, key) !== value;
362
+ }
363
+ // Handle truthy check: key (supports dotted paths)
364
+ const truthyRegex = /^([\w.]+)$/;
365
+ const truthyMatch = truthyRegex.exec(condition);
366
+ if (truthyMatch) {
367
+ const key = truthyMatch[1];
368
+ return Boolean(getNestedValue(context, key));
369
+ }
370
+ // Unknown condition format - pass by default
371
+ return true;
372
+ }
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Test Baseline - Test Ratchet Pattern (WU-1253)
3
+ *
4
+ * Implements a "ratchet" pattern for test failures:
5
+ * - Track known failures in a baseline file (.lumenflow/test-baseline.json)
6
+ * - Block NEW failures (not in baseline)
7
+ * - Allow pre-existing failures with warning
8
+ * - Auto-update baseline when tests are fixed (ratchet forward)
9
+ *
10
+ * This enables agents to work on WUs without being blocked by unrelated
11
+ * test failures, while still preventing introduction of NEW failures.
12
+ *
13
+ * @see https://lumenflow.dev/reference/test-ratchet/
14
+ */
15
+ import { z } from 'zod';
16
+ /** Default path for the test baseline file */
17
+ export declare const DEFAULT_BASELINE_PATH = ".lumenflow/test-baseline.json";
18
+ /** Environment variable to override baseline path */
19
+ export declare const BASELINE_PATH_ENV = "LUMENFLOW_TEST_BASELINE";
20
+ /** Current schema version */
21
+ export declare const BASELINE_VERSION = 1;
22
+ /**
23
+ * Schema for a known test failure entry
24
+ */
25
+ export declare const KnownFailureSchema: z.ZodObject<{
26
+ test_name: z.ZodString;
27
+ file_path: z.ZodString;
28
+ failure_reason: z.ZodString;
29
+ added_at: z.ZodString;
30
+ added_by_wu: z.ZodString;
31
+ expected_fix_wu: z.ZodOptional<z.ZodString>;
32
+ skip_reason: z.ZodOptional<z.ZodString>;
33
+ }, z.core.$strip>;
34
+ export type KnownFailure = z.infer<typeof KnownFailureSchema>;
35
+ /**
36
+ * Schema for baseline statistics
37
+ */
38
+ export declare const BaselineStatsSchema: z.ZodObject<{
39
+ total_known_failures: z.ZodNumber;
40
+ last_ratchet_forward: z.ZodOptional<z.ZodString>;
41
+ }, z.core.$strip>;
42
+ export type BaselineStats = z.infer<typeof BaselineStatsSchema>;
43
+ /**
44
+ * Schema for the complete test baseline file
45
+ */
46
+ export declare const TestBaselineSchema: z.ZodObject<{
47
+ version: z.ZodLiteral<1>;
48
+ updated_at: z.ZodString;
49
+ updated_by: z.ZodString;
50
+ known_failures: z.ZodArray<z.ZodObject<{
51
+ test_name: z.ZodString;
52
+ file_path: z.ZodString;
53
+ failure_reason: z.ZodString;
54
+ added_at: z.ZodString;
55
+ added_by_wu: z.ZodString;
56
+ expected_fix_wu: z.ZodOptional<z.ZodString>;
57
+ skip_reason: z.ZodOptional<z.ZodString>;
58
+ }, z.core.$strip>>;
59
+ stats: z.ZodObject<{
60
+ total_known_failures: z.ZodNumber;
61
+ last_ratchet_forward: z.ZodOptional<z.ZodString>;
62
+ }, z.core.$strip>;
63
+ }, z.core.$strip>;
64
+ export type TestBaseline = z.infer<typeof TestBaselineSchema>;
65
+ /**
66
+ * Result from a test run (input to comparison)
67
+ */
68
+ export interface TestResult {
69
+ test_name: string;
70
+ file_path: string;
71
+ passed: boolean;
72
+ error_message?: string;
73
+ }
74
+ /**
75
+ * Result of comparing test results against baseline
76
+ */
77
+ export interface BaselineComparison {
78
+ /** Tests that failed but are NOT in baseline (blocks gate) */
79
+ newFailures: TestResult[];
80
+ /** Tests that failed and ARE in baseline (warning only) */
81
+ preExistingFailures: KnownFailure[];
82
+ /** Tests that were in baseline but now pass (ratchet forward candidates) */
83
+ fixedTests: KnownFailure[];
84
+ /** Should this block the gate? (true if newFailures > 0) */
85
+ shouldBlock: boolean;
86
+ /** Are there warnings to show? (true if preExistingFailures > 0) */
87
+ hasWarnings: boolean;
88
+ /** Should baseline be updated? (true if fixedTests > 0) */
89
+ shouldRatchetForward: boolean;
90
+ }
91
+ /**
92
+ * Parse result type
93
+ */
94
+ export type ParseResult<T> = {
95
+ success: true;
96
+ data: T;
97
+ } | {
98
+ success: false;
99
+ error: string;
100
+ };
101
+ /**
102
+ * Options for updating the baseline
103
+ */
104
+ export interface UpdateBaselineOptions {
105
+ /** Tests that were fixed (to remove from baseline) */
106
+ fixedTests?: string[];
107
+ /** New known failures to add to baseline */
108
+ newKnownFailures?: Array<{
109
+ test_name: string;
110
+ file_path: string;
111
+ failure_reason: string;
112
+ expected_fix_wu?: string;
113
+ }>;
114
+ }
115
+ /**
116
+ * Parse a test baseline JSON string
117
+ *
118
+ * @param json - JSON string content of baseline file
119
+ * @returns Parse result with baseline data or error
120
+ */
121
+ export declare function parseTestBaseline(json: string): ParseResult<TestBaseline>;
122
+ /**
123
+ * Create a new test baseline
124
+ *
125
+ * @param wuId - WU creating the baseline
126
+ * @param initialFailures - Optional initial failures to add
127
+ * @returns New test baseline
128
+ */
129
+ export declare function createTestBaseline(wuId: string, initialFailures?: Array<{
130
+ test_name: string;
131
+ file_path: string;
132
+ failure_reason: string;
133
+ expected_fix_wu?: string;
134
+ }>): TestBaseline;
135
+ /**
136
+ * Compare current test results against the baseline
137
+ *
138
+ * This is the core ratchet logic:
139
+ * - NEW failures (not in baseline) block the gate
140
+ * - Pre-existing failures (in baseline) show warning
141
+ * - Fixed tests (in baseline but now passing) trigger ratchet forward
142
+ *
143
+ * @param baseline - The test baseline
144
+ * @param currentFailures - Current test failures from test run
145
+ * @returns Comparison result
146
+ */
147
+ export declare function compareTestResults(baseline: TestBaseline, currentFailures: TestResult[]): BaselineComparison;
148
+ /**
149
+ * Update the baseline (ratchet forward or add new known failures)
150
+ *
151
+ * @param baseline - Current baseline
152
+ * @param wuId - WU making the update
153
+ * @param options - Update options
154
+ * @returns Updated baseline (immutable)
155
+ */
156
+ export declare function updateBaseline(baseline: TestBaseline, wuId: string, options: UpdateBaselineOptions): TestBaseline;
157
+ /**
158
+ * Format a warning message for pre-existing failures
159
+ *
160
+ * @param preExisting - Pre-existing failures from baseline
161
+ * @returns Formatted warning string
162
+ */
163
+ export declare function formatBaselineWarning(preExisting: KnownFailure[]): string;
164
+ /**
165
+ * Format an error message for new failures
166
+ *
167
+ * @param newFailures - New test failures
168
+ * @returns Formatted error string
169
+ */
170
+ export declare function formatNewFailureError(newFailures: TestResult[]): string;
171
+ /**
172
+ * Get the path to the test baseline file
173
+ *
174
+ * @returns Baseline file path
175
+ */
176
+ export declare function getBaselineFilePath(): string;