diff --git a/.release-please-manifest.json b/.release-please-manifest.json index d83f74a7..04cd7b2d 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -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" } diff --git a/release-please-config.json b/release-please-config.json index 90301ac5..3d5214e1 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -5,7 +5,12 @@ "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", @@ -13,6 +18,12 @@ "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", diff --git a/scripts/check-layer-boundaries.sh b/scripts/check-layer-boundaries.sh index 7121ce93..78fd2ccf 100755 --- a/scripts/check-layer-boundaries.sh +++ b/scripts/check-layer-boundaries.sh @@ -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 @@ -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%%/*}" } @@ -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 diff --git a/tests/layerBoundaries.test.ts b/tests/layerBoundaries.test.ts new file mode 100644 index 00000000..20853434 --- /dev/null +++ b/tests/layerBoundaries.test.ts @@ -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 `/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); + }); +});