@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,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stamp File Utilities
|
|
3
|
+
*
|
|
4
|
+
* Centralized stamp file operations (create, validate)
|
|
5
|
+
* Eliminates magic string for stamp body template
|
|
6
|
+
*
|
|
7
|
+
* WU-2242: Added format validation for corrupted stamp detection
|
|
8
|
+
*
|
|
9
|
+
* Stamp files (.beacon/stamps/WU-{id}.done) serve as completion markers
|
|
10
|
+
* Used by wu:done, wu:recovery, and validation tools
|
|
11
|
+
*/
|
|
12
|
+
/* eslint-disable security/detect-non-literal-fs-filename */
|
|
13
|
+
import { existsSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
14
|
+
import { readFile, access } from 'node:fs/promises';
|
|
15
|
+
import { constants } from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { WU_PATHS } from './wu-paths.js';
|
|
18
|
+
import { todayISO } from './date-utils.js';
|
|
19
|
+
/**
|
|
20
|
+
* Stamp format error types (WU-2242)
|
|
21
|
+
* @readonly
|
|
22
|
+
* @enum {string}
|
|
23
|
+
*/
|
|
24
|
+
export const STAMP_FORMAT_ERRORS = Object.freeze({
|
|
25
|
+
/** Stamp file is empty or contains only whitespace */
|
|
26
|
+
EMPTY_FILE: 'EMPTY_FILE',
|
|
27
|
+
/** Missing WU identifier line (format: WU WU-123 (em dash) Title) */
|
|
28
|
+
MISSING_WU_LINE: 'MISSING_WU_LINE',
|
|
29
|
+
/** Missing Completed: YYYY-MM-DD line */
|
|
30
|
+
MISSING_COMPLETED_LINE: 'MISSING_COMPLETED_LINE',
|
|
31
|
+
/** Date is not in valid YYYY-MM-DD format or is invalid */
|
|
32
|
+
INVALID_DATE_FORMAT: 'INVALID_DATE_FORMAT',
|
|
33
|
+
/** WU ID in stamp does not match expected ID */
|
|
34
|
+
WU_ID_MISMATCH: 'WU_ID_MISMATCH',
|
|
35
|
+
});
|
|
36
|
+
/**
|
|
37
|
+
* Valid date regex: YYYY-MM-DD format
|
|
38
|
+
* @type {RegExp}
|
|
39
|
+
*/
|
|
40
|
+
const DATE_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
41
|
+
/**
|
|
42
|
+
* Validate that a date string is a valid ISO date
|
|
43
|
+
* @param {string} dateStr - Date string in YYYY-MM-DD format
|
|
44
|
+
* @returns {boolean} True if date is valid
|
|
45
|
+
*/
|
|
46
|
+
function isValidDate(dateStr) {
|
|
47
|
+
const match = dateStr.match(DATE_PATTERN);
|
|
48
|
+
if (!match) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
const year = parseInt(match[1], 10);
|
|
52
|
+
const month = parseInt(match[2], 10);
|
|
53
|
+
const day = parseInt(match[3], 10);
|
|
54
|
+
// Basic validation
|
|
55
|
+
if (month < 1 || month > 12) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
// Check day validity for the month
|
|
59
|
+
const daysInMonth = new Date(year, month, 0).getDate();
|
|
60
|
+
if (day < 1 || day > daysInMonth) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Stamp file body template (eliminates magic string)
|
|
67
|
+
* Single source of truth for stamp format
|
|
68
|
+
*/
|
|
69
|
+
const STAMP_TEMPLATE = (id, title, timestamp) => `WU ${id} — ${title}\nCompleted: ${timestamp}\n`;
|
|
70
|
+
/**
|
|
71
|
+
* Create stamp file (idempotent - safe to call multiple times)
|
|
72
|
+
*
|
|
73
|
+
* @param {object} params - Parameters
|
|
74
|
+
* @param {string} params.id - WU ID (e.g., 'WU-123')
|
|
75
|
+
* @param {string} params.title - WU title
|
|
76
|
+
* @returns {object} Result { created: boolean, path: string, reason?: string }
|
|
77
|
+
*/
|
|
78
|
+
export function createStamp({ id, title }) {
|
|
79
|
+
const stampsDir = WU_PATHS.STAMPS_DIR();
|
|
80
|
+
const stampPath = WU_PATHS.STAMP(id);
|
|
81
|
+
// Ensure stamps directory exists
|
|
82
|
+
if (!existsSync(stampsDir)) {
|
|
83
|
+
mkdirSync(stampsDir, { recursive: true });
|
|
84
|
+
}
|
|
85
|
+
// Idempotent: skip if stamp already exists
|
|
86
|
+
if (existsSync(stampPath)) {
|
|
87
|
+
return { created: false, path: stampPath, reason: 'already_exists' };
|
|
88
|
+
}
|
|
89
|
+
// Create stamp file
|
|
90
|
+
const body = STAMP_TEMPLATE(id, title, todayISO());
|
|
91
|
+
writeFileSync(stampPath, body, { encoding: 'utf-8' });
|
|
92
|
+
return { created: true, path: stampPath };
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Validate stamp exists
|
|
96
|
+
*
|
|
97
|
+
* @param {string} stampPath - Path to stamp file
|
|
98
|
+
* @returns {boolean} True if stamp exists
|
|
99
|
+
*/
|
|
100
|
+
export function validateStamp(stampPath) {
|
|
101
|
+
return existsSync(stampPath);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Get stamp path using WU_PATHS (consistent with codebase)
|
|
105
|
+
*
|
|
106
|
+
* @param {string} id - WU ID
|
|
107
|
+
* @returns {string} Absolute path to stamp file
|
|
108
|
+
*/
|
|
109
|
+
export function getStampPath(id) {
|
|
110
|
+
return WU_PATHS.STAMP(id);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Validate WU line in stamp content
|
|
114
|
+
* Checks for format: "WU WU-123 (em dash) Title"
|
|
115
|
+
* @param {string[]} lines - Stamp file lines
|
|
116
|
+
* @param {string} expectedWuId - Expected WU ID
|
|
117
|
+
* @returns {string|null} Error type or null if valid
|
|
118
|
+
*/
|
|
119
|
+
function validateWuLine(lines, expectedWuId) {
|
|
120
|
+
const wuLine = lines.find((line) => line.startsWith('WU '));
|
|
121
|
+
if (!wuLine) {
|
|
122
|
+
return STAMP_FORMAT_ERRORS.MISSING_WU_LINE;
|
|
123
|
+
}
|
|
124
|
+
const wuIdMatch = wuLine.match(/^WU (WU-\d+)/);
|
|
125
|
+
if (!wuIdMatch) {
|
|
126
|
+
return STAMP_FORMAT_ERRORS.MISSING_WU_LINE;
|
|
127
|
+
}
|
|
128
|
+
if (wuIdMatch[1] !== expectedWuId) {
|
|
129
|
+
return STAMP_FORMAT_ERRORS.WU_ID_MISMATCH;
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Validate Completed line in stamp content
|
|
135
|
+
* @param {string[]} lines - Stamp file lines
|
|
136
|
+
* @returns {string|null} Error type or null if valid
|
|
137
|
+
*/
|
|
138
|
+
function validateCompletedLine(lines) {
|
|
139
|
+
const completedLine = lines.find((line) => line.startsWith('Completed:'));
|
|
140
|
+
if (!completedLine) {
|
|
141
|
+
return STAMP_FORMAT_ERRORS.MISSING_COMPLETED_LINE;
|
|
142
|
+
}
|
|
143
|
+
const dateMatch = completedLine.match(/^Completed:\s*(.+)/);
|
|
144
|
+
if (!dateMatch) {
|
|
145
|
+
return STAMP_FORMAT_ERRORS.MISSING_COMPLETED_LINE;
|
|
146
|
+
}
|
|
147
|
+
const dateStr = dateMatch[1].trim();
|
|
148
|
+
if (!isValidDate(dateStr)) {
|
|
149
|
+
return STAMP_FORMAT_ERRORS.INVALID_DATE_FORMAT;
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Validate stamp file format (WU-2242)
|
|
155
|
+
*
|
|
156
|
+
* Expected format:
|
|
157
|
+
* ```
|
|
158
|
+
* WU WU-123 (em dash) Title here
|
|
159
|
+
* Completed: 2025-12-31
|
|
160
|
+
* ```
|
|
161
|
+
*
|
|
162
|
+
* @param {string} wuId - WU ID (e.g., 'WU-123')
|
|
163
|
+
* @param {string} [projectRoot=process.cwd()] - Project root directory
|
|
164
|
+
* @returns {Promise<{valid: boolean, errors: string[], missing?: boolean}>}
|
|
165
|
+
*/
|
|
166
|
+
export async function validateStampFormat(wuId, projectRoot = process.cwd()) {
|
|
167
|
+
const stampPath = path.join(projectRoot, WU_PATHS.STAMP(wuId));
|
|
168
|
+
// Check if stamp file exists
|
|
169
|
+
try {
|
|
170
|
+
await access(stampPath, constants.R_OK);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return { valid: false, errors: [], missing: true };
|
|
174
|
+
}
|
|
175
|
+
// Read stamp content
|
|
176
|
+
let content;
|
|
177
|
+
try {
|
|
178
|
+
content = await readFile(stampPath, { encoding: 'utf-8' });
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
182
|
+
return { valid: false, errors: [`Failed to read stamp: ${message}`] };
|
|
183
|
+
}
|
|
184
|
+
// Check for empty file
|
|
185
|
+
if (content.trim() === '') {
|
|
186
|
+
return { valid: false, errors: [STAMP_FORMAT_ERRORS.EMPTY_FILE] };
|
|
187
|
+
}
|
|
188
|
+
const lines = content.split('\n');
|
|
189
|
+
const errors = [];
|
|
190
|
+
// Validate WU line
|
|
191
|
+
const wuLineError = validateWuLine(lines, wuId);
|
|
192
|
+
if (wuLineError) {
|
|
193
|
+
errors.push(wuLineError);
|
|
194
|
+
}
|
|
195
|
+
// Validate Completed line
|
|
196
|
+
const completedError = validateCompletedLine(lines);
|
|
197
|
+
if (completedError) {
|
|
198
|
+
errors.push(completedError);
|
|
199
|
+
}
|
|
200
|
+
return { valid: errors.length === 0, errors };
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Parse stamp content to extract metadata
|
|
204
|
+
*
|
|
205
|
+
* @param {string} content - Stamp file content
|
|
206
|
+
* @returns {StampMetadata}
|
|
207
|
+
*/
|
|
208
|
+
export function parseStampContent(content) {
|
|
209
|
+
const result = {};
|
|
210
|
+
const lines = content.split('\n');
|
|
211
|
+
// Parse WU line
|
|
212
|
+
const wuLine = lines.find((line) => line.startsWith('WU '));
|
|
213
|
+
if (wuLine) {
|
|
214
|
+
const match = wuLine.match(/^WU (WU-\d+)\s*[—-]\s*(.+)/);
|
|
215
|
+
if (match) {
|
|
216
|
+
result.wuId = match[1];
|
|
217
|
+
result.title = match[2].trim();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// Parse Completed line
|
|
221
|
+
const completedLine = lines.find((line) => line.startsWith('Completed:'));
|
|
222
|
+
if (completedLine) {
|
|
223
|
+
const match = completedLine.match(/^Completed:\s*(.+)/);
|
|
224
|
+
if (match) {
|
|
225
|
+
result.completedDate = match[1].trim();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return result;
|
|
229
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State Machine Validation Library
|
|
3
|
+
*
|
|
4
|
+
* Enforces canonical WU state transitions according to LumenFlow §2.4
|
|
5
|
+
* Prevents illegal state changes (e.g., done → in_progress) and ensures workflow integrity.
|
|
6
|
+
*
|
|
7
|
+
* Canonical state machine:
|
|
8
|
+
* - ready → in_progress (claim)
|
|
9
|
+
* - in_progress → blocked (block)
|
|
10
|
+
* - in_progress → waiting (implementation complete, awaiting sign-off)
|
|
11
|
+
* - in_progress → done (direct completion)
|
|
12
|
+
* - blocked → in_progress (unblock)
|
|
13
|
+
* - blocked → done (blocker resolved, direct completion)
|
|
14
|
+
* - waiting → in_progress (changes requested)
|
|
15
|
+
* - waiting → done (approved)
|
|
16
|
+
* - done → (terminal, no transitions)
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Validates a state transition and throws if illegal
|
|
20
|
+
*
|
|
21
|
+
* @param {string|null|undefined} from - Current WU status
|
|
22
|
+
* @param {string|null|undefined} to - Desired WU status
|
|
23
|
+
* @param {string} wuid - Work Unit ID (e.g., 'WU-416') for error messages
|
|
24
|
+
* @throws {Error} If transition is illegal or states are invalid
|
|
25
|
+
*/
|
|
26
|
+
export declare function assertTransition(from: any, to: any, wuid: any): void;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State Machine Validation Library
|
|
3
|
+
*
|
|
4
|
+
* Enforces canonical WU state transitions according to LumenFlow §2.4
|
|
5
|
+
* Prevents illegal state changes (e.g., done → in_progress) and ensures workflow integrity.
|
|
6
|
+
*
|
|
7
|
+
* Canonical state machine:
|
|
8
|
+
* - ready → in_progress (claim)
|
|
9
|
+
* - in_progress → blocked (block)
|
|
10
|
+
* - in_progress → waiting (implementation complete, awaiting sign-off)
|
|
11
|
+
* - in_progress → done (direct completion)
|
|
12
|
+
* - blocked → in_progress (unblock)
|
|
13
|
+
* - blocked → done (blocker resolved, direct completion)
|
|
14
|
+
* - waiting → in_progress (changes requested)
|
|
15
|
+
* - waiting → done (approved)
|
|
16
|
+
* - done → (terminal, no transitions)
|
|
17
|
+
*/
|
|
18
|
+
import { createError, ErrorCodes } from './error-handler.js';
|
|
19
|
+
/**
|
|
20
|
+
* Valid WU states as defined in LumenFlow §2.4
|
|
21
|
+
*/
|
|
22
|
+
const VALID_STATES = new Set(['ready', 'in_progress', 'blocked', 'waiting', 'done']);
|
|
23
|
+
/**
|
|
24
|
+
* Transition table mapping each state to its allowed next states
|
|
25
|
+
* Based on LumenFlow §2.4 Flow States & Lanes
|
|
26
|
+
*/
|
|
27
|
+
const TRANSITIONS = {
|
|
28
|
+
ready: ['in_progress'],
|
|
29
|
+
in_progress: ['blocked', 'waiting', 'done'],
|
|
30
|
+
blocked: ['in_progress', 'done'],
|
|
31
|
+
waiting: ['in_progress', 'done'],
|
|
32
|
+
done: [], // Terminal state - no outgoing transitions
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Validates a state transition and throws if illegal
|
|
36
|
+
*
|
|
37
|
+
* @param {string|null|undefined} from - Current WU status
|
|
38
|
+
* @param {string|null|undefined} to - Desired WU status
|
|
39
|
+
* @param {string} wuid - Work Unit ID (e.g., 'WU-416') for error messages
|
|
40
|
+
* @throws {Error} If transition is illegal or states are invalid
|
|
41
|
+
*/
|
|
42
|
+
export function assertTransition(from, to, wuid) {
|
|
43
|
+
// Validate states exist and are non-empty
|
|
44
|
+
if (from === null || from === undefined || from === '') {
|
|
45
|
+
throw createError(ErrorCodes.STATE_ERROR, `Invalid state: ${from}`, {
|
|
46
|
+
wuid,
|
|
47
|
+
from,
|
|
48
|
+
to,
|
|
49
|
+
reason: 'from state is null/undefined/empty',
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
if (to === null || to === undefined || to === '') {
|
|
53
|
+
throw createError(ErrorCodes.STATE_ERROR, `Invalid state: ${to}`, {
|
|
54
|
+
wuid,
|
|
55
|
+
from,
|
|
56
|
+
to,
|
|
57
|
+
reason: 'to state is null/undefined/empty',
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
// Validate states are recognized
|
|
61
|
+
if (!VALID_STATES.has(from)) {
|
|
62
|
+
throw createError(ErrorCodes.STATE_ERROR, `Invalid state: ${from}`, {
|
|
63
|
+
wuid,
|
|
64
|
+
from,
|
|
65
|
+
to,
|
|
66
|
+
validStates: Array.from(VALID_STATES),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
if (!VALID_STATES.has(to)) {
|
|
70
|
+
throw createError(ErrorCodes.STATE_ERROR, `Invalid state: ${to}`, {
|
|
71
|
+
wuid,
|
|
72
|
+
from,
|
|
73
|
+
to,
|
|
74
|
+
validStates: Array.from(VALID_STATES),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
// Check if transition is allowed
|
|
78
|
+
const allowedNextStates = TRANSITIONS[from];
|
|
79
|
+
if (!allowedNextStates.includes(to)) {
|
|
80
|
+
const terminalHint = from === 'done' ? ' (done is a terminal state)' : '';
|
|
81
|
+
throw createError(ErrorCodes.STATE_ERROR, `Illegal state transition for ${wuid}: ${from} → ${to}${terminalHint}`, { wuid, from, to, allowedNextStates });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* System Map Validator Library
|
|
4
|
+
*
|
|
5
|
+
* Validates SYSTEM-MAP.yaml integrity:
|
|
6
|
+
* 1. All paths resolve to existing files/folders
|
|
7
|
+
* 2. No orphan docs not in map
|
|
8
|
+
* 3. Audience tags from canonical list
|
|
9
|
+
* 4. quick_queries resolve correctly
|
|
10
|
+
* 5. No PHI entries tagged for investor/public
|
|
11
|
+
*
|
|
12
|
+
* @module system-map-validator
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Canonical list of valid audience tags as defined in SYSTEM-MAP.yaml header
|
|
16
|
+
* @type {string[]}
|
|
17
|
+
*/
|
|
18
|
+
export declare const CANONICAL_AUDIENCES: string[];
|
|
19
|
+
/**
|
|
20
|
+
* Canonical list of valid classification levels
|
|
21
|
+
* From least to most restrictive: public < internal < confidential < restricted
|
|
22
|
+
* @type {string[]}
|
|
23
|
+
*/
|
|
24
|
+
export declare const CANONICAL_CLASSIFICATIONS: string[];
|
|
25
|
+
/**
|
|
26
|
+
* Validate all paths in system map exist
|
|
27
|
+
*
|
|
28
|
+
* @param {object} systemMap - Parsed SYSTEM-MAP.yaml
|
|
29
|
+
* @param {{exists: (path: string) => boolean}} deps - Dependencies
|
|
30
|
+
* @returns {Promise<string[]>} Array of error messages
|
|
31
|
+
*/
|
|
32
|
+
export declare function validatePaths(systemMap: any, deps: any): Promise<any[]>;
|
|
33
|
+
/**
|
|
34
|
+
* Find orphan docs not indexed in system map
|
|
35
|
+
*
|
|
36
|
+
* @param {object} systemMap - Parsed SYSTEM-MAP.yaml
|
|
37
|
+
* @param {{glob: (pattern: string) => Promise<string[]>}} deps - Dependencies
|
|
38
|
+
* @returns {Promise<string[]>} Array of orphan file paths
|
|
39
|
+
*/
|
|
40
|
+
export declare function findOrphanDocs(systemMap: any, deps: any): Promise<any[]>;
|
|
41
|
+
/**
|
|
42
|
+
* Validate audience tags against canonical list
|
|
43
|
+
*
|
|
44
|
+
* @param {object} systemMap - Parsed SYSTEM-MAP.yaml
|
|
45
|
+
* @returns {string[]} Array of error messages
|
|
46
|
+
*/
|
|
47
|
+
export declare function validateAudienceTags(systemMap: any): any[];
|
|
48
|
+
/**
|
|
49
|
+
* Validate quick_queries reference valid document IDs
|
|
50
|
+
*
|
|
51
|
+
* @param {object} systemMap - Parsed SYSTEM-MAP.yaml
|
|
52
|
+
* @returns {string[]} Array of error messages
|
|
53
|
+
*/
|
|
54
|
+
export declare function validateQuickQueries(systemMap: any): any[];
|
|
55
|
+
/**
|
|
56
|
+
* Validate classification prevents PHI routing to investor/public
|
|
57
|
+
*
|
|
58
|
+
* Rule: restricted (PHI) data should NOT be accessible to external audiences
|
|
59
|
+
* - restricted = PHI data, must NOT go to investor/patient/clinician
|
|
60
|
+
* - confidential = sensitive but OK for investor (investor docs ARE confidential)
|
|
61
|
+
*
|
|
62
|
+
* @param {object} systemMap - Parsed SYSTEM-MAP.yaml
|
|
63
|
+
* @returns {string[]} Array of error messages
|
|
64
|
+
*/
|
|
65
|
+
export declare function validateClassificationRouting(systemMap: any): any[];
|
|
66
|
+
/**
|
|
67
|
+
* Validate entire system map
|
|
68
|
+
*
|
|
69
|
+
* @param {object} systemMap - Parsed SYSTEM-MAP.yaml
|
|
70
|
+
* @param {{exists: (path: string) => boolean, glob: (pattern: string) => Promise<string[]>}} deps - Dependencies
|
|
71
|
+
* @returns {Promise<{valid: boolean, pathErrors: string[], orphanDocs: string[], audienceErrors: string[], queryErrors: string[], classificationErrors: string[]}>}
|
|
72
|
+
*/
|
|
73
|
+
export declare function validateSystemMap(systemMap: any, deps: any): Promise<{
|
|
74
|
+
valid: boolean;
|
|
75
|
+
pathErrors: any[];
|
|
76
|
+
orphanDocs: any[];
|
|
77
|
+
audienceErrors: any[];
|
|
78
|
+
queryErrors: any[];
|
|
79
|
+
classificationErrors: any[];
|
|
80
|
+
}>;
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* System Map Validator Library
|
|
4
|
+
*
|
|
5
|
+
* Validates SYSTEM-MAP.yaml integrity:
|
|
6
|
+
* 1. All paths resolve to existing files/folders
|
|
7
|
+
* 2. No orphan docs not in map
|
|
8
|
+
* 3. Audience tags from canonical list
|
|
9
|
+
* 4. quick_queries resolve correctly
|
|
10
|
+
* 5. No PHI entries tagged for investor/public
|
|
11
|
+
*
|
|
12
|
+
* @module system-map-validator
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Canonical list of valid audience tags as defined in SYSTEM-MAP.yaml header
|
|
16
|
+
* @type {string[]}
|
|
17
|
+
*/
|
|
18
|
+
export const CANONICAL_AUDIENCES = [
|
|
19
|
+
'ceo',
|
|
20
|
+
'cto',
|
|
21
|
+
'engineer',
|
|
22
|
+
'compliance',
|
|
23
|
+
'investor',
|
|
24
|
+
'agent',
|
|
25
|
+
'patient',
|
|
26
|
+
'clinician',
|
|
27
|
+
];
|
|
28
|
+
/**
|
|
29
|
+
* Canonical list of valid classification levels
|
|
30
|
+
* From least to most restrictive: public < internal < confidential < restricted
|
|
31
|
+
* @type {string[]}
|
|
32
|
+
*/
|
|
33
|
+
export const CANONICAL_CLASSIFICATIONS = ['public', 'internal', 'confidential', 'restricted'];
|
|
34
|
+
/**
|
|
35
|
+
* Audiences that should NOT have access to restricted (PHI) data
|
|
36
|
+
* These are considered "external" audiences who should never see PHI
|
|
37
|
+
* Note: investor CAN see confidential (investor materials ARE confidential)
|
|
38
|
+
* @type {string[]}
|
|
39
|
+
*/
|
|
40
|
+
const PHI_RESTRICTED_AUDIENCES = ['investor', 'patient', 'clinician', 'public'];
|
|
41
|
+
/**
|
|
42
|
+
* Extract all document entries from system map (flattens all layer arrays)
|
|
43
|
+
*
|
|
44
|
+
* @param {object} systemMap - Parsed SYSTEM-MAP.yaml
|
|
45
|
+
* @returns {Array<{id: string, path?: string, paths?: string[], audiences: string[], classification: string, summary: string}>}
|
|
46
|
+
*/
|
|
47
|
+
function extractAllEntries(systemMap) {
|
|
48
|
+
const entries = [];
|
|
49
|
+
const skipKeys = ['quick_queries'];
|
|
50
|
+
for (const [key, value] of Object.entries(systemMap)) {
|
|
51
|
+
if (skipKeys.includes(key))
|
|
52
|
+
continue;
|
|
53
|
+
if (Array.isArray(value)) {
|
|
54
|
+
entries.push(...value);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return entries;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Get all paths from an entry (handles both path and paths fields)
|
|
61
|
+
*
|
|
62
|
+
* @param {{path?: string, paths?: string[]}} entry - Document entry
|
|
63
|
+
* @returns {string[]}
|
|
64
|
+
*/
|
|
65
|
+
function getEntryPaths(entry) {
|
|
66
|
+
const result = [];
|
|
67
|
+
if (entry.path)
|
|
68
|
+
result.push(entry.path);
|
|
69
|
+
if (entry.paths && Array.isArray(entry.paths))
|
|
70
|
+
result.push(...entry.paths);
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Build a set of all indexed paths from the system map
|
|
75
|
+
*
|
|
76
|
+
* @param {object} systemMap - Parsed SYSTEM-MAP.yaml
|
|
77
|
+
* @returns {Set<string>}
|
|
78
|
+
*/
|
|
79
|
+
function buildIndexedPathsSet(systemMap) {
|
|
80
|
+
const indexedPaths = new Set();
|
|
81
|
+
const entries = extractAllEntries(systemMap);
|
|
82
|
+
for (const entry of entries) {
|
|
83
|
+
const paths = getEntryPaths(entry);
|
|
84
|
+
for (const p of paths) {
|
|
85
|
+
indexedPaths.add(p);
|
|
86
|
+
// For directory paths, also add the prefix for matching
|
|
87
|
+
if (p.endsWith('/')) {
|
|
88
|
+
indexedPaths.add(p);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return indexedPaths;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Build a set of all document IDs from the system map
|
|
96
|
+
*
|
|
97
|
+
* @param {object} systemMap - Parsed SYSTEM-MAP.yaml
|
|
98
|
+
* @returns {Set<string>}
|
|
99
|
+
*/
|
|
100
|
+
function buildIdSet(systemMap) {
|
|
101
|
+
const idSet = new Set();
|
|
102
|
+
const entries = extractAllEntries(systemMap);
|
|
103
|
+
for (const entry of entries) {
|
|
104
|
+
if (entry.id) {
|
|
105
|
+
idSet.add(entry.id);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return idSet;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Validate all paths in system map exist
|
|
112
|
+
*
|
|
113
|
+
* @param {object} systemMap - Parsed SYSTEM-MAP.yaml
|
|
114
|
+
* @param {{exists: (path: string) => boolean}} deps - Dependencies
|
|
115
|
+
* @returns {Promise<string[]>} Array of error messages
|
|
116
|
+
*/
|
|
117
|
+
export async function validatePaths(systemMap, deps) {
|
|
118
|
+
const errors = [];
|
|
119
|
+
const entries = extractAllEntries(systemMap);
|
|
120
|
+
for (const entry of entries) {
|
|
121
|
+
const paths = getEntryPaths(entry);
|
|
122
|
+
for (const p of paths) {
|
|
123
|
+
if (!deps.exists(p)) {
|
|
124
|
+
errors.push(`Path not found: ${p} (entry: ${entry.id})`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return errors;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Find orphan docs not indexed in system map
|
|
132
|
+
*
|
|
133
|
+
* @param {object} systemMap - Parsed SYSTEM-MAP.yaml
|
|
134
|
+
* @param {{glob: (pattern: string) => Promise<string[]>}} deps - Dependencies
|
|
135
|
+
* @returns {Promise<string[]>} Array of orphan file paths
|
|
136
|
+
*/
|
|
137
|
+
export async function findOrphanDocs(systemMap, deps) {
|
|
138
|
+
const indexedPaths = buildIndexedPathsSet(systemMap);
|
|
139
|
+
// Get all docs files
|
|
140
|
+
const allDocs = await deps.glob('docs/**/*.md');
|
|
141
|
+
const orphans = [];
|
|
142
|
+
for (const docPath of allDocs) {
|
|
143
|
+
// Check if this doc is directly indexed
|
|
144
|
+
if (indexedPaths.has(docPath))
|
|
145
|
+
continue;
|
|
146
|
+
// Check if this doc falls under an indexed directory
|
|
147
|
+
let isUnderIndexedDir = false;
|
|
148
|
+
for (const indexedPath of indexedPaths) {
|
|
149
|
+
const indexedPathStr = String(indexedPath);
|
|
150
|
+
if (indexedPathStr.endsWith('/') && docPath.startsWith(indexedPathStr)) {
|
|
151
|
+
isUnderIndexedDir = true;
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (!isUnderIndexedDir) {
|
|
156
|
+
orphans.push(docPath);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return orphans;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Validate audience tags against canonical list
|
|
163
|
+
*
|
|
164
|
+
* @param {object} systemMap - Parsed SYSTEM-MAP.yaml
|
|
165
|
+
* @returns {string[]} Array of error messages
|
|
166
|
+
*/
|
|
167
|
+
export function validateAudienceTags(systemMap) {
|
|
168
|
+
const errors = [];
|
|
169
|
+
const entries = extractAllEntries(systemMap);
|
|
170
|
+
for (const entry of entries) {
|
|
171
|
+
if (!entry.audiences || !Array.isArray(entry.audiences)) {
|
|
172
|
+
errors.push(`Entry ${entry.id} missing audiences array`);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (entry.audiences.length === 0) {
|
|
176
|
+
errors.push(`Entry ${entry.id} has empty audiences array (must have at least one)`);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
for (const audience of entry.audiences) {
|
|
180
|
+
if (!CANONICAL_AUDIENCES.includes(audience)) {
|
|
181
|
+
errors.push(`Invalid audience '${audience}' in entry ${entry.id}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return errors;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Validate quick_queries reference valid document IDs
|
|
189
|
+
*
|
|
190
|
+
* @param {object} systemMap - Parsed SYSTEM-MAP.yaml
|
|
191
|
+
* @returns {string[]} Array of error messages
|
|
192
|
+
*/
|
|
193
|
+
export function validateQuickQueries(systemMap) {
|
|
194
|
+
const errors = [];
|
|
195
|
+
if (!systemMap.quick_queries) {
|
|
196
|
+
return errors;
|
|
197
|
+
}
|
|
198
|
+
const validIds = buildIdSet(systemMap);
|
|
199
|
+
for (const [queryKey, queryValue] of Object.entries(systemMap.quick_queries)) {
|
|
200
|
+
const query = queryValue;
|
|
201
|
+
// Check primary reference
|
|
202
|
+
if (query.primary && !validIds.has(query.primary)) {
|
|
203
|
+
errors.push(`Quick query '${queryKey}' references non-existent primary: ${query.primary}`);
|
|
204
|
+
}
|
|
205
|
+
// Check related references
|
|
206
|
+
if (query.related && Array.isArray(query.related)) {
|
|
207
|
+
for (const related of query.related) {
|
|
208
|
+
if (!validIds.has(related)) {
|
|
209
|
+
errors.push(`Quick query '${queryKey}' references non-existent related: ${related}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return errors;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Validate classification prevents PHI routing to investor/public
|
|
218
|
+
*
|
|
219
|
+
* Rule: restricted (PHI) data should NOT be accessible to external audiences
|
|
220
|
+
* - restricted = PHI data, must NOT go to investor/patient/clinician
|
|
221
|
+
* - confidential = sensitive but OK for investor (investor docs ARE confidential)
|
|
222
|
+
*
|
|
223
|
+
* @param {object} systemMap - Parsed SYSTEM-MAP.yaml
|
|
224
|
+
* @returns {string[]} Array of error messages
|
|
225
|
+
*/
|
|
226
|
+
export function validateClassificationRouting(systemMap) {
|
|
227
|
+
const errors = [];
|
|
228
|
+
const entries = extractAllEntries(systemMap);
|
|
229
|
+
for (const entry of entries) {
|
|
230
|
+
const classification = entry.classification;
|
|
231
|
+
const audiences = entry.audiences || [];
|
|
232
|
+
// Only check restricted classification (PHI data)
|
|
233
|
+
// Confidential is OK for investors (investor materials are confidential by design)
|
|
234
|
+
if (classification !== 'restricted') {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
// Check if any PHI-restricted audiences have access
|
|
238
|
+
for (const audience of audiences) {
|
|
239
|
+
if (PHI_RESTRICTED_AUDIENCES.includes(audience)) {
|
|
240
|
+
errors.push(`PHI routing violation: ${entry.id} has restricted (PHI) classification but is accessible to '${audience}'`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return errors;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Validate entire system map
|
|
248
|
+
*
|
|
249
|
+
* @param {object} systemMap - Parsed SYSTEM-MAP.yaml
|
|
250
|
+
* @param {{exists: (path: string) => boolean, glob: (pattern: string) => Promise<string[]>}} deps - Dependencies
|
|
251
|
+
* @returns {Promise<{valid: boolean, pathErrors: string[], orphanDocs: string[], audienceErrors: string[], queryErrors: string[], classificationErrors: string[]}>}
|
|
252
|
+
*/
|
|
253
|
+
export async function validateSystemMap(systemMap, deps) {
|
|
254
|
+
const pathErrors = await validatePaths(systemMap, deps);
|
|
255
|
+
const orphanDocs = await findOrphanDocs(systemMap, deps);
|
|
256
|
+
const audienceErrors = validateAudienceTags(systemMap);
|
|
257
|
+
const queryErrors = validateQuickQueries(systemMap);
|
|
258
|
+
const classificationErrors = validateClassificationRouting(systemMap);
|
|
259
|
+
const valid = pathErrors.length === 0 &&
|
|
260
|
+
orphanDocs.length === 0 &&
|
|
261
|
+
audienceErrors.length === 0 &&
|
|
262
|
+
queryErrors.length === 0 &&
|
|
263
|
+
classificationErrors.length === 0;
|
|
264
|
+
return {
|
|
265
|
+
valid,
|
|
266
|
+
pathErrors,
|
|
267
|
+
orphanDocs,
|
|
268
|
+
audienceErrors,
|
|
269
|
+
queryErrors,
|
|
270
|
+
classificationErrors,
|
|
271
|
+
};
|
|
272
|
+
}
|