mirror of
https://github.com/BradGroux/veritas-kanban.git
synced 2026-08-28 02:44:59 +00:00
build: enforce service filesystem boundary (#1190)
This commit is contained in:
parent
2a581a451a
commit
542d6dd760
7 changed files with 619 additions and 0 deletions
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
|
|
@ -289,6 +289,9 @@ jobs:
|
|||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Check service filesystem boundary
|
||||
run: pnpm check:service-filesystem-boundary
|
||||
|
||||
- name: Build shared (dependency for typecheck)
|
||||
run: pnpm --filter @veritas-kanban/shared build
|
||||
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ pnpm check:delivery-cadence # Prevents verification and review policy drift
|
|||
pnpm check:security-gates # Validates CodeQL/gitleaks workflow and exact suppressions
|
||||
pnpm check:gitleaks # Scans reviewed tree and proves new-secret detection
|
||||
pnpm check:vite-native-config # Loads web build and test configs with Vite's native loader
|
||||
pnpm check:service-filesystem-boundary # Prevents new direct filesystem imports in services
|
||||
pnpm test:ci-scope # Validates path-aware CI test selection
|
||||
pnpm smoke:cli-mcp # CLI ↔ MCP compatibility smoke test
|
||||
pnpm test:buzz:compatibility # Credential-free composed Buzz release gate
|
||||
|
|
|
|||
30
docs/architecture/SERVICE-FILESYSTEM-BOUNDARY.md
Normal file
30
docs/architecture/SERVICE-FILESYSTEM-BOUNDARY.md
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# Service Filesystem Boundary
|
||||
|
||||
Service modules must use the storage abstraction instead of importing Node's
|
||||
filesystem APIs directly. The current exceptions are tracked in
|
||||
[`service-filesystem-boundary.json`](service-filesystem-boundary.json) so the
|
||||
existing migration debt is explicit without allowing it to spread.
|
||||
|
||||
Run the boundary gate with:
|
||||
|
||||
```bash
|
||||
pnpm check:service-filesystem-boundary
|
||||
```
|
||||
|
||||
The gate recursively scans `server/src/services/**/*.ts` and recognizes static
|
||||
imports, dynamic imports, and `require()` calls for `fs`, `node:fs`, and their
|
||||
`/promises` variants. Filesystem text embedded in strings and comments is
|
||||
ignored. The command exits nonzero and names the file when it finds:
|
||||
|
||||
- a direct import without a classified inventory entry;
|
||||
- an invalid category, owner, or rationale;
|
||||
- a duplicate entry; or
|
||||
- a stale entry after an import has been removed.
|
||||
|
||||
`maximumEntries` must equal the number of classified exceptions. Any increase
|
||||
therefore requires a visible inventory and ratchet change in the same review.
|
||||
The remaining #1163 child issues own the reductions: #1186 covers coordination
|
||||
state, #1187 operational evidence, #1188 managed content, and #1189 final
|
||||
process I/O plus removal of the last compatibility exceptions. Each migration
|
||||
must delete its stale inventory entries and lower `maximumEntries` in the same
|
||||
change.
|
||||
324
docs/architecture/service-filesystem-boundary.json
Normal file
324
docs/architecture/service-filesystem-boundary.json
Normal file
|
|
@ -0,0 +1,324 @@
|
|||
{
|
||||
"schemaVersion": 1,
|
||||
"maximumEntries": 53,
|
||||
"entries": [
|
||||
{
|
||||
"path": "server/src/services/activity-service.ts",
|
||||
"category": "authoritative-persistence",
|
||||
"owner": "#1186",
|
||||
"rationale": "Coordination-state storage migration is tracked in issue #1186."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/agent-health-service.ts",
|
||||
"category": "authoritative-persistence",
|
||||
"owner": "#1187",
|
||||
"rationale": "Operational evidence storage migration is tracked in issue #1187."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/agent-permission-service.ts",
|
||||
"category": "authoritative-persistence",
|
||||
"owner": "#1186",
|
||||
"rationale": "Coordination-state storage migration is tracked in issue #1186."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/attachment-service.ts",
|
||||
"category": "compatibility-debt",
|
||||
"owner": "#1188",
|
||||
"rationale": "Managed-content storage migration is tracked in issue #1188."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/audit-service.ts",
|
||||
"category": "authoritative-persistence",
|
||||
"owner": "#1187",
|
||||
"rationale": "Operational evidence storage migration is tracked in issue #1187."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/broadcast-storage-service.ts",
|
||||
"category": "authoritative-persistence",
|
||||
"owner": "#1186",
|
||||
"rationale": "Coordination-state storage migration is tracked in issue #1186."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/ceremony-service.ts",
|
||||
"category": "authoritative-persistence",
|
||||
"owner": "#1186",
|
||||
"rationale": "Coordination-state storage migration is tracked in issue #1186."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/chat-service.ts",
|
||||
"category": "authoritative-persistence",
|
||||
"owner": "#1186",
|
||||
"rationale": "Coordination-state storage migration is tracked in issue #1186."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/clawdbot-agent-service.ts",
|
||||
"category": "transient-process-io",
|
||||
"owner": "#1189",
|
||||
"rationale": "Remaining process I/O migration is tracked in issue #1189."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/codex-health-service.ts",
|
||||
"category": "authoritative-persistence",
|
||||
"owner": "#1187",
|
||||
"rationale": "Operational evidence storage migration is tracked in issue #1187."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/config-service.ts",
|
||||
"category": "compatibility-debt",
|
||||
"owner": "#1188",
|
||||
"rationale": "Managed-content storage migration is tracked in issue #1188."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/conflict-service.ts",
|
||||
"category": "authoritative-persistence",
|
||||
"owner": "#1186",
|
||||
"rationale": "Coordination-state storage migration is tracked in issue #1186."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/delegation-service.ts",
|
||||
"category": "authoritative-persistence",
|
||||
"owner": "#1186",
|
||||
"rationale": "Coordination-state storage migration is tracked in issue #1186."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/doc-freshness-service.ts",
|
||||
"category": "packaged-readonly-content",
|
||||
"owner": "#1188",
|
||||
"rationale": "Managed-content storage migration is tracked in issue #1188."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/docs-service.ts",
|
||||
"category": "packaged-readonly-content",
|
||||
"owner": "#1188",
|
||||
"rationale": "Managed-content storage migration is tracked in issue #1188."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/error-learning-service.ts",
|
||||
"category": "authoritative-persistence",
|
||||
"owner": "#1186",
|
||||
"rationale": "Coordination-state storage migration is tracked in issue #1186."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/external-tracker-service.ts",
|
||||
"category": "authoritative-persistence",
|
||||
"owner": "#1187",
|
||||
"rationale": "Operational evidence storage migration is tracked in issue #1187."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/file-lock.ts",
|
||||
"category": "transient-process-io",
|
||||
"owner": "#1189",
|
||||
"rationale": "Remaining process I/O migration is tracked in issue #1189."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/gateway-chat-client.ts",
|
||||
"category": "transient-process-io",
|
||||
"owner": "#1189",
|
||||
"rationale": "Remaining process I/O migration is tracked in issue #1189."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/github-sync-service.ts",
|
||||
"category": "authoritative-persistence",
|
||||
"owner": "#1187",
|
||||
"rationale": "Operational evidence storage migration is tracked in issue #1187."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/integrity-service.ts",
|
||||
"category": "authoritative-persistence",
|
||||
"owner": "#1187",
|
||||
"rationale": "Operational evidence storage migration is tracked in issue #1187."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/lifecycle-hooks-service.ts",
|
||||
"category": "authoritative-persistence",
|
||||
"owner": "#1186",
|
||||
"rationale": "Coordination-state storage migration is tracked in issue #1186."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/maintenance-service.ts",
|
||||
"category": "authoritative-persistence",
|
||||
"owner": "#1187",
|
||||
"rationale": "Operational evidence storage migration is tracked in issue #1187."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/managed-list-service.ts",
|
||||
"category": "compatibility-debt",
|
||||
"owner": "#1188",
|
||||
"rationale": "Managed-content storage migration is tracked in issue #1188."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/metrics/task-metrics.ts",
|
||||
"category": "authoritative-persistence",
|
||||
"owner": "#1187",
|
||||
"rationale": "Operational evidence storage migration is tracked in issue #1187."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/metrics/telemetry-reader.ts",
|
||||
"category": "authoritative-persistence",
|
||||
"owner": "#1187",
|
||||
"rationale": "Operational evidence storage migration is tracked in issue #1187."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/notification-service.ts",
|
||||
"category": "authoritative-persistence",
|
||||
"owner": "#1187",
|
||||
"rationale": "Operational evidence storage migration is tracked in issue #1187."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/outbound-integration-service.ts",
|
||||
"category": "authoritative-persistence",
|
||||
"owner": "#1187",
|
||||
"rationale": "Operational evidence storage migration is tracked in issue #1187."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/pdf-report-service.ts",
|
||||
"category": "transient-process-io",
|
||||
"owner": "#1189",
|
||||
"rationale": "Remaining process I/O migration is tracked in issue #1189."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/progress-service.ts",
|
||||
"category": "authoritative-persistence",
|
||||
"owner": "#1186",
|
||||
"rationale": "Coordination-state storage migration is tracked in issue #1186."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/prompt-registry-service.ts",
|
||||
"category": "compatibility-debt",
|
||||
"owner": "#1188",
|
||||
"rationale": "Managed-content storage migration is tracked in issue #1188."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/queue-intake-monitor-service.ts",
|
||||
"category": "authoritative-persistence",
|
||||
"owner": "#1187",
|
||||
"rationale": "Operational evidence storage migration is tracked in issue #1187."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/reflection-service.ts",
|
||||
"category": "authoritative-persistence",
|
||||
"owner": "#1186",
|
||||
"rationale": "Coordination-state storage migration is tracked in issue #1186."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/run-session-share-service.ts",
|
||||
"category": "authoritative-persistence",
|
||||
"owner": "#1187",
|
||||
"rationale": "Operational evidence storage migration is tracked in issue #1187."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/scheduled-deliverables-service.ts",
|
||||
"category": "authoritative-persistence",
|
||||
"owner": "#1186",
|
||||
"rationale": "Coordination-state storage migration is tracked in issue #1186."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/scheduler-service.ts",
|
||||
"category": "authoritative-persistence",
|
||||
"owner": "#1186",
|
||||
"rationale": "Coordination-state storage migration is tracked in issue #1186."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/search-service.ts",
|
||||
"category": "authoritative-persistence",
|
||||
"owner": "#1187",
|
||||
"rationale": "Operational evidence storage migration is tracked in issue #1187."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/shared-resources-service.ts",
|
||||
"category": "compatibility-debt",
|
||||
"owner": "#1188",
|
||||
"rationale": "Managed-content storage migration is tracked in issue #1188."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/skill-security-service.ts",
|
||||
"category": "packaged-readonly-content",
|
||||
"owner": "#1188",
|
||||
"rationale": "Managed-content storage migration is tracked in issue #1188."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/sqlite-portability-service.ts",
|
||||
"category": "transient-process-io",
|
||||
"owner": "#1189",
|
||||
"rationale": "Remaining process I/O migration is tracked in issue #1189."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/status-history-service.ts",
|
||||
"category": "authoritative-persistence",
|
||||
"owner": "#1186",
|
||||
"rationale": "Coordination-state storage migration is tracked in issue #1186."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/task-identity-diagnostics.ts",
|
||||
"category": "authoritative-persistence",
|
||||
"owner": "#1187",
|
||||
"rationale": "Operational evidence storage migration is tracked in issue #1187."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/task-service.ts",
|
||||
"category": "authoritative-persistence",
|
||||
"owner": "#1186",
|
||||
"rationale": "Coordination-state storage migration is tracked in issue #1186."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/telemetry-service.ts",
|
||||
"category": "authoritative-persistence",
|
||||
"owner": "#1187",
|
||||
"rationale": "Operational evidence storage migration is tracked in issue #1187."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/template-service.ts",
|
||||
"category": "compatibility-debt",
|
||||
"owner": "#1188",
|
||||
"rationale": "Managed-content storage migration is tracked in issue #1188."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/text-extraction-service.ts",
|
||||
"category": "transient-process-io",
|
||||
"owner": "#1189",
|
||||
"rationale": "Remaining process I/O migration is tracked in issue #1189."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/tool-policy-service.ts",
|
||||
"category": "compatibility-debt",
|
||||
"owner": "#1188",
|
||||
"rationale": "Managed-content storage migration is tracked in issue #1188."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/trace-service.ts",
|
||||
"category": "authoritative-persistence",
|
||||
"owner": "#1187",
|
||||
"rationale": "Operational evidence storage migration is tracked in issue #1187."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/transition-hooks-service.ts",
|
||||
"category": "authoritative-persistence",
|
||||
"owner": "#1186",
|
||||
"rationale": "Coordination-state storage migration is tracked in issue #1186."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/work-product-service.ts",
|
||||
"category": "authoritative-persistence",
|
||||
"owner": "#1187",
|
||||
"rationale": "Operational evidence storage migration is tracked in issue #1187."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/workflow-run-service.ts",
|
||||
"category": "authoritative-persistence",
|
||||
"owner": "#1186",
|
||||
"rationale": "Coordination-state storage migration is tracked in issue #1186."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/workflow-service.ts",
|
||||
"category": "authoritative-persistence",
|
||||
"owner": "#1186",
|
||||
"rationale": "Coordination-state storage migration is tracked in issue #1186."
|
||||
},
|
||||
{
|
||||
"path": "server/src/services/workflow-step-executor.ts",
|
||||
"category": "authoritative-persistence",
|
||||
"owner": "#1186",
|
||||
"rationale": "Coordination-state storage migration is tracked in issue #1186."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -41,6 +41,7 @@
|
|||
"check:vite-native-config": "pnpm --filter @veritas-kanban/web exec vite build --configLoader native && vitest run --configLoader native web/src/lib/__tests__/client-policy.test.ts",
|
||||
"check:pnpm-settings": "node scripts/check-pnpm-settings.mjs",
|
||||
"check:security-artifacts": "node scripts/check-security-artifacts.mjs",
|
||||
"check:service-filesystem-boundary": "node --test scripts/check-service-filesystem-boundary.test.mjs && node scripts/check-service-filesystem-boundary.mjs",
|
||||
"typecheck": "pnpm --filter @veritas-kanban/shared build && pnpm -r typecheck",
|
||||
"test": "pnpm test:unit",
|
||||
"test:unit": "node --test scripts/run-workspace-unit-tests.test.mjs && node scripts/run-workspace-unit-tests.mjs",
|
||||
|
|
|
|||
156
scripts/check-service-filesystem-boundary.mjs
Normal file
156
scripts/check-service-filesystem-boundary.mjs
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
#!/usr/bin/env node
|
||||
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import ts from 'typescript';
|
||||
|
||||
const ALLOWED_CATEGORIES = new Set([
|
||||
'authoritative-persistence',
|
||||
'packaged-readonly-content',
|
||||
'transient-process-io',
|
||||
'compatibility-debt',
|
||||
]);
|
||||
const FILESYSTEM_MODULE = /^(?:node:)?fs(?:\/promises)?$/;
|
||||
|
||||
export function findDirectFilesystemImports(source) {
|
||||
const sourceFile = ts.createSourceFile(
|
||||
'service.ts',
|
||||
source,
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
ts.ScriptKind.TS
|
||||
);
|
||||
const imports = [];
|
||||
const addImport = (moduleSpecifier) => {
|
||||
if (moduleSpecifier && ts.isStringLiteralLike(moduleSpecifier)) {
|
||||
const module = moduleSpecifier.text;
|
||||
if (FILESYSTEM_MODULE.test(module)) {
|
||||
const line = sourceFile.getLineAndCharacterOfPosition(moduleSpecifier.getStart()).line + 1;
|
||||
imports.push({ module, line });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const visit = (node) => {
|
||||
if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {
|
||||
addImport(node.moduleSpecifier);
|
||||
} else if (ts.isImportEqualsDeclaration(node)) {
|
||||
if (ts.isExternalModuleReference(node.moduleReference)) {
|
||||
addImport(node.moduleReference.expression);
|
||||
}
|
||||
} else if (ts.isCallExpression(node)) {
|
||||
const isDynamicImport = node.expression.kind === ts.SyntaxKind.ImportKeyword;
|
||||
const isRequire = ts.isIdentifier(node.expression) && node.expression.text === 'require';
|
||||
if (isDynamicImport || isRequire) addImport(node.arguments[0]);
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
visit(sourceFile);
|
||||
|
||||
return imports;
|
||||
}
|
||||
|
||||
function collectTypeScriptFiles(directory) {
|
||||
if (!existsSync(directory)) return [];
|
||||
const files = [];
|
||||
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
||||
const absolutePath = path.join(directory, entry.name);
|
||||
if (entry.isDirectory()) files.push(...collectTypeScriptFiles(absolutePath));
|
||||
else if (entry.name.endsWith('.ts')) files.push(absolutePath);
|
||||
}
|
||||
return files.sort();
|
||||
}
|
||||
|
||||
export function validateServiceFilesystemBoundary({ root, inventory }) {
|
||||
const violations = [];
|
||||
if (inventory.schemaVersion !== 1) violations.push('inventory schemaVersion must equal 1');
|
||||
if (!Array.isArray(inventory.entries)) violations.push('inventory entries must be an array');
|
||||
if (violations.length > 0) return violations;
|
||||
|
||||
if (inventory.maximumEntries !== inventory.entries.length) {
|
||||
violations.push('maximumEntries must equal the classified entry count');
|
||||
}
|
||||
|
||||
const classified = new Map();
|
||||
for (const entry of inventory.entries) {
|
||||
if (!entry || typeof entry.path !== 'string') {
|
||||
violations.push('every inventory entry must have a path');
|
||||
continue;
|
||||
}
|
||||
if (classified.has(entry.path)) violations.push(`${entry.path}: duplicate inventory entry`);
|
||||
classified.set(entry.path, entry);
|
||||
if (!ALLOWED_CATEGORIES.has(entry.category)) {
|
||||
violations.push(`${entry.path}: invalid category ${JSON.stringify(entry.category)}`);
|
||||
}
|
||||
if (!/^#\d+$/.test(entry.owner ?? '')) {
|
||||
violations.push(`${entry.path}: owner must be a GitHub issue reference`);
|
||||
}
|
||||
if (typeof entry.rationale !== 'string' || entry.rationale.trim().length < 20) {
|
||||
violations.push(`${entry.path}: rationale must explain the temporary exception`);
|
||||
}
|
||||
}
|
||||
|
||||
const detected = new Set();
|
||||
const serviceDirectory = path.join(root, 'server/src/services');
|
||||
for (const absolutePath of collectTypeScriptFiles(serviceDirectory)) {
|
||||
const relativePath = path.relative(root, absolutePath).replaceAll('\\', '/');
|
||||
const imports = findDirectFilesystemImports(readFileSync(absolutePath, 'utf8'));
|
||||
if (imports.length === 0) continue;
|
||||
detected.add(relativePath);
|
||||
if (!classified.has(relativePath)) {
|
||||
violations.push(
|
||||
`${relativePath}:${imports[0].line}: unclassified direct filesystem import (${imports.map((item) => item.module).join(', ')})`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const classifiedPath of classified.keys()) {
|
||||
if (!detected.has(classifiedPath)) {
|
||||
violations.push(`${classifiedPath}: stale inventory entry has no direct filesystem import`);
|
||||
}
|
||||
}
|
||||
return violations.sort();
|
||||
}
|
||||
|
||||
function parseArguments(argv) {
|
||||
const options = {
|
||||
root: process.cwd(),
|
||||
inventory: 'docs/architecture/service-filesystem-boundary.json',
|
||||
};
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
if (argv[index] === '--root') options.root = path.resolve(argv[++index]);
|
||||
else if (argv[index] === '--inventory') options.inventory = argv[++index];
|
||||
else throw new Error(`Unknown argument: ${argv[index]}`);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
export function runServiceFilesystemBoundaryCheck(argv = process.argv.slice(2)) {
|
||||
let options;
|
||||
try {
|
||||
options = parseArguments(argv);
|
||||
const inventoryPath = path.resolve(options.root, options.inventory);
|
||||
const inventory = JSON.parse(readFileSync(inventoryPath, 'utf8'));
|
||||
const violations = validateServiceFilesystemBoundary({ root: options.root, inventory });
|
||||
if (violations.length > 0) {
|
||||
console.error('Service filesystem boundary check failed.');
|
||||
for (const violation of violations) console.error(`- ${violation}`);
|
||||
process.exitCode = 1;
|
||||
return false;
|
||||
}
|
||||
console.log(
|
||||
`Service filesystem boundary check passed (${inventory.entries.length} classified exceptions).`
|
||||
);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`Service filesystem boundary check failed: ${error.message}`);
|
||||
process.exitCode = 1;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isDirectExecution() {
|
||||
return process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
||||
}
|
||||
|
||||
if (isDirectExecution()) runServiceFilesystemBoundaryCheck();
|
||||
104
scripts/check-service-filesystem-boundary.test.mjs
Normal file
104
scripts/check-service-filesystem-boundary.test.mjs
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import test from 'node:test';
|
||||
|
||||
import { findDirectFilesystemImports } from './check-service-filesystem-boundary.mjs';
|
||||
|
||||
const CHECK_SCRIPT = path.resolve('scripts/check-service-filesystem-boundary.mjs');
|
||||
|
||||
function createFixture({ source, entries = [], servicePath = 'example-service.ts' }) {
|
||||
const root = mkdtempSync(path.join(tmpdir(), 'veritas-service-fs-boundary-'));
|
||||
const serviceDirectory = path.join(root, 'server/src/services');
|
||||
const sourcePath = path.join(serviceDirectory, servicePath);
|
||||
mkdirSync(path.dirname(sourcePath), { recursive: true });
|
||||
writeFileSync(sourcePath, source);
|
||||
writeFileSync(
|
||||
path.join(root, 'service-filesystem-boundary.json'),
|
||||
`${JSON.stringify({ schemaVersion: 1, maximumEntries: entries.length, entries }, null, 2)}\n`
|
||||
);
|
||||
return root;
|
||||
}
|
||||
|
||||
function runCheck(root) {
|
||||
return spawnSync(
|
||||
process.execPath,
|
||||
[CHECK_SCRIPT, '--root', root, '--inventory', 'service-filesystem-boundary.json'],
|
||||
{ encoding: 'utf8' }
|
||||
);
|
||||
}
|
||||
|
||||
test('rejects a new service filesystem import with a file diagnostic', () => {
|
||||
const root = createFixture({
|
||||
source: "import { readFile } from 'node:fs/promises';\n",
|
||||
});
|
||||
|
||||
const result = runCheck(root);
|
||||
|
||||
assert.equal(result.status, 1);
|
||||
assert.match(result.stderr, /server\/src\/services\/example-service\.ts/);
|
||||
assert.match(result.stderr, /unclassified direct filesystem import/);
|
||||
});
|
||||
|
||||
test('scans nested service directories', () => {
|
||||
const root = createFixture({
|
||||
source: "const fs = require('fs');\n",
|
||||
servicePath: 'nested/example-service.ts',
|
||||
});
|
||||
|
||||
const result = runCheck(root);
|
||||
|
||||
assert.equal(result.status, 1);
|
||||
assert.match(result.stderr, /server\/src\/services\/nested\/example-service\.ts/);
|
||||
});
|
||||
|
||||
test('ignores filesystem calls embedded in strings and comments', () => {
|
||||
const source = [
|
||||
`const command = "require('node:fs').writeFileSync('/tmp/example', 'x')";`,
|
||||
`// import fs from 'node:fs';`,
|
||||
`/* require('fs/promises') */`,
|
||||
].join('\n');
|
||||
|
||||
assert.deepEqual(findDirectFilesystemImports(source), []);
|
||||
});
|
||||
|
||||
test('detects filesystem re-exports', () => {
|
||||
assert.deepEqual(findDirectFilesystemImports("export { readFile } from 'node:fs';\n"), [
|
||||
{ module: 'node:fs', line: 1 },
|
||||
]);
|
||||
});
|
||||
|
||||
test('accepts a classified direct filesystem import', () => {
|
||||
const entry = {
|
||||
path: 'server/src/services/example-service.ts',
|
||||
category: 'compatibility-debt',
|
||||
owner: '#1189',
|
||||
rationale: 'Migration is tracked by the final storage boundary issue.',
|
||||
};
|
||||
const root = createFixture({
|
||||
source: "const fs = await import('fs/promises');\n",
|
||||
entries: [entry],
|
||||
});
|
||||
|
||||
const result = runCheck(root);
|
||||
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.match(result.stdout, /1 classified exception/);
|
||||
});
|
||||
|
||||
test('rejects stale inventory entries so the exception count ratchets downward', () => {
|
||||
const entry = {
|
||||
path: 'server/src/services/example-service.ts',
|
||||
category: 'compatibility-debt',
|
||||
owner: '#1189',
|
||||
rationale: 'Migration is tracked by the final storage boundary issue.',
|
||||
};
|
||||
const root = createFixture({ source: 'export const value = 1;\n', entries: [entry] });
|
||||
|
||||
const result = runCheck(root);
|
||||
|
||||
assert.equal(result.status, 1);
|
||||
assert.match(result.stderr, /stale inventory entry/);
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue