Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
".": "18.66.1",
"packages/brepjs-opencascade": "0.16.0",
"packages/brepjs-voxel-wasm": "0.1.0",
"packages/brepjs-verify": "0.4.0"
}
13 changes: 12 additions & 1 deletion release-please-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,25 @@
"release-type": "node",
"changelog-path": "CHANGELOG.md",
"bump-minor-pre-major": true,
"exclude-paths": ["apps", "packages/brepjs-verify", "packages/brepjs-viewer"]
"exclude-paths": [
"apps",
"packages/brepjs-verify",
"packages/brepjs-viewer",
"packages/brepjs-voxel"
]
},
"packages/brepjs-opencascade": {
"release-type": "node",
"changelog-path": "CHANGELOG.md",
"component": "brepjs-opencascade",
"bump-minor-pre-major": true
},
"packages/brepjs-voxel-wasm": {
"release-type": "node",
"changelog-path": "CHANGELOG.md",
"component": "brepjs-voxel-wasm",
"bump-minor-pre-major": true
},
"packages/brepjs-verify": {
"release-type": "node",
"changelog-path": "CHANGELOG.md",
Expand Down
10 changes: 6 additions & 4 deletions scripts/check-layer-boundaries.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ if [[ "${1:-}" == "--staged" ]]; then
STAGED_ONLY=true
fi

SRC_DIR="src"
# Scan root is overridable (default "src") so the enforcement itself can be
# exercised against throwaway fixtures without touching the real tree.
SRC_DIR="${BOUNDARY_SRC_DIR:-src}"
ERRORS=()

# Map directory to layer number
Expand All @@ -34,8 +36,8 @@ get_layer() {
# Get top-level src directory from a file path
get_src_dir() {
local filepath="$1"
# Strip src/ prefix and get first directory component
local relative="${filepath#src/}"
# Strip the scan-root prefix and get first directory component
local relative="${filepath#"$SRC_DIR"/}"
echo "${relative%%/*}"
}

Expand Down Expand Up @@ -69,7 +71,7 @@ resolve_import_dir() {
local combined="$source_dir/$import_path"
# Normalize path
resolved=$(python3 -c "import os.path; print(os.path.normpath('$combined'))" 2>/dev/null || echo "")
resolved="${resolved#src/}"
resolved="${resolved#"$SRC_DIR"/}"
fi

# Get the top-level directory
Expand Down
82 changes: 82 additions & 0 deletions tests/layerBoundaries.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* Negative tests proving the layer-boundary script actually rejects upward
* imports (ADR-0013 §9 — "add negative tests proving enforcement is live").
*
* `scripts/check-layer-boundaries.sh` only emits a friendly "passed" line; a
* silent escape (e.g. a layer dir missing from `get_layer`, falling through to
* -1) would pass unnoticed. These tests run the real script against throwaway
* fixtures via the `BOUNDARY_SRC_DIR` override, so nothing touches `src/`, and
* assert it fails on forbidden imports and passes on allowed ones.
*
* Focus is the layers wired in for the voxel domain — voxel (L2) and lattice
* (L3) — but the harness covers the general rule.
*/

import { spawnSync } from 'node:child_process';
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { afterEach, describe, expect, it } from 'vitest';

const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const script = join(repoRoot, 'scripts', 'check-layer-boundaries.sh');

const tmpDirs: string[] = [];

afterEach(() => {
for (const dir of tmpDirs.splice(0)) rmSync(dir, { recursive: true, force: true });
});

/**
* Write a single fixture `<layerDir>/fixture.ts` importing `importPath` into a
* throwaway scan root, run the boundary checker against it, and return the exit
* status (0 = passed, non-zero = violation reported).
*/
function checkImport(layerDir: string, importPath: string): { status: number; out: string } {
const root = mkdtempSync(join(tmpdir(), 'brepjs-boundary-'));
tmpDirs.push(root);
mkdirSync(join(root, layerDir), { recursive: true });
writeFileSync(join(root, layerDir, 'fixture.ts'), `import { thing } from '${importPath}';\n`);

const result = spawnSync('bash', [script], {
cwd: repoRoot,
env: { ...process.env, BOUNDARY_SRC_DIR: root },
encoding: 'utf8',
});
return { status: result.status ?? -1, out: `${result.stdout}${result.stderr}` };
}

describe('layer boundaries: forbidden upward imports are rejected', () => {
it('voxel (L2) → lattice (L3) is a violation', () => {
const { status, out } = checkImport('voxel', '@/lattice/index.js');
expect(status).not.toBe(0);
expect(out).toMatch(/VIOLATION/);
});

it('voxel (L2) → sketching (L3) is a violation', () => {
expect(checkImport('voxel', '@/sketching/index.js').status).not.toBe(0);
});

it('core (L1) → voxel (L2) is a violation (voxel sits above core)', () => {
expect(checkImport('core', '@/voxel/index.js').status).not.toBe(0);
});

it('kernel (L0) → lattice (L3) is a violation (lattice is top-layer)', () => {
expect(checkImport('kernel', '@/lattice/index.js').status).not.toBe(0);
});
});

describe('layer boundaries: allowed downward / same-layer imports pass', () => {
it('lattice (L3) → voxel (L2) is allowed', () => {
expect(checkImport('lattice', '@/voxel/index.js').status).toBe(0);
});

it('voxel (L2) → topology (L2) is allowed (same layer)', () => {
expect(checkImport('voxel', '@/topology/index.js').status).toBe(0);
});

it('voxel (L2) → core (L1) is allowed (downward)', () => {
expect(checkImport('voxel', '@/core/result.js').status).toBe(0);
});
});
Loading