@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.
Files changed (263) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +119 -0
  3. package/dist/active-wu-detector.d.ts +33 -0
  4. package/dist/active-wu-detector.js +106 -0
  5. package/dist/adapters/filesystem-metrics.adapter.d.ts +108 -0
  6. package/dist/adapters/filesystem-metrics.adapter.js +519 -0
  7. package/dist/adapters/terminal-renderer.adapter.d.ts +106 -0
  8. package/dist/adapters/terminal-renderer.adapter.js +337 -0
  9. package/dist/arg-parser.d.ts +63 -0
  10. package/dist/arg-parser.js +560 -0
  11. package/dist/backlog-editor.d.ts +98 -0
  12. package/dist/backlog-editor.js +179 -0
  13. package/dist/backlog-generator.d.ts +111 -0
  14. package/dist/backlog-generator.js +381 -0
  15. package/dist/backlog-parser.d.ts +45 -0
  16. package/dist/backlog-parser.js +102 -0
  17. package/dist/backlog-sync-validator.d.ts +78 -0
  18. package/dist/backlog-sync-validator.js +294 -0
  19. package/dist/branch-drift.d.ts +34 -0
  20. package/dist/branch-drift.js +51 -0
  21. package/dist/cleanup-install-config.d.ts +33 -0
  22. package/dist/cleanup-install-config.js +37 -0
  23. package/dist/cleanup-lock.d.ts +139 -0
  24. package/dist/cleanup-lock.js +313 -0
  25. package/dist/code-path-validator.d.ts +146 -0
  26. package/dist/code-path-validator.js +537 -0
  27. package/dist/code-paths-overlap.d.ts +55 -0
  28. package/dist/code-paths-overlap.js +245 -0
  29. package/dist/commands-logger.d.ts +77 -0
  30. package/dist/commands-logger.js +254 -0
  31. package/dist/commit-message-utils.d.ts +25 -0
  32. package/dist/commit-message-utils.js +41 -0
  33. package/dist/compliance-parser.d.ts +150 -0
  34. package/dist/compliance-parser.js +507 -0
  35. package/dist/constants/backlog-patterns.d.ts +20 -0
  36. package/dist/constants/backlog-patterns.js +23 -0
  37. package/dist/constants/dora-constants.d.ts +49 -0
  38. package/dist/constants/dora-constants.js +53 -0
  39. package/dist/constants/gate-constants.d.ts +15 -0
  40. package/dist/constants/gate-constants.js +15 -0
  41. package/dist/constants/linter-constants.d.ts +16 -0
  42. package/dist/constants/linter-constants.js +16 -0
  43. package/dist/constants/tokenizer-constants.d.ts +15 -0
  44. package/dist/constants/tokenizer-constants.js +15 -0
  45. package/dist/core/scope-checker.d.ts +97 -0
  46. package/dist/core/scope-checker.js +163 -0
  47. package/dist/core/tool-runner.d.ts +161 -0
  48. package/dist/core/tool-runner.js +393 -0
  49. package/dist/core/tool.constants.d.ts +105 -0
  50. package/dist/core/tool.constants.js +101 -0
  51. package/dist/core/tool.schemas.d.ts +226 -0
  52. package/dist/core/tool.schemas.js +226 -0
  53. package/dist/core/worktree-guard.d.ts +130 -0
  54. package/dist/core/worktree-guard.js +242 -0
  55. package/dist/coverage-gate.d.ts +108 -0
  56. package/dist/coverage-gate.js +196 -0
  57. package/dist/date-utils.d.ts +75 -0
  58. package/dist/date-utils.js +140 -0
  59. package/dist/dependency-graph.d.ts +142 -0
  60. package/dist/dependency-graph.js +550 -0
  61. package/dist/dependency-guard.d.ts +54 -0
  62. package/dist/dependency-guard.js +142 -0
  63. package/dist/dependency-validator.d.ts +105 -0
  64. package/dist/dependency-validator.js +154 -0
  65. package/dist/docs-path-validator.d.ts +36 -0
  66. package/dist/docs-path-validator.js +95 -0
  67. package/dist/domain/orchestration.constants.d.ts +99 -0
  68. package/dist/domain/orchestration.constants.js +97 -0
  69. package/dist/domain/orchestration.schemas.d.ts +280 -0
  70. package/dist/domain/orchestration.schemas.js +211 -0
  71. package/dist/domain/orchestration.types.d.ts +133 -0
  72. package/dist/domain/orchestration.types.js +12 -0
  73. package/dist/error-handler.d.ts +116 -0
  74. package/dist/error-handler.js +136 -0
  75. package/dist/file-classifiers.d.ts +62 -0
  76. package/dist/file-classifiers.js +108 -0
  77. package/dist/gates-agent-mode.d.ts +81 -0
  78. package/dist/gates-agent-mode.js +94 -0
  79. package/dist/generate-traceability.d.ts +107 -0
  80. package/dist/generate-traceability.js +411 -0
  81. package/dist/git-adapter.d.ts +395 -0
  82. package/dist/git-adapter.js +649 -0
  83. package/dist/git-staged-validator.d.ts +32 -0
  84. package/dist/git-staged-validator.js +48 -0
  85. package/dist/hardcoded-strings.d.ts +61 -0
  86. package/dist/hardcoded-strings.js +270 -0
  87. package/dist/incremental-lint.d.ts +78 -0
  88. package/dist/incremental-lint.js +129 -0
  89. package/dist/incremental-test.d.ts +39 -0
  90. package/dist/incremental-test.js +61 -0
  91. package/dist/index.d.ts +42 -0
  92. package/dist/index.js +61 -0
  93. package/dist/invariants/check-automated-tests.d.ts +50 -0
  94. package/dist/invariants/check-automated-tests.js +166 -0
  95. package/dist/invariants-runner.d.ts +103 -0
  96. package/dist/invariants-runner.js +527 -0
  97. package/dist/lane-checker.d.ts +50 -0
  98. package/dist/lane-checker.js +319 -0
  99. package/dist/lane-inference.d.ts +39 -0
  100. package/dist/lane-inference.js +195 -0
  101. package/dist/lane-lock.d.ts +211 -0
  102. package/dist/lane-lock.js +474 -0
  103. package/dist/lane-validator.d.ts +48 -0
  104. package/dist/lane-validator.js +114 -0
  105. package/dist/logs-lib.d.ts +104 -0
  106. package/dist/logs-lib.js +207 -0
  107. package/dist/lumenflow-config-schema.d.ts +272 -0
  108. package/dist/lumenflow-config-schema.js +207 -0
  109. package/dist/lumenflow-config.d.ts +95 -0
  110. package/dist/lumenflow-config.js +236 -0
  111. package/dist/manual-test-validator.d.ts +80 -0
  112. package/dist/manual-test-validator.js +200 -0
  113. package/dist/merge-lock.d.ts +115 -0
  114. package/dist/merge-lock.js +251 -0
  115. package/dist/micro-worktree.d.ts +159 -0
  116. package/dist/micro-worktree.js +427 -0
  117. package/dist/migration-deployer.d.ts +69 -0
  118. package/dist/migration-deployer.js +151 -0
  119. package/dist/orchestration-advisory-loader.d.ts +28 -0
  120. package/dist/orchestration-advisory-loader.js +87 -0
  121. package/dist/orchestration-advisory.d.ts +58 -0
  122. package/dist/orchestration-advisory.js +94 -0
  123. package/dist/orchestration-di.d.ts +48 -0
  124. package/dist/orchestration-di.js +57 -0
  125. package/dist/orchestration-rules.d.ts +57 -0
  126. package/dist/orchestration-rules.js +201 -0
  127. package/dist/orphan-detector.d.ts +131 -0
  128. package/dist/orphan-detector.js +226 -0
  129. package/dist/path-classifiers.d.ts +57 -0
  130. package/dist/path-classifiers.js +93 -0
  131. package/dist/piped-command-detector.d.ts +34 -0
  132. package/dist/piped-command-detector.js +64 -0
  133. package/dist/ports/dashboard-renderer.port.d.ts +112 -0
  134. package/dist/ports/dashboard-renderer.port.js +25 -0
  135. package/dist/ports/metrics-collector.port.d.ts +132 -0
  136. package/dist/ports/metrics-collector.port.js +26 -0
  137. package/dist/process-detector.d.ts +84 -0
  138. package/dist/process-detector.js +172 -0
  139. package/dist/prompt-linter.d.ts +72 -0
  140. package/dist/prompt-linter.js +312 -0
  141. package/dist/prompt-monitor.d.ts +15 -0
  142. package/dist/prompt-monitor.js +205 -0
  143. package/dist/rebase-artifact-cleanup.d.ts +145 -0
  144. package/dist/rebase-artifact-cleanup.js +433 -0
  145. package/dist/retry-strategy.d.ts +189 -0
  146. package/dist/retry-strategy.js +283 -0
  147. package/dist/risk-detector.d.ts +108 -0
  148. package/dist/risk-detector.js +252 -0
  149. package/dist/rollback-utils.d.ts +76 -0
  150. package/dist/rollback-utils.js +104 -0
  151. package/dist/section-headings.d.ts +43 -0
  152. package/dist/section-headings.js +49 -0
  153. package/dist/spawn-escalation.d.ts +90 -0
  154. package/dist/spawn-escalation.js +253 -0
  155. package/dist/spawn-monitor.d.ts +229 -0
  156. package/dist/spawn-monitor.js +672 -0
  157. package/dist/spawn-recovery.d.ts +82 -0
  158. package/dist/spawn-recovery.js +298 -0
  159. package/dist/spawn-registry-schema.d.ts +98 -0
  160. package/dist/spawn-registry-schema.js +108 -0
  161. package/dist/spawn-registry-store.d.ts +146 -0
  162. package/dist/spawn-registry-store.js +273 -0
  163. package/dist/spawn-tree.d.ts +121 -0
  164. package/dist/spawn-tree.js +285 -0
  165. package/dist/stamp-status-validator.d.ts +84 -0
  166. package/dist/stamp-status-validator.js +134 -0
  167. package/dist/stamp-utils.d.ts +100 -0
  168. package/dist/stamp-utils.js +229 -0
  169. package/dist/state-machine.d.ts +26 -0
  170. package/dist/state-machine.js +83 -0
  171. package/dist/system-map-validator.d.ts +80 -0
  172. package/dist/system-map-validator.js +272 -0
  173. package/dist/telemetry.d.ts +80 -0
  174. package/dist/telemetry.js +213 -0
  175. package/dist/token-counter.d.ts +51 -0
  176. package/dist/token-counter.js +145 -0
  177. package/dist/usecases/get-dashboard-data.usecase.d.ts +52 -0
  178. package/dist/usecases/get-dashboard-data.usecase.js +61 -0
  179. package/dist/usecases/get-suggestions.usecase.d.ts +100 -0
  180. package/dist/usecases/get-suggestions.usecase.js +153 -0
  181. package/dist/user-normalizer.d.ts +41 -0
  182. package/dist/user-normalizer.js +141 -0
  183. package/dist/validators/phi-constants.d.ts +97 -0
  184. package/dist/validators/phi-constants.js +152 -0
  185. package/dist/validators/phi-scanner.d.ts +58 -0
  186. package/dist/validators/phi-scanner.js +215 -0
  187. package/dist/worktree-ownership.d.ts +50 -0
  188. package/dist/worktree-ownership.js +74 -0
  189. package/dist/worktree-scanner.d.ts +103 -0
  190. package/dist/worktree-scanner.js +168 -0
  191. package/dist/worktree-symlink.d.ts +99 -0
  192. package/dist/worktree-symlink.js +359 -0
  193. package/dist/wu-backlog-updater.d.ts +17 -0
  194. package/dist/wu-backlog-updater.js +37 -0
  195. package/dist/wu-checkpoint.d.ts +124 -0
  196. package/dist/wu-checkpoint.js +233 -0
  197. package/dist/wu-claim-helpers.d.ts +26 -0
  198. package/dist/wu-claim-helpers.js +63 -0
  199. package/dist/wu-claim-resume.d.ts +106 -0
  200. package/dist/wu-claim-resume.js +276 -0
  201. package/dist/wu-consistency-checker.d.ts +95 -0
  202. package/dist/wu-consistency-checker.js +567 -0
  203. package/dist/wu-constants.d.ts +1275 -0
  204. package/dist/wu-constants.js +1382 -0
  205. package/dist/wu-create-validators.d.ts +42 -0
  206. package/dist/wu-create-validators.js +93 -0
  207. package/dist/wu-done-branch-only.d.ts +63 -0
  208. package/dist/wu-done-branch-only.js +191 -0
  209. package/dist/wu-done-messages.d.ts +119 -0
  210. package/dist/wu-done-messages.js +185 -0
  211. package/dist/wu-done-pr.d.ts +72 -0
  212. package/dist/wu-done-pr.js +174 -0
  213. package/dist/wu-done-retry-helpers.d.ts +85 -0
  214. package/dist/wu-done-retry-helpers.js +172 -0
  215. package/dist/wu-done-ui.d.ts +37 -0
  216. package/dist/wu-done-ui.js +69 -0
  217. package/dist/wu-done-validators.d.ts +411 -0
  218. package/dist/wu-done-validators.js +1229 -0
  219. package/dist/wu-done-worktree.d.ts +182 -0
  220. package/dist/wu-done-worktree.js +1097 -0
  221. package/dist/wu-helpers.d.ts +128 -0
  222. package/dist/wu-helpers.js +248 -0
  223. package/dist/wu-lint.d.ts +70 -0
  224. package/dist/wu-lint.js +234 -0
  225. package/dist/wu-paths.d.ts +171 -0
  226. package/dist/wu-paths.js +178 -0
  227. package/dist/wu-preflight-validators.d.ts +86 -0
  228. package/dist/wu-preflight-validators.js +251 -0
  229. package/dist/wu-recovery.d.ts +138 -0
  230. package/dist/wu-recovery.js +341 -0
  231. package/dist/wu-repair-core.d.ts +131 -0
  232. package/dist/wu-repair-core.js +669 -0
  233. package/dist/wu-schema-normalization.d.ts +17 -0
  234. package/dist/wu-schema-normalization.js +82 -0
  235. package/dist/wu-schema.d.ts +793 -0
  236. package/dist/wu-schema.js +881 -0
  237. package/dist/wu-spawn-helpers.d.ts +121 -0
  238. package/dist/wu-spawn-helpers.js +271 -0
  239. package/dist/wu-spawn.d.ts +158 -0
  240. package/dist/wu-spawn.js +1306 -0
  241. package/dist/wu-state-schema.d.ts +213 -0
  242. package/dist/wu-state-schema.js +156 -0
  243. package/dist/wu-state-store.d.ts +264 -0
  244. package/dist/wu-state-store.js +691 -0
  245. package/dist/wu-status-transition.d.ts +63 -0
  246. package/dist/wu-status-transition.js +382 -0
  247. package/dist/wu-status-updater.d.ts +25 -0
  248. package/dist/wu-status-updater.js +116 -0
  249. package/dist/wu-transaction-collectors.d.ts +116 -0
  250. package/dist/wu-transaction-collectors.js +272 -0
  251. package/dist/wu-transaction.d.ts +170 -0
  252. package/dist/wu-transaction.js +273 -0
  253. package/dist/wu-validation-constants.d.ts +60 -0
  254. package/dist/wu-validation-constants.js +66 -0
  255. package/dist/wu-validation.d.ts +118 -0
  256. package/dist/wu-validation.js +243 -0
  257. package/dist/wu-validator.d.ts +62 -0
  258. package/dist/wu-validator.js +325 -0
  259. package/dist/wu-yaml-fixer.d.ts +97 -0
  260. package/dist/wu-yaml-fixer.js +264 -0
  261. package/dist/wu-yaml.d.ts +86 -0
  262. package/dist/wu-yaml.js +222 -0
  263. package/package.json +114 -0
