@lumenflow/core 1.0.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/LICENSE +190 -0
- package/README.md +119 -0
- package/dist/active-wu-detector.d.ts +33 -0
- package/dist/active-wu-detector.js +106 -0
- package/dist/adapters/filesystem-metrics.adapter.d.ts +108 -0
- package/dist/adapters/filesystem-metrics.adapter.js +519 -0
- package/dist/adapters/terminal-renderer.adapter.d.ts +106 -0
- package/dist/adapters/terminal-renderer.adapter.js +337 -0
- package/dist/arg-parser.d.ts +63 -0
- package/dist/arg-parser.js +560 -0
- package/dist/backlog-editor.d.ts +98 -0
- package/dist/backlog-editor.js +179 -0
- package/dist/backlog-generator.d.ts +111 -0
- package/dist/backlog-generator.js +381 -0
- package/dist/backlog-parser.d.ts +45 -0
- package/dist/backlog-parser.js +102 -0
- package/dist/backlog-sync-validator.d.ts +78 -0
- package/dist/backlog-sync-validator.js +294 -0
- package/dist/branch-drift.d.ts +34 -0
- package/dist/branch-drift.js +51 -0
- package/dist/cleanup-install-config.d.ts +33 -0
- package/dist/cleanup-install-config.js +37 -0
- package/dist/cleanup-lock.d.ts +139 -0
- package/dist/cleanup-lock.js +313 -0
- package/dist/code-path-validator.d.ts +146 -0
- package/dist/code-path-validator.js +537 -0
- package/dist/code-paths-overlap.d.ts +55 -0
- package/dist/code-paths-overlap.js +245 -0
- package/dist/commands-logger.d.ts +77 -0
- package/dist/commands-logger.js +254 -0
- package/dist/commit-message-utils.d.ts +25 -0
- package/dist/commit-message-utils.js +41 -0
- package/dist/compliance-parser.d.ts +150 -0
- package/dist/compliance-parser.js +507 -0
- package/dist/constants/backlog-patterns.d.ts +20 -0
- package/dist/constants/backlog-patterns.js +23 -0
- package/dist/constants/dora-constants.d.ts +49 -0
- package/dist/constants/dora-constants.js +53 -0
- package/dist/constants/gate-constants.d.ts +15 -0
- package/dist/constants/gate-constants.js +15 -0
- package/dist/constants/linter-constants.d.ts +16 -0
- package/dist/constants/linter-constants.js +16 -0
- package/dist/constants/tokenizer-constants.d.ts +15 -0
- package/dist/constants/tokenizer-constants.js +15 -0
- package/dist/core/scope-checker.d.ts +97 -0
- package/dist/core/scope-checker.js +163 -0
- package/dist/core/tool-runner.d.ts +161 -0
- package/dist/core/tool-runner.js +393 -0
- package/dist/core/tool.constants.d.ts +105 -0
- package/dist/core/tool.constants.js +101 -0
- package/dist/core/tool.schemas.d.ts +226 -0
- package/dist/core/tool.schemas.js +226 -0
- package/dist/core/worktree-guard.d.ts +130 -0
- package/dist/core/worktree-guard.js +242 -0
- package/dist/coverage-gate.d.ts +108 -0
- package/dist/coverage-gate.js +196 -0
- package/dist/date-utils.d.ts +75 -0
- package/dist/date-utils.js +140 -0
- package/dist/dependency-graph.d.ts +142 -0
- package/dist/dependency-graph.js +550 -0
- package/dist/dependency-guard.d.ts +54 -0
- package/dist/dependency-guard.js +142 -0
- package/dist/dependency-validator.d.ts +105 -0
- package/dist/dependency-validator.js +154 -0
- package/dist/docs-path-validator.d.ts +36 -0
- package/dist/docs-path-validator.js +95 -0
- package/dist/domain/orchestration.constants.d.ts +99 -0
- package/dist/domain/orchestration.constants.js +97 -0
- package/dist/domain/orchestration.schemas.d.ts +280 -0
- package/dist/domain/orchestration.schemas.js +211 -0
- package/dist/domain/orchestration.types.d.ts +133 -0
- package/dist/domain/orchestration.types.js +12 -0
- package/dist/error-handler.d.ts +116 -0
- package/dist/error-handler.js +136 -0
- package/dist/file-classifiers.d.ts +62 -0
- package/dist/file-classifiers.js +108 -0
- package/dist/gates-agent-mode.d.ts +81 -0
- package/dist/gates-agent-mode.js +94 -0
- package/dist/generate-traceability.d.ts +107 -0
- package/dist/generate-traceability.js +411 -0
- package/dist/git-adapter.d.ts +395 -0
- package/dist/git-adapter.js +649 -0
- package/dist/git-staged-validator.d.ts +32 -0
- package/dist/git-staged-validator.js +48 -0
- package/dist/hardcoded-strings.d.ts +61 -0
- package/dist/hardcoded-strings.js +270 -0
- package/dist/incremental-lint.d.ts +78 -0
- package/dist/incremental-lint.js +129 -0
- package/dist/incremental-test.d.ts +39 -0
- package/dist/incremental-test.js +61 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +61 -0
- package/dist/invariants/check-automated-tests.d.ts +50 -0
- package/dist/invariants/check-automated-tests.js +166 -0
- package/dist/invariants-runner.d.ts +103 -0
- package/dist/invariants-runner.js +527 -0
- package/dist/lane-checker.d.ts +50 -0
- package/dist/lane-checker.js +319 -0
- package/dist/lane-inference.d.ts +39 -0
- package/dist/lane-inference.js +195 -0
- package/dist/lane-lock.d.ts +211 -0
- package/dist/lane-lock.js +474 -0
- package/dist/lane-validator.d.ts +48 -0
- package/dist/lane-validator.js +114 -0
- package/dist/logs-lib.d.ts +104 -0
- package/dist/logs-lib.js +207 -0
- package/dist/lumenflow-config-schema.d.ts +272 -0
- package/dist/lumenflow-config-schema.js +207 -0
- package/dist/lumenflow-config.d.ts +95 -0
- package/dist/lumenflow-config.js +236 -0
- package/dist/manual-test-validator.d.ts +80 -0
- package/dist/manual-test-validator.js +200 -0
- package/dist/merge-lock.d.ts +115 -0
- package/dist/merge-lock.js +251 -0
- package/dist/micro-worktree.d.ts +159 -0
- package/dist/micro-worktree.js +427 -0
- package/dist/migration-deployer.d.ts +69 -0
- package/dist/migration-deployer.js +151 -0
- package/dist/orchestration-advisory-loader.d.ts +28 -0
- package/dist/orchestration-advisory-loader.js +87 -0
- package/dist/orchestration-advisory.d.ts +58 -0
- package/dist/orchestration-advisory.js +94 -0
- package/dist/orchestration-di.d.ts +48 -0
- package/dist/orchestration-di.js +57 -0
- package/dist/orchestration-rules.d.ts +57 -0
- package/dist/orchestration-rules.js +201 -0
- package/dist/orphan-detector.d.ts +131 -0
- package/dist/orphan-detector.js +226 -0
- package/dist/path-classifiers.d.ts +57 -0
- package/dist/path-classifiers.js +93 -0
- package/dist/piped-command-detector.d.ts +34 -0
- package/dist/piped-command-detector.js +64 -0
- package/dist/ports/dashboard-renderer.port.d.ts +112 -0
- package/dist/ports/dashboard-renderer.port.js +25 -0
- package/dist/ports/metrics-collector.port.d.ts +132 -0
- package/dist/ports/metrics-collector.port.js +26 -0
- package/dist/process-detector.d.ts +84 -0
- package/dist/process-detector.js +172 -0
- package/dist/prompt-linter.d.ts +72 -0
- package/dist/prompt-linter.js +312 -0
- package/dist/prompt-monitor.d.ts +15 -0
- package/dist/prompt-monitor.js +205 -0
- package/dist/rebase-artifact-cleanup.d.ts +145 -0
- package/dist/rebase-artifact-cleanup.js +433 -0
- package/dist/retry-strategy.d.ts +189 -0
- package/dist/retry-strategy.js +283 -0
- package/dist/risk-detector.d.ts +108 -0
- package/dist/risk-detector.js +252 -0
- package/dist/rollback-utils.d.ts +76 -0
- package/dist/rollback-utils.js +104 -0
- package/dist/section-headings.d.ts +43 -0
- package/dist/section-headings.js +49 -0
- package/dist/spawn-escalation.d.ts +90 -0
- package/dist/spawn-escalation.js +253 -0
- package/dist/spawn-monitor.d.ts +229 -0
- package/dist/spawn-monitor.js +672 -0
- package/dist/spawn-recovery.d.ts +82 -0
- package/dist/spawn-recovery.js +298 -0
- package/dist/spawn-registry-schema.d.ts +98 -0
- package/dist/spawn-registry-schema.js +108 -0
- package/dist/spawn-registry-store.d.ts +146 -0
- package/dist/spawn-registry-store.js +273 -0
- package/dist/spawn-tree.d.ts +121 -0
- package/dist/spawn-tree.js +285 -0
- package/dist/stamp-status-validator.d.ts +84 -0
- package/dist/stamp-status-validator.js +134 -0
- package/dist/stamp-utils.d.ts +100 -0
- package/dist/stamp-utils.js +229 -0
- package/dist/state-machine.d.ts +26 -0
- package/dist/state-machine.js +83 -0
- package/dist/system-map-validator.d.ts +80 -0
- package/dist/system-map-validator.js +272 -0
- package/dist/telemetry.d.ts +80 -0
- package/dist/telemetry.js +213 -0
- package/dist/token-counter.d.ts +51 -0
- package/dist/token-counter.js +145 -0
- package/dist/usecases/get-dashboard-data.usecase.d.ts +52 -0
- package/dist/usecases/get-dashboard-data.usecase.js +61 -0
- package/dist/usecases/get-suggestions.usecase.d.ts +100 -0
- package/dist/usecases/get-suggestions.usecase.js +153 -0
- package/dist/user-normalizer.d.ts +41 -0
- package/dist/user-normalizer.js +141 -0
- package/dist/validators/phi-constants.d.ts +97 -0
- package/dist/validators/phi-constants.js +152 -0
- package/dist/validators/phi-scanner.d.ts +58 -0
- package/dist/validators/phi-scanner.js +215 -0
- package/dist/worktree-ownership.d.ts +50 -0
- package/dist/worktree-ownership.js +74 -0
- package/dist/worktree-scanner.d.ts +103 -0
- package/dist/worktree-scanner.js +168 -0
- package/dist/worktree-symlink.d.ts +99 -0
- package/dist/worktree-symlink.js +359 -0
- package/dist/wu-backlog-updater.d.ts +17 -0
- package/dist/wu-backlog-updater.js +37 -0
- package/dist/wu-checkpoint.d.ts +124 -0
- package/dist/wu-checkpoint.js +233 -0
- package/dist/wu-claim-helpers.d.ts +26 -0
- package/dist/wu-claim-helpers.js +63 -0
- package/dist/wu-claim-resume.d.ts +106 -0
- package/dist/wu-claim-resume.js +276 -0
- package/dist/wu-consistency-checker.d.ts +95 -0
- package/dist/wu-consistency-checker.js +567 -0
- package/dist/wu-constants.d.ts +1275 -0
- package/dist/wu-constants.js +1382 -0
- package/dist/wu-create-validators.d.ts +42 -0
- package/dist/wu-create-validators.js +93 -0
- package/dist/wu-done-branch-only.d.ts +63 -0
- package/dist/wu-done-branch-only.js +191 -0
- package/dist/wu-done-messages.d.ts +119 -0
- package/dist/wu-done-messages.js +185 -0
- package/dist/wu-done-pr.d.ts +72 -0
- package/dist/wu-done-pr.js +174 -0
- package/dist/wu-done-retry-helpers.d.ts +85 -0
- package/dist/wu-done-retry-helpers.js +172 -0
- package/dist/wu-done-ui.d.ts +37 -0
- package/dist/wu-done-ui.js +69 -0
- package/dist/wu-done-validators.d.ts +411 -0
- package/dist/wu-done-validators.js +1229 -0
- package/dist/wu-done-worktree.d.ts +182 -0
- package/dist/wu-done-worktree.js +1097 -0
- package/dist/wu-helpers.d.ts +128 -0
- package/dist/wu-helpers.js +248 -0
- package/dist/wu-lint.d.ts +70 -0
- package/dist/wu-lint.js +234 -0
- package/dist/wu-paths.d.ts +171 -0
- package/dist/wu-paths.js +178 -0
- package/dist/wu-preflight-validators.d.ts +86 -0
- package/dist/wu-preflight-validators.js +251 -0
- package/dist/wu-recovery.d.ts +138 -0
- package/dist/wu-recovery.js +341 -0
- package/dist/wu-repair-core.d.ts +131 -0
- package/dist/wu-repair-core.js +669 -0
- package/dist/wu-schema-normalization.d.ts +17 -0
- package/dist/wu-schema-normalization.js +82 -0
- package/dist/wu-schema.d.ts +793 -0
- package/dist/wu-schema.js +881 -0
- package/dist/wu-spawn-helpers.d.ts +121 -0
- package/dist/wu-spawn-helpers.js +271 -0
- package/dist/wu-spawn.d.ts +158 -0
- package/dist/wu-spawn.js +1306 -0
- package/dist/wu-state-schema.d.ts +213 -0
- package/dist/wu-state-schema.js +156 -0
- package/dist/wu-state-store.d.ts +264 -0
- package/dist/wu-state-store.js +691 -0
- package/dist/wu-status-transition.d.ts +63 -0
- package/dist/wu-status-transition.js +382 -0
- package/dist/wu-status-updater.d.ts +25 -0
- package/dist/wu-status-updater.js +116 -0
- package/dist/wu-transaction-collectors.d.ts +116 -0
- package/dist/wu-transaction-collectors.js +272 -0
- package/dist/wu-transaction.d.ts +170 -0
- package/dist/wu-transaction.js +273 -0
- package/dist/wu-validation-constants.d.ts +60 -0
- package/dist/wu-validation-constants.js +66 -0
- package/dist/wu-validation.d.ts +118 -0
- package/dist/wu-validation.js +243 -0
- package/dist/wu-validator.d.ts +62 -0
- package/dist/wu-validator.js +325 -0
- package/dist/wu-yaml-fixer.d.ts +97 -0
- package/dist/wu-yaml-fixer.js +264 -0
- package/dist/wu-yaml.d.ts +86 -0
- package/dist/wu-yaml.js +222 -0
- package/package.json +114 -0
|
@@ -0,0 +1,881 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Work Unit YAML Schema
|
|
3
|
+
*
|
|
4
|
+
* Zod schema for runtime validation of WU YAML structure.
|
|
5
|
+
* Provides compile-time type inference and semantic validation.
|
|
6
|
+
*
|
|
7
|
+
* Part of WU-1162: Add Zod schema validation to prevent placeholder WU completions
|
|
8
|
+
* Part of WU-1539: Add BaseWUSchema pattern for create/edit validation
|
|
9
|
+
*
|
|
10
|
+
* Schema Architecture (DRY pattern):
|
|
11
|
+
* - BaseWUSchema: Structural validation only (field types, formats, lengths)
|
|
12
|
+
* - WUSchema: Extends base + placeholder rejection (for wu:claim, wu:done)
|
|
13
|
+
* - ReadyWUSchema: Alias for BaseWUSchema (for wu:create, wu:edit)
|
|
14
|
+
*
|
|
15
|
+
* @see {@link tools/wu-done.mjs} - Consumer (validates spec completeness, uses WUSchema)
|
|
16
|
+
* @see {@link tools/wu-claim.mjs} - Consumer (validates spec completeness, uses WUSchema)
|
|
17
|
+
* @see {@link tools/wu-create.mjs} - Consumer (structural validation, uses ReadyWUSchema)
|
|
18
|
+
* @see {@link tools/wu-edit.mjs} - Consumer (structural validation, uses ReadyWUSchema)
|
|
19
|
+
* @see {@link tools/validate.mjs} - Consumer (CI validation)
|
|
20
|
+
* @see {@link apps/web/src/lib/llm/schemas/orchestrator.ts} - Pattern reference
|
|
21
|
+
*/
|
|
22
|
+
import { z } from 'zod';
|
|
23
|
+
import { WU_DEFAULTS, STRING_LITERALS, WU_EXPOSURE_VALUES } from './wu-constants.js';
|
|
24
|
+
import { normalizeISODateTime } from './date-utils.js';
|
|
25
|
+
/**
|
|
26
|
+
* Valid WU status values derived from WU_STATUS constant (DRY principle)
|
|
27
|
+
* Used for Zod enum validation with improved error messages
|
|
28
|
+
* Note: Defined as tuple for Zod enum compatibility
|
|
29
|
+
*/
|
|
30
|
+
const VALID_STATUSES = [
|
|
31
|
+
'todo',
|
|
32
|
+
'ready',
|
|
33
|
+
'backlog',
|
|
34
|
+
'in_progress',
|
|
35
|
+
'blocked',
|
|
36
|
+
'done',
|
|
37
|
+
'completed',
|
|
38
|
+
'cancelled',
|
|
39
|
+
'abandoned',
|
|
40
|
+
'deferred',
|
|
41
|
+
'closed',
|
|
42
|
+
'superseded',
|
|
43
|
+
];
|
|
44
|
+
/**
|
|
45
|
+
* Placeholder sentinel constant
|
|
46
|
+
*
|
|
47
|
+
* Used in wu:create template generation and validation.
|
|
48
|
+
* Single source of truth for placeholder detection (DRY principle).
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* // tools/wu-create.mjs
|
|
52
|
+
* description: `${PLACEHOLDER_SENTINEL} Describe the work...`
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* // tools/validate.mjs
|
|
56
|
+
* if (doc.description.includes(PLACEHOLDER_SENTINEL)) { error(); }
|
|
57
|
+
*/
|
|
58
|
+
export const PLACEHOLDER_SENTINEL = '[PLACEHOLDER]';
|
|
59
|
+
/**
|
|
60
|
+
* Minimum description length requirement
|
|
61
|
+
* Stored as constant for DRY error message generation
|
|
62
|
+
*/
|
|
63
|
+
const MIN_DESCRIPTION_LENGTH = 50;
|
|
64
|
+
/**
|
|
65
|
+
* WU ID format validation message (DRY principle)
|
|
66
|
+
* Used across blocks, blocked_by, and ui_pairing_wus fields
|
|
67
|
+
*/
|
|
68
|
+
const WU_ID_FORMAT_MESSAGE = 'Must be WU-XXX format';
|
|
69
|
+
/**
|
|
70
|
+
* Acceptance criterion error message
|
|
71
|
+
* Stored as constant for DRY error message generation (sonarjs/no-duplicate-string)
|
|
72
|
+
*/
|
|
73
|
+
const ACCEPTANCE_REQUIRED_MSG = 'At least one acceptance criterion required';
|
|
74
|
+
// =============================================================================
|
|
75
|
+
// WU-1750: NORMALIZATION TRANSFORMS (Watertight YAML validation)
|
|
76
|
+
// =============================================================================
|
|
77
|
+
/**
|
|
78
|
+
* Regex pattern matching embedded newlines (both literal and escaped)
|
|
79
|
+
* Handles: "a\nb" (literal newline) and "a\\nb" (escaped backslash-n)
|
|
80
|
+
*/
|
|
81
|
+
const NEWLINE_PATTERN = /\\n|\n/;
|
|
82
|
+
/**
|
|
83
|
+
* Transform: Normalize string arrays by splitting embedded newlines
|
|
84
|
+
*
|
|
85
|
+
* WU-1750: Agents sometimes pass multi-item content as single strings with \n.
|
|
86
|
+
* This transform auto-repairs: ["a\nb\nc"] → ["a", "b", "c"]
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* // Input: ["tools/a.mjs\ntools/b.js"]
|
|
90
|
+
* // Output: ["tools/a.js", "tools/b.js"]
|
|
91
|
+
*/
|
|
92
|
+
const normalizedStringArray = z.array(z.string()).transform((arr) => arr
|
|
93
|
+
.flatMap((s) => s.split(NEWLINE_PATTERN))
|
|
94
|
+
.map((s) => s.trim())
|
|
95
|
+
.filter(Boolean));
|
|
96
|
+
/**
|
|
97
|
+
* Transform: Normalize description/notes strings by converting escaped newlines
|
|
98
|
+
*
|
|
99
|
+
* WU-1750: YAML quoted strings preserve literal \\n as two characters.
|
|
100
|
+
* This transform converts them to actual newlines: "a\\n\\nb" → "a\n\nb"
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* // Input: "Problem:\\n\\n1. First issue"
|
|
104
|
+
* // Output: "Problem:\n\n1. First issue"
|
|
105
|
+
*/
|
|
106
|
+
const normalizedMultilineString = z.string().transform((s) => s.replace(/\\n/g, '\n'));
|
|
107
|
+
/**
|
|
108
|
+
* Refinement: File path cannot contain newlines (post-normalization safety check)
|
|
109
|
+
*
|
|
110
|
+
* WU-1750: After normalization, paths should be clean. This catches any edge cases.
|
|
111
|
+
*/
|
|
112
|
+
const filePathItem = z
|
|
113
|
+
.string()
|
|
114
|
+
.refine((s) => !s.includes('\n') && !s.includes('\\n'), {
|
|
115
|
+
message: 'File path cannot contain newlines - split into separate array items',
|
|
116
|
+
});
|
|
117
|
+
/**
|
|
118
|
+
* Normalized code_paths: split embedded newlines + validate each path
|
|
119
|
+
*/
|
|
120
|
+
const normalizedCodePaths = normalizedStringArray.pipe(z.array(filePathItem)).default([]);
|
|
121
|
+
/**
|
|
122
|
+
* Normalized test paths object: all test arrays normalized
|
|
123
|
+
*/
|
|
124
|
+
const normalizedTestPaths = z
|
|
125
|
+
.object({
|
|
126
|
+
manual: normalizedStringArray.optional(),
|
|
127
|
+
unit: normalizedStringArray.optional(),
|
|
128
|
+
integration: normalizedStringArray.optional(),
|
|
129
|
+
e2e: normalizedStringArray.optional(),
|
|
130
|
+
})
|
|
131
|
+
.optional();
|
|
132
|
+
// =============================================================================
|
|
133
|
+
// BASE FIELD DEFINITIONS (DRY - shared between BaseWUSchema and WUSchema)
|
|
134
|
+
// =============================================================================
|
|
135
|
+
/**
|
|
136
|
+
* Base description field (structural validation only)
|
|
137
|
+
* WU-1539: Fixed template string bug (single quotes → function message)
|
|
138
|
+
* WU-1750: Added normalization of escaped newlines (\\n → actual newlines)
|
|
139
|
+
*/
|
|
140
|
+
const baseDescriptionField = z
|
|
141
|
+
.string()
|
|
142
|
+
.min(1, 'Description is required')
|
|
143
|
+
.transform((s) => s.replace(/\\n/g, '\n')) // WU-1750: Normalize escaped newlines
|
|
144
|
+
.refine((val) => val.trim().length >= MIN_DESCRIPTION_LENGTH, {
|
|
145
|
+
// WU-1539 fix: Use function message for dynamic interpolation
|
|
146
|
+
message: `Description must be at least ${MIN_DESCRIPTION_LENGTH} characters`,
|
|
147
|
+
});
|
|
148
|
+
/**
|
|
149
|
+
* Strict description field (with placeholder rejection)
|
|
150
|
+
* Used by wu:claim and wu:done to ensure placeholders are filled
|
|
151
|
+
* WU-1750: Added normalization of escaped newlines (\\n → actual newlines)
|
|
152
|
+
*/
|
|
153
|
+
const strictDescriptionField = z
|
|
154
|
+
.string()
|
|
155
|
+
.min(1, 'Description is required')
|
|
156
|
+
.transform((s) => s.replace(/\\n/g, '\n')) // WU-1750: Normalize escaped newlines
|
|
157
|
+
.refine((val) => !val.includes(PLACEHOLDER_SENTINEL), {
|
|
158
|
+
message: `Description cannot contain ${PLACEHOLDER_SENTINEL} marker`,
|
|
159
|
+
})
|
|
160
|
+
.refine((val) => val.trim().length >= MIN_DESCRIPTION_LENGTH, {
|
|
161
|
+
// WU-1539 fix: Use function message for dynamic interpolation
|
|
162
|
+
message: `Description must be at least ${MIN_DESCRIPTION_LENGTH} characters`,
|
|
163
|
+
});
|
|
164
|
+
/**
|
|
165
|
+
* Recursive helper: Check all nested values for at least one item
|
|
166
|
+
* Shared between base and strict acceptance schemas
|
|
167
|
+
*/
|
|
168
|
+
const hasItems = (value) => {
|
|
169
|
+
if (Array.isArray(value)) {
|
|
170
|
+
return value.length > 0;
|
|
171
|
+
}
|
|
172
|
+
if (typeof value === 'object' && value !== null) {
|
|
173
|
+
return Object.values(value).some(hasItems);
|
|
174
|
+
}
|
|
175
|
+
return false;
|
|
176
|
+
};
|
|
177
|
+
/**
|
|
178
|
+
* Recursive helper: Check all strings for PLACEHOLDER_SENTINEL
|
|
179
|
+
* Used only by strict acceptance schema
|
|
180
|
+
*/
|
|
181
|
+
const checkStringsForPlaceholder = (value) => {
|
|
182
|
+
if (typeof value === 'string') {
|
|
183
|
+
return !value.includes(PLACEHOLDER_SENTINEL);
|
|
184
|
+
}
|
|
185
|
+
if (Array.isArray(value)) {
|
|
186
|
+
return value.every(checkStringsForPlaceholder);
|
|
187
|
+
}
|
|
188
|
+
if (typeof value === 'object' && value !== null) {
|
|
189
|
+
return Object.values(value).every(checkStringsForPlaceholder);
|
|
190
|
+
}
|
|
191
|
+
return true;
|
|
192
|
+
};
|
|
193
|
+
/**
|
|
194
|
+
* Base acceptance field (structural validation only)
|
|
195
|
+
* Validates format but allows placeholder markers
|
|
196
|
+
* WU-1750: Added normalization of embedded newlines in array items
|
|
197
|
+
*/
|
|
198
|
+
const baseAcceptanceField = z.union([
|
|
199
|
+
// Flat array format (legacy): acceptance: ["item1", "item2"]
|
|
200
|
+
// WU-1750: Normalize embedded newlines: ["1. a\n2. b"] → ["1. a", "2. b"]
|
|
201
|
+
normalizedStringArray.pipe(z.array(z.string()).min(1, ACCEPTANCE_REQUIRED_MSG)),
|
|
202
|
+
// Nested object format (structured): acceptance: { category1: ["item1"], category2: ["item2"] }
|
|
203
|
+
z.record(z.string(), normalizedStringArray).refine((obj) => Object.values(obj).some(hasItems), {
|
|
204
|
+
message: ACCEPTANCE_REQUIRED_MSG,
|
|
205
|
+
}),
|
|
206
|
+
]);
|
|
207
|
+
/**
|
|
208
|
+
* Strict acceptance field (with placeholder rejection)
|
|
209
|
+
* Used by wu:claim and wu:done to ensure placeholders are filled
|
|
210
|
+
* WU-1750: Added normalization of embedded newlines in array items
|
|
211
|
+
*/
|
|
212
|
+
const strictAcceptanceField = z.union([
|
|
213
|
+
// Flat array format (legacy): acceptance: ["item1", "item2"]
|
|
214
|
+
// WU-1750: Normalize embedded newlines: ["1. a\n2. b"] → ["1. a", "2. b"]
|
|
215
|
+
normalizedStringArray
|
|
216
|
+
.pipe(z.array(z.string()).min(1, ACCEPTANCE_REQUIRED_MSG))
|
|
217
|
+
.refine((arr) => !arr.some((item) => item.includes(PLACEHOLDER_SENTINEL)), {
|
|
218
|
+
message: `Acceptance criteria cannot contain ${PLACEHOLDER_SENTINEL} markers`,
|
|
219
|
+
}),
|
|
220
|
+
// Nested object format (structured): acceptance: { category1: ["item1"], category2: ["item2"] }
|
|
221
|
+
z
|
|
222
|
+
.record(z.string(), normalizedStringArray)
|
|
223
|
+
.refine((obj) => Object.values(obj).some(hasItems), {
|
|
224
|
+
message: ACCEPTANCE_REQUIRED_MSG,
|
|
225
|
+
})
|
|
226
|
+
.refine((obj) => checkStringsForPlaceholder(obj), {
|
|
227
|
+
message: `Acceptance criteria cannot contain ${PLACEHOLDER_SENTINEL} markers`,
|
|
228
|
+
}),
|
|
229
|
+
]);
|
|
230
|
+
/**
|
|
231
|
+
* Shared field definitions (same for both base and strict schemas)
|
|
232
|
+
* DRY: Defined once, used in both schema variants
|
|
233
|
+
*/
|
|
234
|
+
const sharedFields = {
|
|
235
|
+
/** WU identifier (e.g., WU-1162) */
|
|
236
|
+
id: z.string().regex(/^WU-\d+$/, 'ID must match pattern WU-XXX'),
|
|
237
|
+
/** Short title describing the work */
|
|
238
|
+
title: z.string().min(1, 'Title is required'),
|
|
239
|
+
/** Lane assignment (parent or sub-lane) */
|
|
240
|
+
lane: z.string().min(1, 'Lane is required'),
|
|
241
|
+
/** Work type classification */
|
|
242
|
+
type: z
|
|
243
|
+
.enum(['feature', 'bug', 'documentation', 'process', 'tooling', 'chore', 'refactor'], {
|
|
244
|
+
error: 'Invalid type',
|
|
245
|
+
})
|
|
246
|
+
.default(WU_DEFAULTS.type),
|
|
247
|
+
/** Current status in workflow */
|
|
248
|
+
status: z
|
|
249
|
+
.enum(VALID_STATUSES, {
|
|
250
|
+
error: `Invalid status. Valid values: ${VALID_STATUSES.join(', ')}`,
|
|
251
|
+
})
|
|
252
|
+
.default(WU_DEFAULTS.status),
|
|
253
|
+
/** Priority level */
|
|
254
|
+
priority: z
|
|
255
|
+
.enum(['P0', 'P1', 'P2', 'P3'], {
|
|
256
|
+
error: 'Invalid priority',
|
|
257
|
+
})
|
|
258
|
+
.default(WU_DEFAULTS.priority),
|
|
259
|
+
/** Creation date (YYYY-MM-DD) */
|
|
260
|
+
created: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Created must be YYYY-MM-DD'),
|
|
261
|
+
/** Files modified by this WU - WU-1750: Normalized to split embedded newlines */
|
|
262
|
+
code_paths: normalizedCodePaths,
|
|
263
|
+
/** Test specifications - WU-1750: All test arrays normalized */
|
|
264
|
+
tests: normalizedTestPaths.default(WU_DEFAULTS.tests),
|
|
265
|
+
/** Output artifacts (stamps, docs, etc.) - WU-1750: Normalized */
|
|
266
|
+
artifacts: normalizedStringArray.optional().default(WU_DEFAULTS.artifacts),
|
|
267
|
+
/** Upstream WU dependencies (informational, legacy field) - WU-1750: Normalized */
|
|
268
|
+
dependencies: normalizedStringArray.optional().default(WU_DEFAULTS.dependencies),
|
|
269
|
+
// === Initiative System Fields (WU-1246) ===
|
|
270
|
+
/** Parent initiative reference (format: INIT-{number} or slug) */
|
|
271
|
+
initiative: z.string().optional(),
|
|
272
|
+
/** Phase number within parent initiative */
|
|
273
|
+
phase: z.number().int().positive().optional(),
|
|
274
|
+
/** WU IDs that this WU blocks (downstream dependencies) - WU-1750: Normalized + validated */
|
|
275
|
+
blocks: normalizedStringArray
|
|
276
|
+
.pipe(z.array(z.string().regex(/^WU-\d+$/, WU_ID_FORMAT_MESSAGE)))
|
|
277
|
+
.optional(),
|
|
278
|
+
/** WU IDs that block this WU (upstream dependencies) - WU-1750: Normalized + validated */
|
|
279
|
+
blocked_by: normalizedStringArray
|
|
280
|
+
.pipe(z.array(z.string().regex(/^WU-\d+$/, WU_ID_FORMAT_MESSAGE)))
|
|
281
|
+
.optional(),
|
|
282
|
+
/** Cross-cutting tags (orthogonal to initiative) - WU-1750: Normalized */
|
|
283
|
+
labels: normalizedStringArray.optional(),
|
|
284
|
+
// === End Initiative System Fields ===
|
|
285
|
+
/**
|
|
286
|
+
* WU-1833: References to plans, design docs, external specifications
|
|
287
|
+
* WU-1834: Supports both flat string array AND nested object format for backwards compatibility
|
|
288
|
+
*
|
|
289
|
+
* Flat format (WU-1833+): ['docs/plans/WU-XXX-plan.md']
|
|
290
|
+
* Nested format (legacy): [{file: 'docs/path.md', section: 'heading'}]
|
|
291
|
+
* Mixed format allowed: ['path.md', {section: 'heading'}]
|
|
292
|
+
* Bare object (WU-428): {file: 'docs/path.md', section: 'heading'}
|
|
293
|
+
*/
|
|
294
|
+
spec_refs: z
|
|
295
|
+
.union([
|
|
296
|
+
// Single object format (WU-428 style): {file: '...', section: '...'}
|
|
297
|
+
z.object({
|
|
298
|
+
file: z.string().optional(),
|
|
299
|
+
section: z.string(),
|
|
300
|
+
}),
|
|
301
|
+
// Array format (WU-1833+): strings, objects, or mixed
|
|
302
|
+
z.array(z.union([
|
|
303
|
+
z.string(), // Flat format: 'docs/path.md'
|
|
304
|
+
z.object({
|
|
305
|
+
// Nested format: {file: 'path', section: 'heading'}
|
|
306
|
+
file: z.string().optional(),
|
|
307
|
+
section: z.string(),
|
|
308
|
+
}),
|
|
309
|
+
])),
|
|
310
|
+
])
|
|
311
|
+
.optional(),
|
|
312
|
+
/** Known risks or constraints - WU-1750: Normalized */
|
|
313
|
+
risks: normalizedStringArray.optional().default(WU_DEFAULTS.risks),
|
|
314
|
+
/**
|
|
315
|
+
* Free-form notes - supports string or array (auto-converted to string)
|
|
316
|
+
* WU-1750: Normalizes escaped newlines (\\n → actual newlines)
|
|
317
|
+
*/
|
|
318
|
+
notes: z
|
|
319
|
+
.union([
|
|
320
|
+
z.string(),
|
|
321
|
+
z.array(z.string()), // Legacy array format - will be converted
|
|
322
|
+
])
|
|
323
|
+
.optional()
|
|
324
|
+
.transform((val) => {
|
|
325
|
+
// Convert array to newline-joined string (legacy format)
|
|
326
|
+
if (Array.isArray(val)) {
|
|
327
|
+
return val.filter((s) => s.trim().length > 0).join(STRING_LITERALS.NEWLINE);
|
|
328
|
+
}
|
|
329
|
+
// WU-1750: Normalize escaped newlines in string format
|
|
330
|
+
if (typeof val === 'string') {
|
|
331
|
+
return val.replace(/\\n/g, '\n');
|
|
332
|
+
}
|
|
333
|
+
return val ?? WU_DEFAULTS.notes;
|
|
334
|
+
}),
|
|
335
|
+
/** Requires human review before merge */
|
|
336
|
+
requires_review: z.boolean().optional().default(WU_DEFAULTS.requires_review),
|
|
337
|
+
/** Locked state (done WUs only) */
|
|
338
|
+
locked: z.boolean().optional(),
|
|
339
|
+
/** Completion date (done WUs only) - auto-normalized to ISO datetime */
|
|
340
|
+
completed_at: z
|
|
341
|
+
.string()
|
|
342
|
+
.optional()
|
|
343
|
+
.transform((val) => normalizeISODateTime(val)),
|
|
344
|
+
/** Claimed mode (worktree/branch-only/pr) */
|
|
345
|
+
claimed_mode: z.enum(['worktree', 'branch-only', 'worktree-pr']).optional(),
|
|
346
|
+
/** Assigned agent email */
|
|
347
|
+
assigned_to: z.string().email().optional(),
|
|
348
|
+
/** Claim timestamp - auto-normalized to ISO datetime */
|
|
349
|
+
claimed_at: z
|
|
350
|
+
.string()
|
|
351
|
+
.optional()
|
|
352
|
+
.transform((val) => normalizeISODateTime(val)),
|
|
353
|
+
/** Block reason (blocked WUs only) */
|
|
354
|
+
blocked_reason: z.string().optional(),
|
|
355
|
+
/** Worktree path (claimed WUs only) */
|
|
356
|
+
worktree_path: z.string().optional(),
|
|
357
|
+
/** Current active session ID (WU-1438: auto-set on claim, cleared on done) */
|
|
358
|
+
session_id: z.string().uuid().optional(),
|
|
359
|
+
/** Agent sessions (issue logging metadata, WU-1231) */
|
|
360
|
+
agent_sessions: z
|
|
361
|
+
.array(z.object({
|
|
362
|
+
session_id: z.string().uuid(),
|
|
363
|
+
started: z.string().datetime(),
|
|
364
|
+
completed: z.string().datetime().optional(),
|
|
365
|
+
agent_type: z.enum(['claude-code', 'cursor', 'copilot', 'other']),
|
|
366
|
+
context_tier: z.union([z.literal(1), z.literal(2), z.literal(3)]),
|
|
367
|
+
incidents_logged: z.number().int().min(0).default(0),
|
|
368
|
+
incidents_major: z.number().int().min(0).default(0),
|
|
369
|
+
artifacts: z.array(z.string()).optional(),
|
|
370
|
+
}))
|
|
371
|
+
.optional(),
|
|
372
|
+
// === Exposure System Fields (WU-1998) ===
|
|
373
|
+
/**
|
|
374
|
+
* WU-1998: Exposure level - defines how the WU exposes functionality to users
|
|
375
|
+
*
|
|
376
|
+
* Valid values:
|
|
377
|
+
* - 'ui': User-facing UI changes (pages, components, widgets)
|
|
378
|
+
* - 'api': API endpoints called by UI or external clients
|
|
379
|
+
* - 'backend-only': Backend-only changes (no user visibility)
|
|
380
|
+
* - 'documentation': Documentation changes only
|
|
381
|
+
*
|
|
382
|
+
* Optional during transition period, will become required after backlog update.
|
|
383
|
+
*/
|
|
384
|
+
exposure: z
|
|
385
|
+
.enum(WU_EXPOSURE_VALUES, {
|
|
386
|
+
error: `Invalid exposure value. Valid values: ${WU_EXPOSURE_VALUES.join(', ')}`,
|
|
387
|
+
})
|
|
388
|
+
.optional(),
|
|
389
|
+
/**
|
|
390
|
+
* WU-1998: User journey description for user-facing WUs
|
|
391
|
+
*
|
|
392
|
+
* Recommended for exposure: 'ui' and 'api'.
|
|
393
|
+
* Describes the end-user interaction flow affected by this WU.
|
|
394
|
+
*/
|
|
395
|
+
user_journey: z.string().optional(),
|
|
396
|
+
/**
|
|
397
|
+
* WU-1998: Related UI WUs for backend/API changes
|
|
398
|
+
*
|
|
399
|
+
* For WUs with exposure: 'api', this field lists UI WUs that consume the API.
|
|
400
|
+
* Ensures backend features have corresponding UI coverage.
|
|
401
|
+
* Each entry must match WU-XXX format.
|
|
402
|
+
*/
|
|
403
|
+
ui_pairing_wus: normalizedStringArray
|
|
404
|
+
.pipe(z.array(z.string().regex(/^WU-\d+$/, WU_ID_FORMAT_MESSAGE)))
|
|
405
|
+
.optional(),
|
|
406
|
+
/**
|
|
407
|
+
* WU-2022: Navigation path for UI-exposed features
|
|
408
|
+
*
|
|
409
|
+
* For WUs with exposure: 'ui', specifies the route where the feature is accessible.
|
|
410
|
+
* Used by wu:done to verify that UI features are actually navigable.
|
|
411
|
+
* Prevents "orphaned code" where features exist but users cannot access them.
|
|
412
|
+
*
|
|
413
|
+
* Example: '/dashboard', '/settings/preferences', '/space'
|
|
414
|
+
*/
|
|
415
|
+
navigation_path: z.string().optional(),
|
|
416
|
+
// === End Exposure System Fields ===
|
|
417
|
+
// === Agent-First Approval Fields (WU-2079 → WU-2080) ===
|
|
418
|
+
/**
|
|
419
|
+
* WU-2080: Escalation triggers detected for this WU
|
|
420
|
+
*
|
|
421
|
+
* Agent-first model: agents auto-approve by default.
|
|
422
|
+
* Human escalation only when these triggers are detected:
|
|
423
|
+
* - phi_pii: Changes to PHI/PII data handling
|
|
424
|
+
* - security_p0: P0 security incident or vulnerability
|
|
425
|
+
* - budget: Budget/resource allocation above threshold
|
|
426
|
+
* - external_compliance: External regulatory submission
|
|
427
|
+
* - cross_lane_arch: Cross-lane architectural decision
|
|
428
|
+
*
|
|
429
|
+
* Empty array = no escalation needed, agent proceeds autonomously.
|
|
430
|
+
*/
|
|
431
|
+
escalation_triggers: z
|
|
432
|
+
.array(z.enum(['phi_pii', 'security_p0', 'budget', 'external_compliance', 'cross_lane_arch']))
|
|
433
|
+
.optional()
|
|
434
|
+
.default([]),
|
|
435
|
+
/**
|
|
436
|
+
* WU-2080: Human escalation required flag
|
|
437
|
+
*
|
|
438
|
+
* Auto-set to true when escalation_triggers is non-empty.
|
|
439
|
+
* When true, wu:done requires human confirmation before completion.
|
|
440
|
+
*/
|
|
441
|
+
requires_human_escalation: z.boolean().optional().default(false),
|
|
442
|
+
/**
|
|
443
|
+
* WU-2080: Email(s) of approvers who signed off
|
|
444
|
+
*
|
|
445
|
+
* Auto-populated with claiming agent at wu:claim.
|
|
446
|
+
* Additional human approvers added when escalation is resolved.
|
|
447
|
+
*/
|
|
448
|
+
approved_by: z.array(z.string().email()).optional(),
|
|
449
|
+
/**
|
|
450
|
+
* WU-2080: Timestamp when approval was granted
|
|
451
|
+
*
|
|
452
|
+
* Auto-set at wu:claim for agent auto-approval.
|
|
453
|
+
* Updated when human escalation is resolved.
|
|
454
|
+
*/
|
|
455
|
+
approved_at: z
|
|
456
|
+
.string()
|
|
457
|
+
.optional()
|
|
458
|
+
.transform((val) => normalizeISODateTime(val)),
|
|
459
|
+
/**
|
|
460
|
+
* WU-2080: Human who resolved escalation (if any)
|
|
461
|
+
*
|
|
462
|
+
* Only set when requires_human_escalation was true and resolved.
|
|
463
|
+
*/
|
|
464
|
+
escalation_resolved_by: z.string().email().optional(),
|
|
465
|
+
/**
|
|
466
|
+
* WU-2080: Timestamp when human resolved escalation
|
|
467
|
+
*/
|
|
468
|
+
escalation_resolved_at: z
|
|
469
|
+
.string()
|
|
470
|
+
.optional()
|
|
471
|
+
.transform((val) => normalizeISODateTime(val)),
|
|
472
|
+
// Legacy fields (deprecated, kept for backwards compatibility)
|
|
473
|
+
/** @deprecated Use escalation_triggers instead */
|
|
474
|
+
requires_cso_approval: z.boolean().optional().default(false),
|
|
475
|
+
/** @deprecated Use escalation_triggers instead */
|
|
476
|
+
requires_cto_approval: z.boolean().optional().default(false),
|
|
477
|
+
/** @deprecated Use escalation_triggers instead */
|
|
478
|
+
requires_design_approval: z.boolean().optional().default(false),
|
|
479
|
+
// === End Agent-First Approval Fields ===
|
|
480
|
+
};
|
|
481
|
+
// =============================================================================
|
|
482
|
+
// SCHEMA DEFINITIONS
|
|
483
|
+
// =============================================================================
|
|
484
|
+
/**
|
|
485
|
+
* Base WU Schema (structural validation only)
|
|
486
|
+
*
|
|
487
|
+
* WU-1539: Used by wu:create and wu:edit for fail-fast structural validation.
|
|
488
|
+
* Allows placeholder markers - only checks field types, formats, and lengths.
|
|
489
|
+
*
|
|
490
|
+
* Use case: Validate WU structure at creation/edit time before placeholders are filled.
|
|
491
|
+
*/
|
|
492
|
+
export const BaseWUSchema = z.object({
|
|
493
|
+
...sharedFields,
|
|
494
|
+
description: baseDescriptionField,
|
|
495
|
+
acceptance: baseAcceptanceField,
|
|
496
|
+
});
|
|
497
|
+
/**
|
|
498
|
+
* Ready WU Schema (alias for BaseWUSchema)
|
|
499
|
+
*
|
|
500
|
+
* WU-1539: Semantic alias for clarity in wu:create and wu:edit.
|
|
501
|
+
* Same validation as BaseWUSchema - allows placeholders, enforces structure.
|
|
502
|
+
*/
|
|
503
|
+
export const ReadyWUSchema = BaseWUSchema;
|
|
504
|
+
/**
|
|
505
|
+
* Strict WU Schema (structural + placeholder rejection)
|
|
506
|
+
*
|
|
507
|
+
* Validates WU files against LumenFlow requirements:
|
|
508
|
+
* - No placeholder text in done WUs
|
|
509
|
+
* - Minimum description length (50 chars)
|
|
510
|
+
* - Code paths present for non-documentation WUs
|
|
511
|
+
* - Proper status/lane/type enums
|
|
512
|
+
*
|
|
513
|
+
* Used by wu:claim and wu:done to ensure specs are complete.
|
|
514
|
+
* Provides runtime validation and TypeScript type inference.
|
|
515
|
+
*/
|
|
516
|
+
export const WUSchema = z.object({
|
|
517
|
+
...sharedFields,
|
|
518
|
+
description: strictDescriptionField,
|
|
519
|
+
acceptance: strictAcceptanceField,
|
|
520
|
+
});
|
|
521
|
+
/**
|
|
522
|
+
* TypeScript type inferred from schema
|
|
523
|
+
*
|
|
524
|
+
* Single source of truth for both runtime validation and compile-time types.
|
|
525
|
+
* Replaces manual WU interfaces (DRY principle).
|
|
526
|
+
*
|
|
527
|
+
* Note: Type inference available in TypeScript via z.infer<typeof WUSchema>
|
|
528
|
+
* This is a JavaScript file, so the type export is not needed here.
|
|
529
|
+
*
|
|
530
|
+
* @typedef {import('zod').z.infer<typeof WUSchema>} WU
|
|
531
|
+
*/
|
|
532
|
+
/**
|
|
533
|
+
* Validates WU data against strict schema (placeholder rejection)
|
|
534
|
+
*
|
|
535
|
+
* Used by wu:claim and wu:done to ensure specs are complete.
|
|
536
|
+
* Rejects WUs with placeholder markers.
|
|
537
|
+
*
|
|
538
|
+
* @param {unknown} data - Parsed YAML data to validate
|
|
539
|
+
* @returns {z.SafeParseReturnType<WU, WU>} Validation result
|
|
540
|
+
*
|
|
541
|
+
* @example
|
|
542
|
+
* const result = validateWU(yamlData);
|
|
543
|
+
* if (!result.success) {
|
|
544
|
+
* result.error.issues.forEach(issue => {
|
|
545
|
+
* console.error(`${issue.path.join('.')}: ${issue.message}`);
|
|
546
|
+
* });
|
|
547
|
+
* }
|
|
548
|
+
*/
|
|
549
|
+
export function validateWU(data) {
|
|
550
|
+
return WUSchema.safeParse(data);
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Validates WU data against base schema (structural only)
|
|
554
|
+
*
|
|
555
|
+
* WU-1539: Used by wu:create and wu:edit for fail-fast structural validation.
|
|
556
|
+
* Allows placeholder markers - only checks field types, formats, and lengths.
|
|
557
|
+
*
|
|
558
|
+
* @param {unknown} data - Parsed YAML data to validate
|
|
559
|
+
* @returns {z.SafeParseReturnType<WU, WU>} Validation result
|
|
560
|
+
*
|
|
561
|
+
* @example
|
|
562
|
+
* const result = validateReadyWU(yamlData);
|
|
563
|
+
* if (!result.success) {
|
|
564
|
+
* const errors = result.error.issues
|
|
565
|
+
* .map(issue => ` • ${issue.path.join('.')}: ${issue.message}`)
|
|
566
|
+
* .join('\n');
|
|
567
|
+
* die(`WU YAML validation failed:\n\n${errors}`);
|
|
568
|
+
* }
|
|
569
|
+
*/
|
|
570
|
+
export function validateReadyWU(data) {
|
|
571
|
+
return ReadyWUSchema.safeParse(data);
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Validates WU spec completeness for done status
|
|
575
|
+
*
|
|
576
|
+
* Additional validation beyond schema for WUs marked as done:
|
|
577
|
+
* - Code paths required for non-documentation WUs
|
|
578
|
+
* - Locked must be true
|
|
579
|
+
* - Completed timestamp must be present
|
|
580
|
+
*
|
|
581
|
+
* @param {WU} wu - Validated WU data
|
|
582
|
+
* @returns {{valid: boolean, errors: string[]}} Validation result
|
|
583
|
+
*
|
|
584
|
+
* @example
|
|
585
|
+
* const schemaResult = validateWU(data);
|
|
586
|
+
* if (schemaResult.success && data.status === 'done') {
|
|
587
|
+
* const completenessResult = validateDoneWU(schemaResult.data);
|
|
588
|
+
* if (!completenessResult.valid) {
|
|
589
|
+
* console.error(completenessResult.errors);
|
|
590
|
+
* }
|
|
591
|
+
* }
|
|
592
|
+
*/
|
|
593
|
+
export function validateDoneWU(wu) {
|
|
594
|
+
const errors = [];
|
|
595
|
+
// Check code_paths for non-documentation WUs
|
|
596
|
+
if (wu.type !== 'documentation' && wu.type !== 'process') {
|
|
597
|
+
if (!wu.code_paths || wu.code_paths.length === 0) {
|
|
598
|
+
errors.push('Code paths required for non-documentation WUs');
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
// Note: locked and completed_at are set automatically by wu:done
|
|
602
|
+
// No need to validate them here (they don't exist yet at validation time)
|
|
603
|
+
return {
|
|
604
|
+
valid: errors.length === 0,
|
|
605
|
+
errors,
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* WU-2080: Human escalation email for notifications
|
|
610
|
+
*
|
|
611
|
+
* When escalation triggers fire, this email receives notification.
|
|
612
|
+
* In production, this would integrate with PagerDuty/Slack.
|
|
613
|
+
*/
|
|
614
|
+
const ESCALATION_EMAIL = 'tom@hellm.ai';
|
|
615
|
+
/**
|
|
616
|
+
* WU-2080: Valid escalation trigger types
|
|
617
|
+
*
|
|
618
|
+
* These are the only conditions that require human intervention.
|
|
619
|
+
* Everything else is auto-approved by agents.
|
|
620
|
+
*/
|
|
621
|
+
export const ESCALATION_TRIGGER_TYPES = [
|
|
622
|
+
'phi_pii', // PHI/PII data handling changes
|
|
623
|
+
'security_p0', // P0 security incident
|
|
624
|
+
'budget', // Budget/resource above threshold
|
|
625
|
+
'external_compliance', // External regulatory submission
|
|
626
|
+
'cross_lane_arch', // Cross-lane architectural decision
|
|
627
|
+
];
|
|
628
|
+
/**
|
|
629
|
+
* WU-2080: Agent-first approval validation
|
|
630
|
+
*
|
|
631
|
+
* AGENT-FIRST MODEL: Agents auto-approve by default.
|
|
632
|
+
* Human escalation only when escalation_triggers is non-empty
|
|
633
|
+
* AND requires_human_escalation is true AND not yet resolved.
|
|
634
|
+
*
|
|
635
|
+
* Returns:
|
|
636
|
+
* - valid: true if agent can proceed (no unresolved escalation)
|
|
637
|
+
* - errors: blocking issues requiring human resolution
|
|
638
|
+
* - warnings: advisory messages (non-blocking)
|
|
639
|
+
*
|
|
640
|
+
* @param {object} wu - Validated WU data
|
|
641
|
+
* @returns {{valid: boolean, errors: string[], warnings: string[]}}
|
|
642
|
+
*/
|
|
643
|
+
export function validateApprovalGates(wu) {
|
|
644
|
+
const errors = [];
|
|
645
|
+
const warnings = [];
|
|
646
|
+
// Agent-first: check for unresolved escalation triggers
|
|
647
|
+
const triggers = wu.escalation_triggers || [];
|
|
648
|
+
const requiresEscalation = wu.requires_human_escalation || triggers.length > 0;
|
|
649
|
+
if (requiresEscalation) {
|
|
650
|
+
// Check if escalation was resolved by human
|
|
651
|
+
const resolved = wu.escalation_resolved_by && wu.escalation_resolved_at;
|
|
652
|
+
if (!resolved) {
|
|
653
|
+
errors.push(`Human escalation required for: ${triggers.join(', ')}\n` +
|
|
654
|
+
` To resolve: Add escalation_resolved_by: "${ESCALATION_EMAIL}" and escalation_resolved_at to WU YAML\n` +
|
|
655
|
+
` Or use: pnpm wu:escalate --resolve --id ${wu.id}`);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
// Legacy backwards compatibility: map old fields to new model
|
|
659
|
+
if (wu.requires_cso_approval || wu.requires_cto_approval || wu.requires_design_approval) {
|
|
660
|
+
warnings.push('Using deprecated requires_X_approval fields. Migrate to escalation_triggers model.');
|
|
661
|
+
}
|
|
662
|
+
return {
|
|
663
|
+
valid: errors.length === 0,
|
|
664
|
+
errors,
|
|
665
|
+
warnings,
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* WU-2080: Detect escalation triggers from WU content
|
|
670
|
+
*
|
|
671
|
+
* Analyzes WU metadata to detect conditions requiring human escalation.
|
|
672
|
+
* Called by wu:claim to auto-set escalation_triggers.
|
|
673
|
+
*
|
|
674
|
+
* @param {object} wu - WU data with lane, type, code_paths
|
|
675
|
+
* @returns {string[]} Array of triggered escalation types
|
|
676
|
+
*/
|
|
677
|
+
export function detectEscalationTriggers(wu) {
|
|
678
|
+
const triggers = [];
|
|
679
|
+
const lane = (wu.lane || '').toLowerCase();
|
|
680
|
+
const codePaths = wu.code_paths || [];
|
|
681
|
+
// PHI/PII: Changes to patient data, auth, or medical records
|
|
682
|
+
const phiPatterns = [
|
|
683
|
+
'phi',
|
|
684
|
+
'pii',
|
|
685
|
+
'patient',
|
|
686
|
+
'medical',
|
|
687
|
+
'health',
|
|
688
|
+
'hipaa',
|
|
689
|
+
'supabase/migrations',
|
|
690
|
+
];
|
|
691
|
+
const touchesPhi = codePaths.some((p) => phiPatterns.some((pat) => p.toLowerCase().includes(pat)));
|
|
692
|
+
if (touchesPhi || lane.includes('phi') || lane.includes('pii')) {
|
|
693
|
+
triggers.push('phi_pii');
|
|
694
|
+
}
|
|
695
|
+
// Security P0: Explicit security lane or auth changes
|
|
696
|
+
if (wu.priority === 'P0' && lane.includes('security')) {
|
|
697
|
+
triggers.push('security_p0');
|
|
698
|
+
}
|
|
699
|
+
// External compliance: Regulatory submissions
|
|
700
|
+
const compliancePatterns = ['fda', 'mhra', 'ce-mark', 'regulatory', 'submission'];
|
|
701
|
+
const touchesCompliance = codePaths.some((p) => compliancePatterns.some((pat) => p.toLowerCase().includes(pat)));
|
|
702
|
+
if (touchesCompliance || lane.includes('compliance')) {
|
|
703
|
+
triggers.push('external_compliance');
|
|
704
|
+
}
|
|
705
|
+
return triggers;
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* WU-2080: Generate auto-approval metadata for wu:claim
|
|
709
|
+
*
|
|
710
|
+
* Called by wu:claim to auto-approve agents within policy.
|
|
711
|
+
* Sets approved_by and approved_at, detects escalation triggers.
|
|
712
|
+
*
|
|
713
|
+
* @param {object} wu - WU data
|
|
714
|
+
* @param {string} agentEmail - Email of claiming agent
|
|
715
|
+
* @returns {{approved_by: string[], approved_at: string, escalation_triggers: string[], requires_human_escalation: boolean}}
|
|
716
|
+
*/
|
|
717
|
+
export function generateAutoApproval(wu, agentEmail) {
|
|
718
|
+
const triggers = detectEscalationTriggers(wu);
|
|
719
|
+
const now = new Date().toISOString();
|
|
720
|
+
return {
|
|
721
|
+
approved_by: [agentEmail],
|
|
722
|
+
approved_at: now,
|
|
723
|
+
escalation_triggers: triggers,
|
|
724
|
+
requires_human_escalation: triggers.length > 0,
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* @deprecated Use detectEscalationTriggers instead
|
|
729
|
+
* WU-2079: Legacy function for backwards compatibility
|
|
730
|
+
*/
|
|
731
|
+
export function determineRequiredApprovals(wu) {
|
|
732
|
+
const triggers = detectEscalationTriggers(wu);
|
|
733
|
+
return {
|
|
734
|
+
requires_cso_approval: triggers.includes('security_p0') || triggers.includes('phi_pii'),
|
|
735
|
+
requires_cto_approval: triggers.includes('cross_lane_arch'),
|
|
736
|
+
requires_design_approval: false, // Design no longer requires human escalation
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* WU-1811: Validates and normalizes WU YAML data with auto-fixable normalisations
|
|
741
|
+
*
|
|
742
|
+
* This function validates the WU YAML schema and applies fixable normalisations:
|
|
743
|
+
* - Trimming whitespace from string fields
|
|
744
|
+
* - Normalizing escaped newlines (\\n → \n)
|
|
745
|
+
* - Splitting embedded newlines in arrays (["a\nb"] → ["a", "b"])
|
|
746
|
+
*
|
|
747
|
+
* Returns:
|
|
748
|
+
* - valid: true if schema validation passes (after normalisations)
|
|
749
|
+
* - normalized: the normalized data (even if validation fails, partial normalization is returned)
|
|
750
|
+
* - errors: validation errors if any
|
|
751
|
+
* - wasNormalized: true if any normalisations were applied
|
|
752
|
+
*
|
|
753
|
+
* @param {unknown} data - Parsed YAML data to validate and normalize
|
|
754
|
+
* @returns {{valid: boolean, normalized: object|null, errors: string[], wasNormalized: boolean}}
|
|
755
|
+
*
|
|
756
|
+
* @example
|
|
757
|
+
* const { valid, normalized, errors, wasNormalized } = validateAndNormalizeWUYAML(yamlData);
|
|
758
|
+
* if (valid && wasNormalized) {
|
|
759
|
+
* // Write normalized data back to YAML file
|
|
760
|
+
* writeWU(wuPath, normalized);
|
|
761
|
+
* }
|
|
762
|
+
* if (!valid) {
|
|
763
|
+
* die(`Validation failed:\n${errors.join('\n')}`);
|
|
764
|
+
* }
|
|
765
|
+
*/
|
|
766
|
+
export function validateAndNormalizeWUYAML(data) {
|
|
767
|
+
// First try to parse with schema (which applies normalizations)
|
|
768
|
+
const result = WUSchema.safeParse(data);
|
|
769
|
+
if (!result.success) {
|
|
770
|
+
// Schema validation failed - return errors
|
|
771
|
+
const errors = result.error.issues.map((issue) => `${issue.path.join('.')}: ${issue.message}`);
|
|
772
|
+
return {
|
|
773
|
+
valid: false,
|
|
774
|
+
normalized: null,
|
|
775
|
+
errors,
|
|
776
|
+
wasNormalized: false,
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
// Schema passed - check if data was normalized (compare key fields)
|
|
780
|
+
const normalized = result.data;
|
|
781
|
+
const wasNormalized = detectNormalizationChanges(data, normalized);
|
|
782
|
+
return {
|
|
783
|
+
valid: true,
|
|
784
|
+
normalized,
|
|
785
|
+
errors: [],
|
|
786
|
+
wasNormalized,
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* WU-1833: Validate WU spec completeness with advisory warnings
|
|
791
|
+
*
|
|
792
|
+
* Provides soft validation that warns (doesn't fail) when recommended fields are missing.
|
|
793
|
+
* Used by wu:validate command to surface quality issues without blocking workflow.
|
|
794
|
+
*
|
|
795
|
+
* Feature and bug WUs should have:
|
|
796
|
+
* - notes (implementation context, deployment instructions)
|
|
797
|
+
* - tests.manual (verification steps)
|
|
798
|
+
* - spec_refs (links to plans, design docs) - for features only
|
|
799
|
+
*
|
|
800
|
+
* @param {object} wu - Validated WU data (must pass WUSchema first)
|
|
801
|
+
* @returns {{warnings: string[]}} Array of warning messages
|
|
802
|
+
*
|
|
803
|
+
* @example
|
|
804
|
+
* const schemaResult = validateWU(data);
|
|
805
|
+
* if (schemaResult.success) {
|
|
806
|
+
* const { warnings } = validateWUCompleteness(schemaResult.data);
|
|
807
|
+
* if (warnings.length > 0) {
|
|
808
|
+
* console.warn('Quality warnings:');
|
|
809
|
+
* warnings.forEach(w => console.warn(` ⚠️ ${w}`));
|
|
810
|
+
* }
|
|
811
|
+
* }
|
|
812
|
+
*/
|
|
813
|
+
export function validateWUCompleteness(wu) {
|
|
814
|
+
const warnings = [];
|
|
815
|
+
const type = wu.type || 'feature';
|
|
816
|
+
// Only check feature and bug WUs - docs/chore/process don't need these
|
|
817
|
+
const requiresContext = ['feature', 'bug', 'refactor'].includes(type);
|
|
818
|
+
if (!requiresContext) {
|
|
819
|
+
return { warnings };
|
|
820
|
+
}
|
|
821
|
+
// Check for notes (implementation context)
|
|
822
|
+
if (!wu.notes || wu.notes.trim().length === 0) {
|
|
823
|
+
warnings.push(`${wu.id}: Missing 'notes' field. Add implementation context, deployment instructions, or plan links.`);
|
|
824
|
+
}
|
|
825
|
+
// Check for manual tests
|
|
826
|
+
const hasManualTests = wu.tests?.manual && wu.tests.manual.length > 0;
|
|
827
|
+
if (!hasManualTests) {
|
|
828
|
+
warnings.push(`${wu.id}: Missing 'tests.manual' field. Add manual verification steps for acceptance criteria.`);
|
|
829
|
+
}
|
|
830
|
+
// Check for spec_refs (features should link to plans/specs)
|
|
831
|
+
if (type === 'feature') {
|
|
832
|
+
const hasSpecRefs = wu.spec_refs && wu.spec_refs.length > 0;
|
|
833
|
+
if (!hasSpecRefs) {
|
|
834
|
+
warnings.push(`${wu.id}: Missing 'spec_refs' field. Link to plan file in docs/04-operations/plans/ for traceability.`);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
return { warnings };
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* WU-1811: Detect if normalizations were applied by comparing original and normalized data
|
|
841
|
+
*
|
|
842
|
+
* Compares fields that are commonly normalized:
|
|
843
|
+
* - description (escaped newlines)
|
|
844
|
+
* - code_paths (embedded newlines split)
|
|
845
|
+
* - acceptance (embedded newlines split)
|
|
846
|
+
*
|
|
847
|
+
* @param {object} original - Original parsed YAML data
|
|
848
|
+
* @param {object} normalized - Schema-normalized data
|
|
849
|
+
* @returns {boolean} True if any normalisations were applied
|
|
850
|
+
*/
|
|
851
|
+
function detectNormalizationChanges(original, normalized) {
|
|
852
|
+
// Compare description (newline normalization)
|
|
853
|
+
if (original.description !== normalized.description) {
|
|
854
|
+
return true;
|
|
855
|
+
}
|
|
856
|
+
// Compare code_paths (array splitting)
|
|
857
|
+
const origPaths = original.code_paths || [];
|
|
858
|
+
const normPaths = normalized.code_paths || [];
|
|
859
|
+
if (origPaths.length !== normPaths.length) {
|
|
860
|
+
return true;
|
|
861
|
+
}
|
|
862
|
+
for (let i = 0; i < origPaths.length; i++) {
|
|
863
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
864
|
+
if (origPaths[i] !== normPaths[i]) {
|
|
865
|
+
return true;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
// Compare acceptance if both are arrays (most common case)
|
|
869
|
+
if (Array.isArray(original.acceptance) && Array.isArray(normalized.acceptance)) {
|
|
870
|
+
if (original.acceptance.length !== normalized.acceptance.length) {
|
|
871
|
+
return true;
|
|
872
|
+
}
|
|
873
|
+
for (let i = 0; i < original.acceptance.length; i++) {
|
|
874
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
875
|
+
if (original.acceptance[i] !== normalized.acceptance[i]) {
|
|
876
|
+
return true;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
return false;
|
|
881
|
+
}
|