@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,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WU Exposure Validation (WU-1999, WU-2022)
|
|
3
|
+
*
|
|
4
|
+
* WU-1999: Validates exposure field and UI pairing for wu:done.
|
|
5
|
+
* Provides warnings (not errors) to guide completion without blocking.
|
|
6
|
+
*
|
|
7
|
+
* WU-2022: Adds BLOCKING validation for feature accessibility.
|
|
8
|
+
* When exposure=ui, ensures the feature is actually accessible via navigation.
|
|
9
|
+
*
|
|
10
|
+
* Part of INIT-031 Phase 4: Prevent backend-without-UI pattern.
|
|
11
|
+
*
|
|
12
|
+
* @see {@link tools/wu-done.mjs} - Consumer
|
|
13
|
+
* @see {@link tools/lib/wu-schema.mjs} - WU schema with exposure field
|
|
14
|
+
* @see {@link tools/lib/wu-constants.mjs} - WU_EXPOSURE values
|
|
15
|
+
*/
|
|
16
|
+
import { WU_EXPOSURE } from './wu-constants.js';
|
|
17
|
+
/**
|
|
18
|
+
* UI verification keywords to search for in acceptance criteria.
|
|
19
|
+
* Case-insensitive patterns that indicate the acceptance criteria
|
|
20
|
+
* mentions UI verification.
|
|
21
|
+
*/
|
|
22
|
+
const UI_VERIFICATION_KEYWORDS = [
|
|
23
|
+
'ui',
|
|
24
|
+
'frontend',
|
|
25
|
+
'component',
|
|
26
|
+
'widget',
|
|
27
|
+
'page',
|
|
28
|
+
'displays',
|
|
29
|
+
'shows',
|
|
30
|
+
'renders',
|
|
31
|
+
'user sees',
|
|
32
|
+
'visible',
|
|
33
|
+
'screen',
|
|
34
|
+
'interface',
|
|
35
|
+
];
|
|
36
|
+
/**
|
|
37
|
+
* Warning message templates with remediation guidance.
|
|
38
|
+
* All messages include the WU ID for context.
|
|
39
|
+
*/
|
|
40
|
+
export const EXPOSURE_WARNING_MESSAGES = {
|
|
41
|
+
/**
|
|
42
|
+
* Warning when exposure field is missing entirely.
|
|
43
|
+
* @param {string} wuId - The WU identifier
|
|
44
|
+
* @returns {string} Warning message with remediation
|
|
45
|
+
*/
|
|
46
|
+
MISSING_EXPOSURE: (wuId) => `${wuId}: exposure field is missing. ` +
|
|
47
|
+
`Add 'exposure: ui|api|backend-only|documentation' to the WU YAML. ` +
|
|
48
|
+
`This helps ensure user-facing features have corresponding UI coverage.`,
|
|
49
|
+
/**
|
|
50
|
+
* Warning when API exposure lacks UI pairing WUs.
|
|
51
|
+
* @param {string} wuId - The WU identifier
|
|
52
|
+
* @returns {string} Warning message with remediation
|
|
53
|
+
*/
|
|
54
|
+
MISSING_UI_PAIRING: (wuId) => `${wuId}: exposure=api but ui_pairing_wus not specified. ` +
|
|
55
|
+
`Add 'ui_pairing_wus: [WU-XXX]' listing UI WUs that consume this API, ` +
|
|
56
|
+
`or set exposure to 'backend-only' if no UI is planned.`,
|
|
57
|
+
/**
|
|
58
|
+
* Warning when API exposure lacks UI verification in acceptance criteria.
|
|
59
|
+
* @param {string} wuId - The WU identifier
|
|
60
|
+
* @returns {string} Warning message with remediation
|
|
61
|
+
*/
|
|
62
|
+
MISSING_UI_VERIFICATION: (wuId) => `${wuId}: exposure=api but acceptance criteria lacks UI verification mention. ` +
|
|
63
|
+
`Consider adding a criterion like 'UI displays the data correctly' to ensure end-to-end coverage.`,
|
|
64
|
+
/**
|
|
65
|
+
* Recommendation for user_journey when exposure is UI.
|
|
66
|
+
* @param {string} wuId - The WU identifier
|
|
67
|
+
* @returns {string} Warning message with remediation
|
|
68
|
+
*/
|
|
69
|
+
MISSING_USER_JOURNEY: (wuId) => `${wuId}: exposure=ui but user_journey field not present. ` +
|
|
70
|
+
`Adding 'user_journey: "<description>"' is recommended to document the user flow.`,
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Check if acceptance criteria mentions UI verification.
|
|
74
|
+
*
|
|
75
|
+
* Searches acceptance criteria (array or nested object) for keywords
|
|
76
|
+
* that indicate UI verification is mentioned.
|
|
77
|
+
*
|
|
78
|
+
* @param {string[]|Record<string, string[]>} acceptance - Acceptance criteria
|
|
79
|
+
* @returns {boolean} True if UI verification is mentioned
|
|
80
|
+
*/
|
|
81
|
+
function hasUIVerificationInAcceptance(acceptance) {
|
|
82
|
+
// Flatten acceptance to array of strings
|
|
83
|
+
let criteria = [];
|
|
84
|
+
if (Array.isArray(acceptance)) {
|
|
85
|
+
criteria = acceptance;
|
|
86
|
+
}
|
|
87
|
+
else if (typeof acceptance === 'object' && acceptance !== null) {
|
|
88
|
+
// Nested object format: { category: [items] }
|
|
89
|
+
criteria = Object.values(acceptance).flat();
|
|
90
|
+
}
|
|
91
|
+
// Search for UI-related keywords (case-insensitive)
|
|
92
|
+
const lowerCriteria = criteria.map((c) => (typeof c === 'string' ? c.toLowerCase() : ''));
|
|
93
|
+
return lowerCriteria.some((criterion) => UI_VERIFICATION_KEYWORDS.some((keyword) => criterion.includes(keyword.toLowerCase())));
|
|
94
|
+
}
|
|
95
|
+
export function validateExposure(wu, options = {}) {
|
|
96
|
+
const warnings = [];
|
|
97
|
+
// Early return if skip flag is set
|
|
98
|
+
if (options.skipExposureCheck) {
|
|
99
|
+
return { valid: true, warnings: [] };
|
|
100
|
+
}
|
|
101
|
+
const wuId = wu.id || 'WU-???';
|
|
102
|
+
const exposure = wu.exposure;
|
|
103
|
+
// Check 1: exposure field presence
|
|
104
|
+
if (!exposure) {
|
|
105
|
+
warnings.push(EXPOSURE_WARNING_MESSAGES.MISSING_EXPOSURE(wuId));
|
|
106
|
+
// Can't check further without exposure
|
|
107
|
+
return { valid: true, warnings };
|
|
108
|
+
}
|
|
109
|
+
// Check 2 & 3: API exposure checks
|
|
110
|
+
if (exposure === WU_EXPOSURE.API) {
|
|
111
|
+
// Check for ui_pairing_wus
|
|
112
|
+
const uiPairingWus = wu.ui_pairing_wus;
|
|
113
|
+
if (!uiPairingWus || uiPairingWus.length === 0) {
|
|
114
|
+
warnings.push(EXPOSURE_WARNING_MESSAGES.MISSING_UI_PAIRING(wuId));
|
|
115
|
+
}
|
|
116
|
+
// Check acceptance criteria for UI verification mention
|
|
117
|
+
const acceptance = wu.acceptance;
|
|
118
|
+
if (acceptance && !hasUIVerificationInAcceptance(acceptance)) {
|
|
119
|
+
warnings.push(EXPOSURE_WARNING_MESSAGES.MISSING_UI_VERIFICATION(wuId));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Check 4: UI exposure checks
|
|
123
|
+
if (exposure === WU_EXPOSURE.UI) {
|
|
124
|
+
// Recommend user_journey if not present
|
|
125
|
+
if (!wu.user_journey) {
|
|
126
|
+
warnings.push(EXPOSURE_WARNING_MESSAGES.MISSING_USER_JOURNEY(wuId));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// backend-only and documentation exposures: no additional checks
|
|
130
|
+
return { valid: true, warnings };
|
|
131
|
+
}
|
|
132
|
+
// =============================================================================
|
|
133
|
+
// WU-2022: Feature Accessibility Validation (BLOCKING)
|
|
134
|
+
// =============================================================================
|
|
135
|
+
/**
|
|
136
|
+
* Navigation keywords to search for in tests.manual.
|
|
137
|
+
* Case-insensitive patterns that indicate manual navigation testing.
|
|
138
|
+
*/
|
|
139
|
+
const NAVIGATION_KEYWORDS = [
|
|
140
|
+
'navigate',
|
|
141
|
+
'navigation',
|
|
142
|
+
'accessible',
|
|
143
|
+
'access',
|
|
144
|
+
'visible',
|
|
145
|
+
'reachable',
|
|
146
|
+
'go to',
|
|
147
|
+
'visit',
|
|
148
|
+
'open',
|
|
149
|
+
'click',
|
|
150
|
+
'link',
|
|
151
|
+
'route',
|
|
152
|
+
'url',
|
|
153
|
+
'path',
|
|
154
|
+
'/space',
|
|
155
|
+
'/dashboard',
|
|
156
|
+
'/settings',
|
|
157
|
+
];
|
|
158
|
+
/**
|
|
159
|
+
* Pattern to detect Next.js page files in code_paths.
|
|
160
|
+
* Matches: app/.../page.tsx, pages/.../index.tsx, pages/.../*.tsx
|
|
161
|
+
*/
|
|
162
|
+
const PAGE_FILE_PATTERNS = [
|
|
163
|
+
/app\/.*\/page\.tsx$/,
|
|
164
|
+
/app\/.*\/page\.ts$/,
|
|
165
|
+
/pages\/.*\.tsx$/,
|
|
166
|
+
/pages\/.*\.ts$/,
|
|
167
|
+
];
|
|
168
|
+
/**
|
|
169
|
+
* Error message templates for accessibility validation (WU-2022).
|
|
170
|
+
* These are BLOCKING errors, not warnings.
|
|
171
|
+
*/
|
|
172
|
+
export const ACCESSIBILITY_ERROR_MESSAGES = {
|
|
173
|
+
/**
|
|
174
|
+
* Error when UI exposure lacks navigation accessibility proof.
|
|
175
|
+
* @param {string} wuId - The WU identifier
|
|
176
|
+
* @returns {string} Error message with remediation guidance
|
|
177
|
+
*/
|
|
178
|
+
UI_NOT_ACCESSIBLE: (wuId) => `${wuId}: exposure=ui but feature accessibility not verified. ` +
|
|
179
|
+
`Add one of the following:\n` +
|
|
180
|
+
` 1. navigation_path: '/your-route' - specify the route where feature is accessible\n` +
|
|
181
|
+
` 2. code_paths: [..., 'apps/web/src/app/.../page.tsx'] - include a page file\n` +
|
|
182
|
+
` 3. tests.manual: ['Navigate to /path and verify feature is accessible'] - add navigation test\n\n` +
|
|
183
|
+
`This prevents "orphaned code" - features that exist but users cannot access. ` +
|
|
184
|
+
`Use --skip-accessibility-check to bypass (not recommended).`,
|
|
185
|
+
};
|
|
186
|
+
/**
|
|
187
|
+
* Check if code_paths includes a page file (Next.js page).
|
|
188
|
+
*
|
|
189
|
+
* @param {string[]} codePaths - Array of code paths
|
|
190
|
+
* @returns {boolean} True if any code path matches a page file pattern
|
|
191
|
+
*/
|
|
192
|
+
function hasPageFileInCodePaths(codePaths) {
|
|
193
|
+
if (!codePaths || !Array.isArray(codePaths)) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
return codePaths.some((codePath) => PAGE_FILE_PATTERNS.some((pattern) => pattern.test(codePath)));
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Check if tests.manual includes navigation verification.
|
|
200
|
+
*
|
|
201
|
+
* @param {object} tests - Tests object from WU YAML
|
|
202
|
+
* @returns {boolean} True if manual tests mention navigation
|
|
203
|
+
*/
|
|
204
|
+
function hasNavigationInManualTests(tests) {
|
|
205
|
+
if (!tests || typeof tests !== 'object') {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
const manualTests = tests.manual;
|
|
209
|
+
if (!manualTests || !Array.isArray(manualTests)) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
const lowerTests = manualTests.map((t) => (typeof t === 'string' ? t.toLowerCase() : ''));
|
|
213
|
+
return lowerTests.some((test) => NAVIGATION_KEYWORDS.some((keyword) => test.includes(keyword.toLowerCase())));
|
|
214
|
+
}
|
|
215
|
+
export function validateFeatureAccessibility(wu, options = {}) {
|
|
216
|
+
const errors = [];
|
|
217
|
+
// Early return if skip flag is set
|
|
218
|
+
if (options.skipAccessibilityCheck) {
|
|
219
|
+
return { valid: true, errors: [] };
|
|
220
|
+
}
|
|
221
|
+
const exposure = wu.exposure;
|
|
222
|
+
// Skip validation for non-UI exposures
|
|
223
|
+
if (!exposure || exposure !== WU_EXPOSURE.UI) {
|
|
224
|
+
return { valid: true, errors: [] };
|
|
225
|
+
}
|
|
226
|
+
// For exposure=ui, verify accessibility via one of three methods
|
|
227
|
+
const wuId = wu.id || 'WU-???';
|
|
228
|
+
// Method 1: navigation_path is specified
|
|
229
|
+
if (wu.navigation_path && wu.navigation_path.trim().length > 0) {
|
|
230
|
+
return { valid: true, errors: [] };
|
|
231
|
+
}
|
|
232
|
+
// Method 2: code_paths includes a page file
|
|
233
|
+
if (hasPageFileInCodePaths(wu.code_paths)) {
|
|
234
|
+
return { valid: true, errors: [] };
|
|
235
|
+
}
|
|
236
|
+
// Method 3: tests.manual includes navigation verification
|
|
237
|
+
if (hasNavigationInManualTests(wu.tests)) {
|
|
238
|
+
return { valid: true, errors: [] };
|
|
239
|
+
}
|
|
240
|
+
// No accessibility proof found - this is a blocking error
|
|
241
|
+
errors.push(ACCESSIBILITY_ERROR_MESSAGES.UI_NOT_ACCESSIBLE(wuId));
|
|
242
|
+
return { valid: false, errors };
|
|
243
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* WU Validator - Enforces code quality rules from Definition of Done
|
|
4
|
+
*
|
|
5
|
+
* Validates WU completion requirements:
|
|
6
|
+
* - No TODO/FIXME/HACK/XXX comments in production code
|
|
7
|
+
* - No Mock/Stub/Fake classes in production code
|
|
8
|
+
* - Excludes test files from scans
|
|
9
|
+
* - Excludes markdown files from TODO scans (documentation prose)
|
|
10
|
+
*
|
|
11
|
+
* Used by wu:done before creating stamp.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Check if a file path is a test file
|
|
15
|
+
* @param {string} filePath - Path to check
|
|
16
|
+
* @returns {boolean} True if file is a test file
|
|
17
|
+
*/
|
|
18
|
+
export declare function isTestFile(filePath: any): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Check if a file path is a markdown file
|
|
21
|
+
* @param {string} filePath - Path to check
|
|
22
|
+
* @returns {boolean} True if file is a markdown file
|
|
23
|
+
*/
|
|
24
|
+
export declare function isMarkdownFile(filePath: any): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Scan a file for TODO/FIXME/HACK/XXX comments
|
|
27
|
+
* @param {string} filePath - Path to file to scan
|
|
28
|
+
* @returns {{found: boolean, matches: Array<{line: number, text: string, pattern: string}>}}
|
|
29
|
+
*/
|
|
30
|
+
export declare function scanFileForTODOs(filePath: any): {
|
|
31
|
+
found: boolean;
|
|
32
|
+
matches: any[];
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Scan a file for Mock/Stub/Fake class/function names
|
|
36
|
+
* @param {string} filePath - Path to file to scan
|
|
37
|
+
* @returns {{found: boolean, matches: Array<{line: number, text: string, type: string}>}}
|
|
38
|
+
*/
|
|
39
|
+
export declare function scanFileForMocks(filePath: any): {
|
|
40
|
+
found: boolean;
|
|
41
|
+
matches: any[];
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Options for validating WU code paths
|
|
45
|
+
*/
|
|
46
|
+
export interface ValidateWUCodePathsOptions {
|
|
47
|
+
/** Allow TODO comments (with warning) */
|
|
48
|
+
allowTodos?: boolean;
|
|
49
|
+
/** Optional worktree path to validate files from */
|
|
50
|
+
worktreePath?: string | null;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Validate all code paths for a WU
|
|
54
|
+
* @param {Array<string>} codePaths - Array of file/directory paths from WU YAML
|
|
55
|
+
* @param {ValidateWUCodePathsOptions} options - Validation options
|
|
56
|
+
* @returns {{valid: boolean, errors: Array<string>, warnings: Array<string>}}
|
|
57
|
+
*/
|
|
58
|
+
export declare function validateWUCodePaths(codePaths: any, options?: ValidateWUCodePathsOptions): {
|
|
59
|
+
valid: boolean;
|
|
60
|
+
errors: any[];
|
|
61
|
+
warnings: any[];
|
|
62
|
+
};
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* WU Validator - Enforces code quality rules from Definition of Done
|
|
4
|
+
*
|
|
5
|
+
* Validates WU completion requirements:
|
|
6
|
+
* - No TODO/FIXME/HACK/XXX comments in production code
|
|
7
|
+
* - No Mock/Stub/Fake classes in production code
|
|
8
|
+
* - Excludes test files from scans
|
|
9
|
+
* - Excludes markdown files from TODO scans (documentation prose)
|
|
10
|
+
*
|
|
11
|
+
* Used by wu:done before creating stamp.
|
|
12
|
+
*/
|
|
13
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { execSync } from 'node:child_process';
|
|
16
|
+
import { STDIO } from './wu-constants.js';
|
|
17
|
+
/**
|
|
18
|
+
* Check if a file path is a test file
|
|
19
|
+
* @param {string} filePath - Path to check
|
|
20
|
+
* @returns {boolean} True if file is a test file
|
|
21
|
+
*/
|
|
22
|
+
export function isTestFile(filePath) {
|
|
23
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
24
|
+
// Test file patterns
|
|
25
|
+
const testPatterns = [
|
|
26
|
+
/\.test\.(ts|tsx|js|jsx|mjs)$/,
|
|
27
|
+
/\.spec\.(ts|tsx|js|jsx|mjs)$/,
|
|
28
|
+
/__tests__\//,
|
|
29
|
+
/\.test-utils\./,
|
|
30
|
+
/\.mock\./,
|
|
31
|
+
];
|
|
32
|
+
return testPatterns.some((pattern) => pattern.test(normalized));
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Check if a file path is a markdown file
|
|
36
|
+
* @param {string} filePath - Path to check
|
|
37
|
+
* @returns {boolean} True if file is a markdown file
|
|
38
|
+
*/
|
|
39
|
+
export function isMarkdownFile(filePath) {
|
|
40
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
41
|
+
return /\.md$/i.test(normalized);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Scan a file for TODO/FIXME/HACK/XXX comments
|
|
45
|
+
* @param {string} filePath - Path to file to scan
|
|
46
|
+
* @returns {{found: boolean, matches: Array<{line: number, text: string, pattern: string}>}}
|
|
47
|
+
*/
|
|
48
|
+
export function scanFileForTODOs(filePath) {
|
|
49
|
+
if (!existsSync(filePath)) {
|
|
50
|
+
return { found: false, matches: [] };
|
|
51
|
+
}
|
|
52
|
+
// Skip test files
|
|
53
|
+
if (isTestFile(filePath)) {
|
|
54
|
+
return { found: false, matches: [] };
|
|
55
|
+
}
|
|
56
|
+
// Skip markdown files (documentation prose often mentions TODO in workflow explanations)
|
|
57
|
+
if (isMarkdownFile(filePath)) {
|
|
58
|
+
return { found: false, matches: [] };
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const content = readFileSync(filePath, { encoding: 'utf-8' });
|
|
62
|
+
const lines = content.split(/\r?\n/);
|
|
63
|
+
const matches = [];
|
|
64
|
+
// Match TODO/FIXME/HACK/XXX as actionable markers in comments
|
|
65
|
+
// Covers: // TODO:, /* TODO */, * TODO, <!-- TODO -->, # TODO, @todo, etc.
|
|
66
|
+
//
|
|
67
|
+
// WU-1807: Tightened detection to prevent false positives:
|
|
68
|
+
// - Only matches markers at the START of comment text (after comment symbol)
|
|
69
|
+
// - Excludes slash-separated keyword lists like "TODO/FIXME/HACK" (documentation)
|
|
70
|
+
// - Excludes WU-XXX placeholders (not an XXX marker)
|
|
71
|
+
// - Excludes keywords appearing mid-sentence in prose
|
|
72
|
+
//
|
|
73
|
+
// Pattern explanation:
|
|
74
|
+
// - Comment start: ^[\s]*(\/\/|\/\*+|\*|<!--|#)[\s]* captures comment prefixes
|
|
75
|
+
// - Keyword at start: TODO/FIXME/HACK/XXX immediately after comment start
|
|
76
|
+
// - Or @-prefixed: @todo, @fixme, @hack, @xxx anywhere in comment
|
|
77
|
+
/**
|
|
78
|
+
* Check if a line contains an actionable TODO/FIXME/HACK/XXX marker
|
|
79
|
+
* @param {string} line - Line to check
|
|
80
|
+
* @returns {{found: boolean, pattern: string|null}} Match result
|
|
81
|
+
*/
|
|
82
|
+
const checkForActionableMarker = (line) => {
|
|
83
|
+
const trimmed = line.trim();
|
|
84
|
+
// Skip lines that are documentation about the patterns themselves
|
|
85
|
+
// These contain keywords as examples, not actionable markers
|
|
86
|
+
if (trimmed.includes('// TODO:,') || trimmed.includes('/* TODO */')) {
|
|
87
|
+
return { found: false, pattern: null };
|
|
88
|
+
}
|
|
89
|
+
if (trimmed.includes('@todo,') || trimmed.includes('@-prefixed:')) {
|
|
90
|
+
return { found: false, pattern: null };
|
|
91
|
+
}
|
|
92
|
+
// Pattern 1: @-prefixed tags at start of JSDoc comment line
|
|
93
|
+
// Matches: * @todo Implement this
|
|
94
|
+
// Does NOT match: // mentions @todo in documentation
|
|
95
|
+
const atTagMatch = trimmed.match(/^\*\s+@(todo|fixme|hack|xxx)\b/i);
|
|
96
|
+
if (atTagMatch) {
|
|
97
|
+
return { found: true, pattern: atTagMatch[1].toUpperCase() };
|
|
98
|
+
}
|
|
99
|
+
// Pattern 2: Keyword at start of comment content
|
|
100
|
+
// Matches: // TODO:, /* TODO, * TODO, # TODO, <!-- TODO
|
|
101
|
+
// Does NOT match: // mentions TODO in workflow, // TODO/FIXME/HACK list
|
|
102
|
+
const commentStartMatch = trimmed.match(/^(?:\/\/|\/\*+|\*|<!--|#)\s*(TODO|FIXME|HACK|XXX)(?::|[\s]|$)/i);
|
|
103
|
+
if (commentStartMatch) {
|
|
104
|
+
// Check it is not followed by / (slash-separated list)
|
|
105
|
+
const afterKeyword = trimmed.slice(trimmed.indexOf(commentStartMatch[1]) + commentStartMatch[1].length);
|
|
106
|
+
if (!afterKeyword.startsWith('/')) {
|
|
107
|
+
return { found: true, pattern: commentStartMatch[1].toUpperCase() };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Pattern 3: Keyword in inline comment after code
|
|
111
|
+
// Matches: someCode(); // TODO: fix this
|
|
112
|
+
// Does NOT match: someCode(); // mentions TODO in prose
|
|
113
|
+
// Does NOT match: error('// TODO'); (// inside string literal)
|
|
114
|
+
const inlineCommentMatch = line.match(/\/\/\s*(TODO|FIXME|HACK|XXX)(?::|[\s]|$)/i);
|
|
115
|
+
if (inlineCommentMatch && !line.match(/\/\/\s*(TODO|FIXME|HACK|XXX)\//i)) {
|
|
116
|
+
// Verify the // is not inside a string literal
|
|
117
|
+
const doubleSlashIndex = line.indexOf('//');
|
|
118
|
+
const beforeSlash = line.slice(0, doubleSlashIndex);
|
|
119
|
+
// Count unescaped quotes - if odd number, we're inside a string
|
|
120
|
+
const singleQuotes = (beforeSlash.match(/(?<!\\)'/g) || []).length;
|
|
121
|
+
const doubleQuotes = (beforeSlash.match(/(?<!\\)"/g) || []).length;
|
|
122
|
+
const backticks = (beforeSlash.match(/(?<!\\)`/g) || []).length;
|
|
123
|
+
if (singleQuotes % 2 !== 0 || doubleQuotes % 2 !== 0 || backticks % 2 !== 0) {
|
|
124
|
+
// The // is inside a string literal, not a real comment
|
|
125
|
+
return { found: false, pattern: null };
|
|
126
|
+
}
|
|
127
|
+
// Verify the keyword is right after // (not buried in prose)
|
|
128
|
+
const commentPart = line.slice(doubleSlashIndex);
|
|
129
|
+
const keywordIndex = commentPart.search(/\b(TODO|FIXME|HACK|XXX)\b/i);
|
|
130
|
+
// Only flag if keyword appears within first 10 chars of comment
|
|
131
|
+
if (keywordIndex >= 0 && keywordIndex <= 10) {
|
|
132
|
+
return { found: true, pattern: inlineCommentMatch[1].toUpperCase() };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Pattern 4: Special check for XXX - must not be preceded by WU-
|
|
136
|
+
// This is handled by patterns above, but add explicit WU-XXX exclusion
|
|
137
|
+
if (trimmed.match(/\bWU-XXX\b/i)) {
|
|
138
|
+
return { found: false, pattern: null };
|
|
139
|
+
}
|
|
140
|
+
return { found: false, pattern: null };
|
|
141
|
+
};
|
|
142
|
+
lines.forEach((line, index) => {
|
|
143
|
+
const lineNumber = index + 1;
|
|
144
|
+
const trimmed = line.trim();
|
|
145
|
+
// Check if line contains a comment marker
|
|
146
|
+
const isComment = /^(\/\/|\/\*|\*|<!--|#)/.test(trimmed) || line.includes('//') || line.includes('/*');
|
|
147
|
+
if (isComment) {
|
|
148
|
+
const result = checkForActionableMarker(line);
|
|
149
|
+
if (result.found) {
|
|
150
|
+
matches.push({
|
|
151
|
+
line: lineNumber,
|
|
152
|
+
text: trimmed,
|
|
153
|
+
pattern: result.pattern,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
return {
|
|
159
|
+
found: matches.length > 0,
|
|
160
|
+
matches,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// If file can't be read, skip it
|
|
165
|
+
return { found: false, matches: [] };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Scan a file for Mock/Stub/Fake class/function names
|
|
170
|
+
* @param {string} filePath - Path to file to scan
|
|
171
|
+
* @returns {{found: boolean, matches: Array<{line: number, text: string, type: string}>}}
|
|
172
|
+
*/
|
|
173
|
+
export function scanFileForMocks(filePath) {
|
|
174
|
+
if (!existsSync(filePath)) {
|
|
175
|
+
return { found: false, matches: [] };
|
|
176
|
+
}
|
|
177
|
+
// Skip test files
|
|
178
|
+
if (isTestFile(filePath)) {
|
|
179
|
+
return { found: false, matches: [] };
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
const content = readFileSync(filePath, { encoding: 'utf-8' });
|
|
183
|
+
const lines = content.split(/\r?\n/);
|
|
184
|
+
const matches = [];
|
|
185
|
+
// Match Mock/Stub/Fake/Placeholder in class/function/const names
|
|
186
|
+
const mockPatterns = [
|
|
187
|
+
// Classes: class MockService, export class StubAdapter
|
|
188
|
+
{ name: 'Mock', regex: /\b(class|export\s+class)\s+(\w*Mock\w*)/i },
|
|
189
|
+
{ name: 'Stub', regex: /\b(class|export\s+class)\s+(\w*Stub\w*)/i },
|
|
190
|
+
{ name: 'Fake', regex: /\b(class|export\s+class)\s+(\w*Fake\w*)/i },
|
|
191
|
+
{ name: 'Placeholder', regex: /\b(class|export\s+class)\s+(\w*Placeholder\w*)/i },
|
|
192
|
+
// Functions: function mockService, const stubAdapter =
|
|
193
|
+
{ name: 'Mock', regex: /\b(function|const|let|var)\s+(\w*mock\w*)/i },
|
|
194
|
+
{ name: 'Stub', regex: /\b(function|const|let|var)\s+(\w*stub\w*)/i },
|
|
195
|
+
{ name: 'Fake', regex: /\b(function|const|let|var)\s+(\w*fake\w*)/i },
|
|
196
|
+
{ name: 'Placeholder', regex: /\b(function|const|let|var)\s+(\w*placeholder\w*)/i },
|
|
197
|
+
];
|
|
198
|
+
lines.forEach((line, index) => {
|
|
199
|
+
const lineNumber = index + 1;
|
|
200
|
+
mockPatterns.forEach(({ name, regex }) => {
|
|
201
|
+
const match = regex.exec(line);
|
|
202
|
+
if (match) {
|
|
203
|
+
matches.push({
|
|
204
|
+
line: lineNumber,
|
|
205
|
+
text: line.trim(),
|
|
206
|
+
type: name,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
return {
|
|
212
|
+
found: matches.length > 0,
|
|
213
|
+
matches,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
// If file can't be read, skip it
|
|
218
|
+
return { found: false, matches: [] };
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Get the repo root directory
|
|
223
|
+
* @returns {string} Absolute path to repo root
|
|
224
|
+
*/
|
|
225
|
+
function getRepoRoot() {
|
|
226
|
+
try {
|
|
227
|
+
return execSync('git rev-parse --show-toplevel', {
|
|
228
|
+
encoding: 'utf-8',
|
|
229
|
+
stdio: [STDIO.PIPE, STDIO.PIPE, STDIO.IGNORE],
|
|
230
|
+
}).trim();
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return process.cwd();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Validate all code paths for a WU
|
|
238
|
+
* @param {Array<string>} codePaths - Array of file/directory paths from WU YAML
|
|
239
|
+
* @param {ValidateWUCodePathsOptions} options - Validation options
|
|
240
|
+
* @returns {{valid: boolean, errors: Array<string>, warnings: Array<string>}}
|
|
241
|
+
*/
|
|
242
|
+
export function validateWUCodePaths(codePaths, options = {}) {
|
|
243
|
+
const { allowTodos = false, worktreePath = null } = options;
|
|
244
|
+
const errors = [];
|
|
245
|
+
const warnings = [];
|
|
246
|
+
const repoRoot = worktreePath || getRepoRoot();
|
|
247
|
+
if (!codePaths || codePaths.length === 0) {
|
|
248
|
+
return { valid: true, errors, warnings };
|
|
249
|
+
}
|
|
250
|
+
const todoFindings = [];
|
|
251
|
+
const mockFindings = [];
|
|
252
|
+
// Scan each code path
|
|
253
|
+
for (const codePath of codePaths) {
|
|
254
|
+
const absolutePath = path.join(repoRoot, codePath);
|
|
255
|
+
if (!existsSync(absolutePath)) {
|
|
256
|
+
errors.push(`\n❌ Code path validation failed: File does not exist: ${codePath}\n\n` +
|
|
257
|
+
`This indicates the WU claims to have created/modified a file that doesn't exist.\n` +
|
|
258
|
+
`Either create the file, or remove it from code_paths in the WU YAML.\n`);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
// Scan for TODOs
|
|
262
|
+
const todoResult = scanFileForTODOs(absolutePath);
|
|
263
|
+
if (todoResult.found) {
|
|
264
|
+
todoFindings.push({ path: codePath, ...todoResult });
|
|
265
|
+
}
|
|
266
|
+
// Scan for Mocks
|
|
267
|
+
const mockResult = scanFileForMocks(absolutePath);
|
|
268
|
+
if (mockResult.found) {
|
|
269
|
+
mockFindings.push({ path: codePath, ...mockResult });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
// Report TODO findings
|
|
273
|
+
if (todoFindings.length > 0) {
|
|
274
|
+
const message = formatTODOFindings(todoFindings);
|
|
275
|
+
if (allowTodos) {
|
|
276
|
+
warnings.push(message);
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
errors.push(message);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
// Report Mock findings (always warnings, not errors)
|
|
283
|
+
if (mockFindings.length > 0) {
|
|
284
|
+
warnings.push(formatMockFindings(mockFindings));
|
|
285
|
+
}
|
|
286
|
+
return {
|
|
287
|
+
valid: errors.length === 0,
|
|
288
|
+
errors,
|
|
289
|
+
warnings,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Format TODO findings for display
|
|
294
|
+
* @param {Array} findings - TODO findings
|
|
295
|
+
* @returns {string} Formatted message
|
|
296
|
+
*/
|
|
297
|
+
function formatTODOFindings(findings) {
|
|
298
|
+
let msg = '\n❌ TODO/FIXME/HACK/XXX comments found in production code:\n';
|
|
299
|
+
findings.forEach(({ path, matches }) => {
|
|
300
|
+
msg += `\n ${path}:\n`;
|
|
301
|
+
matches.forEach(({ line, text }) => {
|
|
302
|
+
msg += ` Line ${line}: ${text}\n`;
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
msg += '\nThese indicate incomplete work and must be resolved before WU completion.';
|
|
306
|
+
msg += '\nEither complete the work or use --allow-todo with justification in WU notes.';
|
|
307
|
+
return msg;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Format Mock findings for display
|
|
311
|
+
* @param {Array} findings - Mock findings
|
|
312
|
+
* @returns {string} Formatted message
|
|
313
|
+
*/
|
|
314
|
+
function formatMockFindings(findings) {
|
|
315
|
+
let msg = '\n⚠️ Mock/Stub/Fake/Placeholder classes found in production code:\n';
|
|
316
|
+
findings.forEach(({ path, matches }) => {
|
|
317
|
+
msg += `\n ${path}:\n`;
|
|
318
|
+
matches.forEach(({ line, text }) => {
|
|
319
|
+
msg += ` Line ${line}: ${text}\n`;
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
msg += '\nThese suggest incomplete implementation (interface ≠ implementation).';
|
|
323
|
+
msg += '\nVerify these are actual implementations, not placeholder code.';
|
|
324
|
+
return msg;
|
|
325
|
+
}
|