@@ -0,0 +1,58 @@
1
+ /**
2
+ * PHI (Protected Health Information) Scanner
3
+ *
4
+ * Detects potential PHI in content using library-first approach:
5
+ * - NHS numbers validated with nhs-number-validator (Modulus 11 checksum)
6
+ * - UK postcodes parsed with postcode library, flagged only in medical context
7
+ *
8
+ * Part of WU-1404: PHI Scanner Integration
9
+ *
10
+ * @see {@link https://github.com/spikeheap/nhs-number-validator} NHS validation
11
+ * @see {@link https://github.com/ideal-postcodes/postcode} Postcode parsing
12
+ */
13
+ /**
14
+ * Check if a file path should be excluded from PHI scanning
15
+ *
16
+ * @param {string|null|undefined} filePath - Path to check
17
+ * @returns {boolean} True if path should be excluded
18
+ */
19
+ export declare function isPathExcluded(filePath: any): boolean;
20
+ /**
21
+ * Options for PHI scanning
22
+ */
23
+ export interface ScanForPHIOptions {
24
+ /** File path for exclusion check */
25
+ filePath?: string;
26
+ }
27
+ /**
28
+ * Scan content for PHI (Protected Health Information)
29
+ *
30
+ * Detects:
31
+ * - Valid NHS numbers (validated with Modulus 11 checksum)
32
+ * - UK postcodes in medical context
33
+ *
34
+ * @param {string|null|undefined} content - Content to scan
35
+ * @param {ScanForPHIOptions} [options] - Scan options
36
+ * @returns {{hasPHI: boolean, matches: Array, warnings: string[]}} Scan result
37
+ */
38
+ interface PHIMatch {
39
+ type: string;
40
+ value: string;
41
+ startIndex: number;
42
+ endIndex: number;
43
+ medicalKeyword?: string;
44
+ }
45
+ export declare function scanForPHI(content: string, options?: ScanForPHIOptions): {
46
+ hasPHI: boolean;
47
+ matches: PHIMatch[];
48
+ warnings: string[];
49
+ filePath?: string;
50
+ };
51
+ /**
52
+ * Create a human-readable summary of PHI matches
53
+ *
54
+ * @param {Array} matches - PHI matches from scanForPHI
55
+ * @returns {string} Summary message
56
+ */
57
+ export declare function formatPHISummary(matches: any): string;
58
+ export {};
@@ -0,0 +1,215 @@
1
+ /**
2
+ * PHI (Protected Health Information) Scanner
3
+ *
4
+ * Detects potential PHI in content using library-first approach:
5
+ * - NHS numbers validated with nhs-number-validator (Modulus 11 checksum)
6
+ * - UK postcodes parsed with postcode library, flagged only in medical context
7
+ *
8
+ * Part of WU-1404: PHI Scanner Integration
9
+ *
10
+ * @see {@link https://github.com/spikeheap/nhs-number-validator} NHS validation
11
+ * @see {@link https://github.com/ideal-postcodes/postcode} Postcode parsing
12
+ */
13
+ import nhsValidator from 'nhs-number-validator';
14
+ import { isValid as isValidPostcode, parse as parsePostcode } from 'postcode';
15
+ import { PHI_TYPES, MEDICAL_CONTEXT_KEYWORDS, MEDICAL_CONTEXT_WINDOW_SIZE, TEST_NHS_NUMBERS, NHS_TEST_PREFIX, TEST_POSTCODES, TEST_DATA_MARKERS, EXCLUDED_PATH_PATTERNS, NHS_CANDIDATE_PATTERN, } from './phi-constants.js';
16
+ /**
17
+ * Check if a file path should be excluded from PHI scanning
18
+ *
19
+ * @param {string|null|undefined} filePath - Path to check
20
+ * @returns {boolean} True if path should be excluded
21
+ */
22
+ export function isPathExcluded(filePath) {
23
+ if (!filePath || typeof filePath !== 'string') {
24
+ return false;
25
+ }
26
+ return EXCLUDED_PATH_PATTERNS.some((pattern) => pattern.test(filePath));
27
+ }
28
+ /**
29
+ * Check if content contains test data markers
30
+ *
31
+ * @param {string} content - Content to check
32
+ * @returns {boolean} True if test markers are present
33
+ */
34
+ function hasTestDataMarkers(content) {
35
+ const contentLower = content.toLowerCase();
36
+ return TEST_DATA_MARKERS.some((marker) => contentLower.includes(marker.toLowerCase()));
37
+ }
38
+ /**
39
+ * Normalize NHS number by removing spaces and dashes
40
+ *
41
+ * @param {string} nhsNumber - NHS number with possible formatting
42
+ * @returns {string} Normalized 10-digit NHS number
43
+ */
44
+ function normalizeNhsNumber(nhsNumber) {
45
+ return nhsNumber.replace(/[\s-]/g, '');
46
+ }
47
+ /**
48
+ * Check if NHS number is a known test number
49
+ *
50
+ * @param {string} nhsNumber - Normalized NHS number
51
+ * @returns {boolean} True if it's a test number
52
+ */
53
+ function isTestNhsNumber(nhsNumber) {
54
+ // Check against explicit test numbers
55
+ if (TEST_NHS_NUMBERS.includes(nhsNumber)) {
56
+ return true;
57
+ }
58
+ // Check for 999 prefix (NHS Digital test range)
59
+ if (nhsNumber.startsWith(NHS_TEST_PREFIX)) {
60
+ return true;
61
+ }
62
+ return false;
63
+ }
64
+ /**
65
+ * Normalize postcode for comparison (uppercase, no spaces)
66
+ *
67
+ * @param {string} postcode - Postcode to normalize
68
+ * @returns {string} Normalized postcode
69
+ */
70
+ function normalizePostcode(postcode) {
71
+ return postcode.toUpperCase().replace(/\s/g, '');
72
+ }
73
+ /**
74
+ * Check if postcode is a known test postcode
75
+ *
76
+ * @param {string} postcode - Postcode to check
77
+ * @returns {boolean} True if it's a test postcode
78
+ */
79
+ function isTestPostcode(postcode) {
80
+ const normalized = normalizePostcode(postcode);
81
+ return TEST_POSTCODES.some((testPc) => normalizePostcode(testPc) === normalized);
82
+ }
83
+ /**
84
+ * Check if there's a medical context keyword within the context window
85
+ *
86
+ * @param {string} content - Full content
87
+ * @param {number} postcodeIndex - Index of postcode in content
88
+ * @param {number} postcodeLength - Length of the postcode string
89
+ * @returns {{found: boolean, keyword?: string}} Medical context result
90
+ */
91
+ function findMedicalContext(content, postcodeIndex, postcodeLength) {
92
+ // Define the window around the postcode
93
+ const windowStart = Math.max(0, postcodeIndex - MEDICAL_CONTEXT_WINDOW_SIZE);
94
+ const windowEnd = Math.min(content.length, postcodeIndex + postcodeLength + MEDICAL_CONTEXT_WINDOW_SIZE);
95
+ const windowContent = content.slice(windowStart, windowEnd).toLowerCase();
96
+ for (const keyword of MEDICAL_CONTEXT_KEYWORDS) {
97
+ if (windowContent.includes(keyword.toLowerCase())) {
98
+ return { found: true, keyword };
99
+ }
100
+ }
101
+ return { found: false };
102
+ }
103
+ /**
104
+ * Extract potential UK postcodes from content
105
+ *
106
+ * UK postcode format is complex - we use the postcode library for validation
107
+ * but need to extract candidates first. The library's isValid handles edge cases.
108
+ *
109
+ * @param {string} content - Content to scan
110
+ * @returns {Array<{value: string, index: number}>} Postcode candidates with positions
111
+ */
112
+ function extractPostcodeCandidates(content) {
113
+ const candidates = [];
114
+ // UK postcode pattern (simplified - library validates properly)
115
+ // Format: A(A)N(N) NAA or variations
116
+ const postcodePattern = /\b([A-Z]{1,2}\d[A-Z\d]?\s*\d[A-Z]{2})\b/gi;
117
+ let match;
118
+ while ((match = postcodePattern.exec(content)) !== null) {
119
+ const candidate = match[1];
120
+ // Validate with library
121
+ if (isValidPostcode(candidate)) {
122
+ const parsed = parsePostcode(candidate);
123
+ if (parsed.valid) {
124
+ candidates.push({
125
+ value: parsed.postcode, // Normalized postcode
126
+ index: match.index,
127
+ originalValue: match[1],
128
+ });
129
+ }
130
+ }
131
+ }
132
+ return candidates;
133
+ }
134
+ export function scanForPHI(content, options = {}) {
135
+ const result = {
136
+ hasPHI: false,
137
+ matches: [],
138
+ warnings: [],
139
+ };
140
+ // Handle null/undefined/empty content
141
+ if (!content || typeof content !== 'string' || content.trim() === '') {
142
+ return result;
143
+ }
144
+ // Check path exclusions
145
+ if (options.filePath && isPathExcluded(options.filePath)) {
146
+ result.warnings.push(`Path excluded from PHI scanning: ${options.filePath}`);
147
+ return result;
148
+ }
149
+ // Check for test data markers
150
+ if (hasTestDataMarkers(content)) {
151
+ result.warnings.push('Test data markers detected - PHI scanning skipped');
152
+ return result;
153
+ }
154
+ // Scan for NHS numbers
155
+ const nhsCandidates = [...content.matchAll(NHS_CANDIDATE_PATTERN)];
156
+ for (const match of nhsCandidates) {
157
+ const rawNumber = match[1];
158
+ const normalized = normalizeNhsNumber(rawNumber);
159
+ // Skip if it's a test number
160
+ if (isTestNhsNumber(normalized)) {
161
+ continue;
162
+ }
163
+ // Validate with library (Modulus 11 checksum)
164
+ if (nhsValidator.validate(normalized)) {
165
+ result.matches.push({
166
+ type: PHI_TYPES.NHS_NUMBER,
167
+ value: normalized,
168
+ startIndex: match.index,
169
+ endIndex: match.index + rawNumber.length,
170
+ });
171
+ }
172
+ }
173
+ // Scan for postcodes in medical context
174
+ const postcodeCandidates = extractPostcodeCandidates(content);
175
+ for (const candidate of postcodeCandidates) {
176
+ // Skip test postcodes
177
+ if (isTestPostcode(candidate.value)) {
178
+ continue;
179
+ }
180
+ // Check for medical context
181
+ const medicalContext = findMedicalContext(content, candidate.index, candidate.value.length);
182
+ if (medicalContext.found) {
183
+ result.matches.push({
184
+ type: PHI_TYPES.POSTCODE_MEDICAL_CONTEXT,
185
+ value: candidate.value,
186
+ startIndex: candidate.index,
187
+ endIndex: candidate.index + candidate.originalValue.length,
188
+ medicalKeyword: medicalContext.keyword,
189
+ });
190
+ }
191
+ }
192
+ result.hasPHI = result.matches.length > 0;
193
+ return result;
194
+ }
195
+ /**
196
+ * Create a human-readable summary of PHI matches
197
+ *
198
+ * @param {Array} matches - PHI matches from scanForPHI
199
+ * @returns {string} Summary message
200
+ */
201
+ export function formatPHISummary(matches) {
202
+ if (matches.length === 0) {
203
+ return 'No PHI detected';
204
+ }
205
+ const nhsCount = matches.filter((m) => m.type === PHI_TYPES.NHS_NUMBER).length;
206
+ const postcodeCount = matches.filter((m) => m.type === PHI_TYPES.POSTCODE_MEDICAL_CONTEXT).length;
207
+ const parts = [];
208
+ if (nhsCount > 0) {
209
+ parts.push(`${nhsCount} NHS number${nhsCount > 1 ? 's' : ''}`);
210
+ }
211
+ if (postcodeCount > 0) {
212
+ parts.push(`${postcodeCount} postcode${postcodeCount > 1 ? 's' : ''} in medical context`);
213
+ }
214
+ return `PHI detected: ${parts.join(', ')}`;
215
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * WU-2278: Worktree Ownership Validation
3
+ *
4
+ * Validates that a WU can only clean up its own worktree.
5
+ * Prevents cross-agent worktree deletion during parallel execution.
6
+ *
7
+ * Note: No external library exists for LumenFlow-specific worktree ownership
8
+ * validation - this is internal workflow tooling.
9
+ *
10
+ * @module worktree-ownership
11
+ */
12
+ /**
13
+ * Extract WU ID from worktree path
14
+ *
15
+ * Worktree paths follow pattern: worktrees/<lane>-wu-<id>
16
+ * Examples:
17
+ * - worktrees/operations-wu-100 -> WU-100
18
+ * - worktrees/operations-tooling-wu-2278 -> WU-2278
19
+ * - worktrees/experience-chat-wu-500 -> WU-500
20
+ *
21
+ * @param {string} worktreePath - Path to worktree
22
+ * @returns {string|null} Extracted WU ID (uppercase) or null if not found
23
+ */
24
+ export declare function extractWUFromWorktreePath(worktreePath: any): string;
25
+ /**
26
+ * Validate that the WU can safely clean up the given worktree
27
+ *
28
+ * Blocks deletion when:
29
+ * - Worktree path contains a different WU ID
30
+ *
31
+ * Allows deletion when:
32
+ * - Worktree path is null/undefined (nothing to clean up)
33
+ * - Worktree path matches the WU ID
34
+ * - Worktree path doesn't follow WU naming convention (manual worktree)
35
+ *
36
+ * @param {Object} params - Validation parameters
37
+ * @param {string|null|undefined} params.worktreePath - Path to worktree
38
+ * @param {string} params.wuId - WU ID attempting cleanup (e.g., "WU-100")
39
+ * @returns {{ valid: boolean, error?: string }} Validation result
40
+ */
41
+ export declare function validateWorktreeOwnership({ worktreePath, wuId }: {
42
+ worktreePath: any;
43
+ wuId: any;
44
+ }): {
45
+ valid: boolean;
46
+ error?: undefined;
47
+ } | {
48
+ valid: boolean;
49
+ error: string;
50
+ };
@@ -0,0 +1,74 @@
1
+ /**
2
+ * WU-2278: Worktree Ownership Validation
3
+ *
4
+ * Validates that a WU can only clean up its own worktree.
5
+ * Prevents cross-agent worktree deletion during parallel execution.
6
+ *
7
+ * Note: No external library exists for LumenFlow-specific worktree ownership
8
+ * validation - this is internal workflow tooling.
9
+ *
10
+ * @module worktree-ownership
11
+ */
12
+ /**
13
+ * Extract WU ID from worktree path
14
+ *
15
+ * Worktree paths follow pattern: worktrees/<lane>-wu-<id>
16
+ * Examples:
17
+ * - worktrees/operations-wu-100 -> WU-100
18
+ * - worktrees/operations-tooling-wu-2278 -> WU-2278
19
+ * - worktrees/experience-chat-wu-500 -> WU-500
20
+ *
21
+ * @param {string} worktreePath - Path to worktree
22
+ * @returns {string|null} Extracted WU ID (uppercase) or null if not found
23
+ */
24
+ export function extractWUFromWorktreePath(worktreePath) {
25
+ if (!worktreePath || typeof worktreePath !== 'string') {
26
+ return null;
27
+ }
28
+ // Match wu-<id> pattern at end of path (case insensitive)
29
+ const match = worktreePath.match(/wu-(\d+)(?:\/)?$/i);
30
+ if (!match) {
31
+ return null;
32
+ }
33
+ return `WU-${match[1]}`;
34
+ }
35
+ /**
36
+ * Validate that the WU can safely clean up the given worktree
37
+ *
38
+ * Blocks deletion when:
39
+ * - Worktree path contains a different WU ID
40
+ *
41
+ * Allows deletion when:
42
+ * - Worktree path is null/undefined (nothing to clean up)
43
+ * - Worktree path matches the WU ID
44
+ * - Worktree path doesn't follow WU naming convention (manual worktree)
45
+ *
46
+ * @param {Object} params - Validation parameters
47
+ * @param {string|null|undefined} params.worktreePath - Path to worktree
48
+ * @param {string} params.wuId - WU ID attempting cleanup (e.g., "WU-100")
49
+ * @returns {{ valid: boolean, error?: string }} Validation result
50
+ */
51
+ export function validateWorktreeOwnership({ worktreePath, wuId }) {
52
+ // No worktree path = nothing to validate
53
+ if (!worktreePath) {
54
+ return { valid: true };
55
+ }
56
+ const worktreeWuId = extractWUFromWorktreePath(worktreePath);
57
+ // If worktree doesn't follow WU naming, allow cleanup (manual worktree)
58
+ if (!worktreeWuId) {
59
+ return {
60
+ valid: false,
61
+ error: `Worktree ownership mismatch: cannot determine owner of ${worktreePath}`,
62
+ };
63
+ }
64
+ // Normalize WU IDs for comparison (case insensitive)
65
+ const normalizedWorktreeId = worktreeWuId.toUpperCase();
66
+ const normalizedWuId = wuId.toUpperCase();
67
+ if (normalizedWorktreeId !== normalizedWuId) {
68
+ return {
69
+ valid: false,
70
+ error: `Worktree ownership mismatch: worktree belongs to ${normalizedWorktreeId}, but ${normalizedWuId} attempted cleanup. This could delete another agent's work.`,
71
+ };
72
+ }
73
+ return { valid: true };
74
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Worktree Scanner (WU-1748)
3
+ *
4
+ * Scans existing git worktrees to detect uncommitted changes.
5
+ * Provides cross-agent visibility for abandoned WU work.
6
+ *
7
+ * Features:
8
+ * - Parses git worktree list output
9
+ * - Detects uncommitted changes per worktree
10
+ * - Reports last activity timestamp
11
+ * - Identifies potentially abandoned WUs
12
+ *
13
+ * @see {@link tools/lib/__tests__/worktree-scanner.test.mjs} - Tests
14
+ */
15
+ /**
16
+ * @typedef {object} WorktreeInfo
17
+ * @property {string} path - Absolute path to worktree
18
+ * @property {string} sha - Current commit SHA
19
+ * @property {string} branch - Branch name or "(detached HEAD)"
20
+ * @property {boolean} isMain - Whether this is the main worktree
21
+ * @property {string} [wuId] - WU ID if this is a lane worktree
22
+ */
23
+ /**
24
+ * @typedef {object} WorktreeStatus
25
+ * @property {boolean} hasUncommittedChanges - Whether there are uncommitted changes
26
+ * @property {number} uncommittedFileCount - Number of uncommitted files
27
+ * @property {string[]} uncommittedFiles - List of uncommitted file paths
28
+ * @property {string} lastActivityTimestamp - ISO timestamp of last git activity
29
+ * @property {string} [error] - Error message if git commands failed
30
+ */
31
+ /**
32
+ * @typedef {object} WorktreeScanResult
33
+ * @property {(WorktreeInfo & WorktreeStatus)[]} worktrees - All WU worktrees with status
34
+ * @property {(WorktreeInfo & WorktreeStatus)[]} worktreesWithUncommittedWork - Worktrees with uncommitted changes
35
+ * @property {object} summary - Summary statistics
36
+ * @property {number} summary.totalWorktrees - Total number of WU worktrees
37
+ * @property {number} summary.withUncommittedChanges - Number with uncommitted changes
38
+ * @property {number} summary.totalUncommittedFiles - Total uncommitted files across all worktrees
39
+ */
40
+ /**
41
+ * Parses git worktree list output into structured data.
42
+ *
43
+ * @param {string} output - Raw output from `git worktree list`
44
+ * @returns {WorktreeInfo[]} Parsed worktree information
45
+ *
46
+ * @example
47
+ * const info = parseWorktreeList('/home/user/project abc1234 [main]');
48
+ * // Returns: [{ path: '/home/user/project', sha: 'abc1234', branch: 'main', isMain: true }]
49
+ */
50
+ export declare function parseWorktreeList(output: any): any[];
51
+ /**
52
+ * Gets the status of a single worktree including uncommitted changes.
53
+ *
54
+ * @param {string} worktreePath - Path to the worktree
55
+ * @param {object} [options] - Options
56
+ * @param {WorktreeScannerOptions} [options] - Options
57
+ * @returns {Promise<WorktreeStatus>} Worktree status
58
+ *
59
+ * @example
60
+ * const status = await getWorktreeStatus('/path/to/worktree');
61
+ * if (status.hasUncommittedChanges) {
62
+ * console.log(`Found ${status.uncommittedFileCount} uncommitted files`);
63
+ * }
64
+ */
65
+ interface WorktreeScannerOptions {
66
+ /** Custom exec function for testing */
67
+ execAsync?: (cmd: string) => Promise<{
68
+ stdout: string;
69
+ stderr: string;
70
+ }>;
71
+ }
72
+ export declare function getWorktreeStatus(worktreePath: any, options?: WorktreeScannerOptions): Promise<{
73
+ hasUncommittedChanges: boolean;
74
+ uncommittedFileCount: number;
75
+ uncommittedFiles: string[];
76
+ lastActivityTimestamp: string;
77
+ error?: string;
78
+ }>;
79
+ /**
80
+ * Scans all worktrees and returns their status.
81
+ *
82
+ * Excludes the main worktree and focuses on WU worktrees (lane branches).
83
+ *
84
+ * @param {string} basePath - Path to main repository
85
+ * @param {WorktreeScannerOptions} [options] - Options
86
+ * @returns {Promise<WorktreeScanResult>} Scan results with all worktrees and summary
87
+ *
88
+ * @example
89
+ * const result = await scanWorktrees('/path/to/repo');
90
+ * for (const wt of result.worktreesWithUncommittedWork) {
91
+ * console.log(`${wt.wuId}: ${wt.uncommittedFileCount} uncommitted files`);
92
+ * }
93
+ */
94
+ export declare function scanWorktrees(basePath: any, options?: WorktreeScannerOptions): Promise<{
95
+ worktrees: any[];
96
+ worktreesWithUncommittedWork: any[];
97
+ summary: {
98
+ totalWorktrees: number;
99
+ withUncommittedChanges: number;
100
+ totalUncommittedFiles: any;
101
+ };
102
+ }>;
103
+ export {};
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Worktree Scanner (WU-1748)
3
+ *
4
+ * Scans existing git worktrees to detect uncommitted changes.
5
+ * Provides cross-agent visibility for abandoned WU work.
6
+ *
7
+ * Features:
8
+ * - Parses git worktree list output
9
+ * - Detects uncommitted changes per worktree
10
+ * - Reports last activity timestamp
11
+ * - Identifies potentially abandoned WUs
12
+ *
13
+ * @see {@link tools/lib/__tests__/worktree-scanner.test.mjs} - Tests
14
+ */
15
+ import { exec } from 'node:child_process';
16
+ import { promisify } from 'node:util';
17
+ const execAsync = promisify(exec);
18
+ /**
19
+ * Regex pattern to extract WU ID from lane branch name
20
+ * Matches patterns like: lane/operations/wu-1234, lane/operations-tooling/wu-1234
21
+ */
22
+ const WU_ID_PATTERN = /wu-(\d+)$/i;
23
+ /**
24
+ * Regex pattern to parse git worktree list output line
25
+ * Format: /path/to/worktree SHA1 [branch] or (detached HEAD)
26
+ */
27
+ const WORKTREE_LINE_PATTERN = /^(\S+)\s+(\S+)\s+(?:\[([^\]]+)\]|\(([^)]+)\))$/;
28
+ /**
29
+ * @typedef {object} WorktreeInfo
30
+ * @property {string} path - Absolute path to worktree
31
+ * @property {string} sha - Current commit SHA
32
+ * @property {string} branch - Branch name or "(detached HEAD)"
33
+ * @property {boolean} isMain - Whether this is the main worktree
34
+ * @property {string} [wuId] - WU ID if this is a lane worktree
35
+ */
36
+ /**
37
+ * @typedef {object} WorktreeStatus
38
+ * @property {boolean} hasUncommittedChanges - Whether there are uncommitted changes
39
+ * @property {number} uncommittedFileCount - Number of uncommitted files
40
+ * @property {string[]} uncommittedFiles - List of uncommitted file paths
41
+ * @property {string} lastActivityTimestamp - ISO timestamp of last git activity
42
+ * @property {string} [error] - Error message if git commands failed
43
+ */
44
+ /**
45
+ * @typedef {object} WorktreeScanResult
46
+ * @property {(WorktreeInfo & WorktreeStatus)[]} worktrees - All WU worktrees with status
47
+ * @property {(WorktreeInfo & WorktreeStatus)[]} worktreesWithUncommittedWork - Worktrees with uncommitted changes
48
+ * @property {object} summary - Summary statistics
49
+ * @property {number} summary.totalWorktrees - Total number of WU worktrees
50
+ * @property {number} summary.withUncommittedChanges - Number with uncommitted changes
51
+ * @property {number} summary.totalUncommittedFiles - Total uncommitted files across all worktrees
52
+ */
53
+ /**
54
+ * Parses git worktree list output into structured data.
55
+ *
56
+ * @param {string} output - Raw output from `git worktree list`
57
+ * @returns {WorktreeInfo[]} Parsed worktree information
58
+ *
59
+ * @example
60
+ * const info = parseWorktreeList('/home/user/project abc1234 [main]');
61
+ * // Returns: [{ path: '/home/user/project', sha: 'abc1234', branch: 'main', isMain: true }]
62
+ */
63
+ export function parseWorktreeList(output) {
64
+ if (!output || !output.trim()) {
65
+ return [];
66
+ }
67
+ const lines = output.trim().split('\n');
68
+ const worktrees = [];
69
+ for (const line of lines) {
70
+ const trimmed = line.trim();
71
+ if (!trimmed)
72
+ continue;
73
+ const match = trimmed.match(WORKTREE_LINE_PATTERN);
74
+ if (!match)
75
+ continue;
76
+ const [, path, sha, bracketBranch, parenBranch] = match;
77
+ const branch = bracketBranch || parenBranch;
78
+ const isMain = branch === 'main' || branch === 'master';
79
+ /** @type {WorktreeInfo} */
80
+ const info = {
81
+ path,
82
+ sha,
83
+ branch,
84
+ isMain,
85
+ };
86
+ // Extract WU ID from lane branch name
87
+ const wuMatch = branch.match(WU_ID_PATTERN);
88
+ if (wuMatch) {
89
+ info.wuId = `WU-${wuMatch[1]}`;
90
+ }
91
+ worktrees.push(info);
92
+ }
93
+ return worktrees;
94
+ }
95
+ export async function getWorktreeStatus(worktreePath, options = {}) {
96
+ const runCmd = options.execAsync || execAsync;
97
+ /** @type {WorktreeStatus} */
98
+ const status = {
99
+ hasUncommittedChanges: false,
100
+ uncommittedFileCount: 0,
101
+ uncommittedFiles: [],
102
+ lastActivityTimestamp: '',
103
+ };
104
+ try {
105
+ // Get uncommitted changes via git status --porcelain
106
+ const statusResult = await runCmd(`git -C "${worktreePath}" status --porcelain`);
107
+ // Note: Don't trim() the whole output as it would remove leading space from first line
108
+ // Git status --porcelain format: XY filename (2 chars + space + path)
109
+ const statusOutput = statusResult.stdout;
110
+ if (statusOutput && statusOutput.trim()) {
111
+ const files = statusOutput
112
+ .split('\n')
113
+ .filter((line) => line.length > 3) // Filter empty lines
114
+ .map((line) => line.slice(3).trim()); // Remove XY prefix and trim path
115
+ status.uncommittedFiles = files;
116
+ status.uncommittedFileCount = files.length;
117
+ status.hasUncommittedChanges = files.length > 0;
118
+ }
119
+ // Get last activity timestamp from git log
120
+ const logResult = await runCmd(`git -C "${worktreePath}" log -1 --format=%aI 2>/dev/null || echo ""`);
121
+ status.lastActivityTimestamp = logResult.stdout.trim();
122
+ }
123
+ catch (error) {
124
+ status.error = error instanceof Error ? error.message : String(error);
125
+ }
126
+ return status;
127
+ }
128
+ /**
129
+ * Scans all worktrees and returns their status.
130
+ *
131
+ * Excludes the main worktree and focuses on WU worktrees (lane branches).
132
+ *
133
+ * @param {string} basePath - Path to main repository
134
+ * @param {WorktreeScannerOptions} [options] - Options
135
+ * @returns {Promise<WorktreeScanResult>} Scan results with all worktrees and summary
136
+ *
137
+ * @example
138
+ * const result = await scanWorktrees('/path/to/repo');
139
+ * for (const wt of result.worktreesWithUncommittedWork) {
140
+ * console.log(`${wt.wuId}: ${wt.uncommittedFileCount} uncommitted files`);
141
+ * }
142
+ */
143
+ export async function scanWorktrees(basePath, options = {}) {
144
+ const runCmd = options.execAsync || execAsync;
145
+ // Get worktree list
146
+ const listResult = await runCmd(`git -C "${basePath}" worktree list`);
147
+ const allWorktrees = parseWorktreeList(listResult.stdout);
148
+ // Filter to WU worktrees only (exclude main)
149
+ const wuWorktrees = allWorktrees.filter((wt) => !wt.isMain && wt.wuId);
150
+ // Get status for each WU worktree
151
+ const worktreesWithStatus = await Promise.all(wuWorktrees.map(async (wt) => {
152
+ const status = await getWorktreeStatus(wt.path, { execAsync: runCmd });
153
+ return { ...wt, ...status };
154
+ }));
155
+ // Filter to those with uncommitted changes
156
+ const worktreesWithUncommittedWork = worktreesWithStatus.filter((wt) => wt.hasUncommittedChanges);
157
+ // Calculate summary
158
+ const summary = {
159
+ totalWorktrees: worktreesWithStatus.length,
160
+ withUncommittedChanges: worktreesWithUncommittedWork.length,
161
+ totalUncommittedFiles: worktreesWithStatus.reduce((sum, wt) => sum + wt.uncommittedFileCount, 0),
162
+ };
163
+ return {
164
+ worktrees: worktreesWithStatus,
165
+ worktreesWithUncommittedWork,
166
+ summary,
167
+ };
168
+ }