chore: enforce scoped formatting and browser package boundaries

This commit is contained in:
Sameh Khamis committed 2026-09-11 16:06:35 -07:00
1 parent ef69f01cd8
commit 76ddc6a9ef
15 files changed
+547 -22

No files matched your search

+12
View File
@@ -42,6 +42,12 @@ jobs:
- name: Run setup policy checks - name: Run setup policy checks
run: npm run doctor -- --json run: npm run doctor -- --json
- name: Check adopted formatting
run: npm run format:check
- name: Check package boundaries
run: npm run check:boundaries
- name: Run unit tests - name: Run unit tests
run: npm test run: npm test
@@ -70,6 +76,12 @@ jobs:
- name: Run the Pinokio install path - name: Run the Pinokio install path
run: node scripts/pinokio-install.mjs run: node scripts/pinokio-install.mjs
- name: Check adopted formatting
run: npm run format:check
- name: Check package boundaries
run: npm run check:boundaries
- name: Run focused onboarding tests - name: Run focused onboarding tests
run: node --test src/setupDoctor.test.mjs src/pinokioEnvironment.test.mjs src/pinokioPreflight.test.mjs src/pinokioLauncherContract.test.mjs src/mapStartup.test.mjs src/hudSummaryResponse.test.mjs run: node --test src/setupDoctor.test.mjs src/pinokioEnvironment.test.mjs src/pinokioPreflight.test.mjs src/pinokioLauncherContract.test.mjs src/mapStartup.test.mjs src/hudSummaryResponse.test.mjs
+9 -1
View File
@@ -11,4 +11,12 @@ pinokio/.installed
src/data/local_data/ src/data/local_data/
*.geojson *.geojson
*.geojsonl *.geojsonl
*.html
# Local configuration, generated assets and browser evidence
.env
.env.*
!.env.example
node_modules
.vite
.vite-*
*.min.js
+3 -1
View File
@@ -1,4 +1,6 @@
{ {
"singleQuote": true, "singleQuote": true,
"tabWidth": 2 "tabWidth": 2,
"semi": true,
"endOfLine": "lf"
} }
+7
View File
@@ -13,6 +13,13 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md
## [Unreleased] ## [Unreleased]
### Development
- Adopt Prettier tooling contributed by RohanDaCoder (#227), with an explicit
file scope, pinned formatter and Linux/Windows CI checks. Format the reusable
infrastructure modules and their consumer tests. Package boundary checks keep
those exports separate from app startup and local Node services.
### Fixed ### Fixed
- Separate optional Google server credentials for Places and Street View from - Separate optional Google server credentials for Places and Street View from
+16
View File
@@ -51,6 +51,22 @@ The highest-leverage places to jump in:
- Match the surrounding code — comment density, naming, and idiom. - Match the surrounding code — comment density, naming, and idiom.
- Prefer small, reviewable commits. Conventional-commit-style prefixes (`feat:`, `fix:`, `perf:`, `docs:`) are appreciated but not required. - Prefer small, reviewable commits. Conventional-commit-style prefixes (`feat:`, `fix:`, `perf:`, `docs:`) are appreciated but not required.
## Formatting and reusable components
Run `npm run format` before submitting changes to adopted modules, then
`npm run format:check` and `npm run check:boundaries`. Formatting uses the
explicit file list in `scripts/format-scope.json`; add newly extracted modules
and tests there in a separate mechanical commit. Keep unadopted files consistent
with their surrounding style. CI checks all adopted files and package boundaries
on Linux and Windows.
Reusable package exports own their state and receive application operations
through explicit callbacks. They must not import the standalone bootstrap or
local Node services. Update `scripts/package-boundaries.json` and consumer tests
when adding an export or expanding a component. See
[formatting and component boundaries](docs/CODE-BOUNDARIES.md) for the current
ownership and adoption process.
## Pull requests ## Pull requests
1. Branch off `main`. 1. Branch off `main`.
+50
View File
@@ -0,0 +1,50 @@
# Formatting and component boundaries
Run `npm run format` to format the files in `scripts/format-scope.json` and
`npm run format:check` to check that same list without writing. CI checks the
entire adopted list on Linux and Windows. Prettier is pinned in the development
dependencies; use the installed version so local and CI output agree. The shared
configuration specifies two spaces, single quotes, semicolons and LF endings.
Add new reusable modules and their tests to the list as they are extracted.
Keep mechanical formatting in its own commit after behavior is stable. Existing
source-text regression assertions still apply; investigate failures and preserve
their behavioral coverage when a move or line wrap changes a tested shape.
Files outside the list retain their surrounding style until deliberately adopted.
Generated output, local configuration, browser evidence and bundled datasets are
excluded. The formatter validates every entry before writing any file.
## Current component ownership
| Surface | Owns | Receives from its caller |
| -------------------------------------- | ---------------------------------------------------------------------- | --------------------------------------------------- |
| `gods-eye-view/infrastructure` | Datacenter/dam definitions and fresh layer construction | Context, overlay and render operations |
| `gods-eye-view/infrastructure/geojson` | Data loading, Cesium entities, selection handling and resource cleanup | A viewer and those same operations |
| `gods-eye-view/infrastructure/lod` | Pure visibility budgets and selection policy | Position/visibility records and camera measurements |
| `src/data/localGeojson.js` | Standalone compatibility wiring | The application's existing shared services |
| `src/main.js` and `vite.config.js` | Standalone startup and local Node services | Local configuration |
The three scoped package exports are browser source modules. Use their documented
exports instead of importing standalone startup or reaching into internal files.
The application owns the viewer, context store, overlay host and render scheduler;
layers use the supplied callbacks. See [the infrastructure contract](INFRASTRUCTURE-LAYERS.md).
`npm run check:boundaries` builds every declared package export, with app Vite
configuration disabled. `scripts/package-boundaries.json` lists each export's
component, owned modules and external runtime dependencies. A new export must be
classified. Imports outside the declared modules fail, including unused and
literal dynamic imports. Cesium stays external so the consuming application
supplies the same compatible instance as its viewer. Existing consumer tests
also check import-time inactivity and asset URLs under a non-root base.
These checks cover the declared exports, not every import in the application.
They check build-time imports, not arbitrary runtime-generated module URLs.
Keep runtime module discovery out of these exports. When extracting another
component, add its ownership and consumer tests together. Node services must use
separate entry points and their own checks when they become reusable; importing
them into a browser component is not supported. No Node service is exported yet.
This setup establishes boundaries for the existing infrastructure component.
Application construction, UI panels and individual source adapters remain future
extractions; they should become smaller modules with explicit lifecycle owners
as their callers migrate.
+9
View File
@@ -1,5 +1,14 @@
# God's Eye View Current State # God's Eye View Current State
## Scoped formatting and package checks
`npm run format` and `npm run format:check` operate on the explicit adopted-file
list. `npm run check:boundaries` checks the browser dependency graph of all
current package exports; infrastructure owns its three implementation modules
and takes Cesium from the consumer. CI runs both checks on Linux and Windows.
The standalone app, layer behavior and public export paths remain unchanged.
See [component ownership and adoption](CODE-BOUNDARIES.md).
## Google browser and server keys ## Google browser and server keys
Local Places nearby/text search and the CCTV Street View fallback prefer Local Places nearby/text search and the CCTV Street View fallback prefer
+1 -13
View File
@@ -17,9 +17,7 @@
"satellite.js": "^6.0.2" "satellite.js": "^6.0.2"
}, },
"devDependencies": { "devDependencies": {
"prettier": "^3.9.6", "prettier": "3.9.6",
"puppeteer": "^24.37.5",
"sharp": "^0.34.5",
"puppeteer": "^25.10.0", "puppeteer": "^25.10.0",
"sharp": "^0.35.4", "sharp": "^0.35.4",
"vite": "^6.0.0", "vite": "^6.0.0",
@@ -2239,16 +2237,6 @@
"url": "https://github.com/prettier/prettier?sponsor=1" "url": "https://github.com/prettier/prettier?sponsor=1"
} }
}, },
"node_modules/progress": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/protobufjs": { "node_modules/protobufjs": {
"version": "8.8.0", "version": "8.8.0",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.8.0.tgz", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.8.0.tgz",
+6 -7
View File
@@ -2,7 +2,7 @@
"name": "gods-eye-view", "name": "gods-eye-view",
"private": true, "private": true,
"version": "0.1.1", "version": "0.1.1",
"description": "A real-time intelligence console for planet Earth — photorealistic 3D globe, live aircraft/ships/satellites/earthquakes/CCTV, and hands-free voice control. Runs in a browser.", "description": "A real-time intelligence console for planet Earth \u2014 photorealistic 3D globe, live aircraft/ships/satellites/earthquakes/CCTV, and hands-free voice control. Runs in a browser.",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"author": "Bilawal Sidhu", "author": "Bilawal Sidhu",
@@ -33,8 +33,8 @@
], ],
"scripts": { "scripts": {
"doctor": "node scripts/setup-doctor.mjs", "doctor": "node scripts/setup-doctor.mjs",
"format": "prettier --write .", "format": "node scripts/format.mjs --write",
"format:check": "prettier --check .", "format:check": "node scripts/format.mjs --check",
"dev": "vite", "dev": "vite",
"dev:secure": "./scripts/dev-secure.sh", "dev:secure": "./scripts/dev-secure.sh",
"opensky:import": "./scripts/opensky-import-client.sh", "opensky:import": "./scripts/opensky-import-client.sh",
@@ -42,7 +42,8 @@
"preview": "vite preview", "preview": "vite preview",
"test": "node scripts/run-unit-tests.mjs", "test": "node scripts/run-unit-tests.mjs",
"test:track": "node scripts/track-regression.mjs", "test:track": "node scripts/track-regression.mjs",
"qa:map-source-tray": "node scripts/qa-map-source-tray.mjs" "qa:map-source-tray": "node scripts/qa-map-source-tray.mjs",
"check:boundaries": "node scripts/check-package-boundaries.mjs"
}, },
"dependencies": { "dependencies": {
"@mapbox/vector-tile": "^3.0.0", "@mapbox/vector-tile": "^3.0.0",
@@ -53,9 +54,7 @@
"satellite.js": "^6.0.2" "satellite.js": "^6.0.2"
}, },
"devDependencies": { "devDependencies": {
"prettier": "^3.9.6", "prettier": "3.9.6",
"puppeteer": "^24.37.5",
"sharp": "^0.34.5",
"puppeteer": "^25.10.0", "puppeteer": "^25.10.0",
"sharp": "^0.35.4", "sharp": "^0.35.4",
"vite": "^6.0.0", "vite": "^6.0.0",
+131
View File
@@ -0,0 +1,131 @@
import { readFile, realpath } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { build, normalizePath } from 'vite';
/** Build each browser export group and reject imports outside its declared ownership. */
export async function checkPackageBoundaries(root) {
root = await realpath(root);
const readJson = async (name) =>
JSON.parse(await readFile(path.join(root, name), 'utf8'));
const pkg = await readJson('package.json');
const groups = await readJson('scripts/package-boundaries.json');
const declaredExports = Object.keys(pkg.exports || {}).sort();
const classifiedExports = Object.values(groups)
.flatMap((group) => group.exports)
.sort();
if (
!declaredExports.length ||
JSON.stringify(declaredExports) !== JSON.stringify(classifiedExports)
) {
throw new Error(
'Every package export must belong to exactly one boundary group',
);
}
const reports = [];
for (const [name, group] of Object.entries(groups)) {
if (
!Array.isArray(group.modules) ||
!group.modules.length ||
!Array.isArray(group.external)
) {
throw new Error(`Invalid package boundary: ${name}`);
}
const allowed = new Set();
for (const module of group.modules) {
if (
typeof module !== 'string' ||
path.isAbsolute(module) ||
module.includes('\\') ||
module.split('/').includes('..')
) {
throw new Error(`Boundary modules must be repository paths: ${name}`);
}
const resolved = await realpath(path.join(root, module));
const relative = path.relative(root, resolved);
if (
relative === '..' ||
relative.startsWith(`..${path.sep}`) ||
path.isAbsolute(relative)
) {
throw new Error(`Boundary module escapes repository: ${name}`);
}
allowed.add(normalizePath(resolved));
}
for (const external of group.external) {
if (
!Object.hasOwn(pkg.dependencies || {}, external) &&
!Object.hasOwn(pkg.peerDependencies || {}, external)
) {
throw new Error(
`Boundary external must be a declared runtime dependency: ${external}`,
);
}
}
const input = group.exports.map((key) => {
const target = pkg.exports[key];
if (
typeof target !== 'string' ||
!target.startsWith('./') ||
!allowed.has(normalizePath(path.resolve(root, target)))
) {
throw new Error(`Export must point to an owned module: ${key}`);
}
return path.resolve(root, target);
});
const seen = new Set();
await build({
root,
configFile: false,
envFile: false,
publicDir: false,
logLevel: 'silent',
plugins: [
{
name: 'check-package-ownership',
moduleParsed(info) {
if (!allowed.has(info.id)) {
throw new Error(
`Package boundary ${name} imports an unowned module: ${path.relative(root, info.id)}`,
);
}
seen.add(info.id);
},
},
],
build: {
lib: { entry: input, formats: ['es'] },
write: false,
minify: false,
assetsInlineLimit: 0,
rollupOptions: {
input,
external: group.external,
// Unused imports must still obey ownership; tree shaking is not a boundary.
treeshake: false,
preserveEntrySignatures: 'strict',
},
},
});
reports.push({ name, exports: input.length, modules: seen.size });
}
return reports;
}
const invoked = process.argv[1]
? pathToFileURL(path.resolve(process.argv[1])).href
: '';
if (import.meta.url === invoked) {
try {
const reports = await checkPackageBoundaries(
fileURLToPath(new URL('../', import.meta.url)),
);
for (const report of reports)
console.log(
`Checked ${report.name}: ${report.exports} exports, ${report.modules} owned modules.`,
);
} catch (error) {
console.error(error.message);
process.exitCode = 1;
}
}
+15
View File
@@ -0,0 +1,15 @@
[
".prettierrc.json",
"package.json",
"scripts/format-scope.json",
"scripts/package-boundaries.json",
"scripts/format.mjs",
"scripts/check-package-boundaries.mjs",
"src/tooling/format.test.mjs",
"src/tooling/packageBoundaries.test.mjs",
"src/data/infrastructure.js",
"src/data/infrastructure.test.mjs",
"src/data/localGeojsonCore.js",
"src/data/localGeojsonLod.js",
"docs/CODE-BOUNDARIES.md"
]
+101
View File
@@ -0,0 +1,101 @@
import { readFile, writeFile, realpath, stat } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import * as prettier from 'prettier';
/** Check or format the explicitly adopted files, without scanning other source. */
export async function formatAdoptedFiles(root, mode) {
if (!['--check', '--write'].includes(mode)) {
throw new Error('Usage: node scripts/format.mjs --check|--write');
}
root = await realpath(root);
const scope = JSON.parse(
await readFile(path.join(root, 'scripts/format-scope.json'), 'utf8'),
);
if (
!Array.isArray(scope) ||
!scope.length ||
new Set(scope).size !== scope.length
) {
throw new Error(
'Formatting scope must be a nonempty list of unique file paths',
);
}
// Validate the whole scope before any write, including resolved symlink paths.
const files = [];
for (const name of scope) {
if (
typeof name !== 'string' ||
!name ||
name.includes('\\') ||
path.isAbsolute(name) ||
name.split('/').includes('..')
) {
throw new Error(
'Formatting scope entries must be paths inside the repository',
);
}
const file = path.join(root, name);
const relative = path.relative(root, await realpath(file));
if (
relative === '..' ||
relative.startsWith(`..${path.sep}`) ||
path.isAbsolute(relative) ||
!(await stat(file)).isFile()
) {
throw new Error(
`Formatting scope must contain repository files: ${name}`,
);
}
const info = await prettier.getFileInfo(file, {
ignorePath: path.join(root, '.prettierignore'),
});
if (info.ignored || !info.inferredParser) {
throw new Error(
`Formatting scope contains an ignored or unsupported file: ${name}`,
);
}
const options = { ...(await prettier.resolveConfig(file)), filepath: file };
const source = await readFile(file, 'utf8');
files.push({
name,
file,
source,
formatted: await prettier.format(source, options),
});
}
const changed = files.filter(({ source, formatted }) => source !== formatted);
if (mode === '--write') {
for (const { file, formatted } of changed) await writeFile(file, formatted);
}
return { count: files.length, changed: changed.map(({ name }) => name) };
}
const invoked = process.argv[1]
? pathToFileURL(path.resolve(process.argv[1])).href
: '';
if (import.meta.url === invoked) {
try {
if (process.argv.length !== 3)
throw new Error('Usage: node scripts/format.mjs --check|--write');
const mode = process.argv[2];
const result = await formatAdoptedFiles(
fileURLToPath(new URL('../', import.meta.url)),
mode,
);
if (mode === '--check' && result.changed.length) {
for (const name of result.changed)
console.error(`Needs formatting: ${name}`);
process.exitCode = 1;
} else {
console.log(
`${mode === '--write' ? 'Formatted' : 'Checked'} ${result.count} adopted files.`,
);
}
} catch (error) {
console.error(error.message);
process.exitCode = 1;
}
}
+15
View File
@@ -0,0 +1,15 @@
{
"infrastructure": {
"exports": [
"./infrastructure",
"./infrastructure/geojson",
"./infrastructure/lod"
],
"modules": [
"src/data/infrastructure.js",
"src/data/localGeojsonCore.js",
"src/data/localGeojsonLod.js"
],
"external": ["cesium"]
}
}
+95
View File
@@ -0,0 +1,95 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {
mkdtemp,
mkdir,
writeFile,
readFile,
rm,
symlink,
} from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { formatAdoptedFiles } from '../../scripts/format.mjs';
async function fixture(t, scope = ['adopted.js']) {
const root = await mkdtemp(path.join(tmpdir(), 'gev-format-'));
t.after(() => rm(root, { recursive: true, force: true }));
await mkdir(path.join(root, 'scripts'));
await writeFile(
path.join(root, 'scripts/format-scope.json'),
JSON.stringify(scope),
);
await writeFile(
path.join(root, '.prettierrc.json'),
JSON.stringify({
singleQuote: true,
semi: true,
tabWidth: 2,
endOfLine: 'lf',
}),
);
await writeFile(path.join(root, '.prettierignore'), 'ignored.js\n');
await writeFile(path.join(root, 'adopted.js'), 'const value="yes"\r\n');
return root;
}
test('format check is read-only; write only changes adopted files and is repeatable', async (t) => {
const root = await fixture(t);
const outside = 'const untouched="yes"';
await writeFile(path.join(root, 'legacy.js'), outside);
assert.deepEqual((await formatAdoptedFiles(root, '--check')).changed, [
'adopted.js',
]);
assert.equal(
await readFile(path.join(root, 'adopted.js'), 'utf8'),
'const value="yes"\r\n',
);
await formatAdoptedFiles(root, '--write');
assert.equal(
await readFile(path.join(root, 'adopted.js'), 'utf8'),
"const value = 'yes';\n",
);
assert.equal(await readFile(path.join(root, 'legacy.js'), 'utf8'), outside);
assert.deepEqual((await formatAdoptedFiles(root, '--check')).changed, []);
assert.deepEqual((await formatAdoptedFiles(root, '--write')).changed, []);
});
test('invalid, ignored and missing scope entries fail before any file is written', async (t) => {
for (const scope of [
[],
['adopted.js', 'adopted.js'],
['adopted.js', '../escape.js'],
['adopted.js', 'ignored.js'],
['adopted.js', 'missing.js'],
]) {
const root = await fixture(t, scope);
await writeFile(path.join(root, 'ignored.js'), 'const ignored=1');
await assert.rejects(formatAdoptedFiles(root, '--write'));
assert.equal(
await readFile(path.join(root, 'adopted.js'), 'utf8'),
'const value="yes"\r\n',
);
}
});
test(
'formatting rejects a symlink outside the repository',
{ skip: process.platform === 'win32' },
async (t) => {
const root = await fixture(t, ['adopted.js', 'outside.js']);
const other = await fixture(t);
await symlink(
path.join(other, 'adopted.js'),
path.join(root, 'outside.js'),
);
await assert.rejects(
formatAdoptedFiles(root, '--write'),
/repository files/,
);
assert.equal(
await readFile(path.join(root, 'adopted.js'), 'utf8'),
'const value="yes"\r\n',
);
},
);
+77
View File
@@ -0,0 +1,77 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { checkPackageBoundaries } from '../../scripts/check-package-boundaries.mjs';
async function fixture(
t,
entry = 'export const value = 1;',
extraExports = {},
) {
const root = await mkdtemp(path.join(tmpdir(), 'gev-boundaries-'));
t.after(() => rm(root, { recursive: true, force: true }));
await mkdir(path.join(root, 'scripts'));
await writeFile(
path.join(root, 'package.json'),
JSON.stringify({
name: 'boundary-fixture',
type: 'module',
exports: { './feature': './entry.js', ...extraExports },
}),
);
await writeFile(
path.join(root, 'scripts/package-boundaries.json'),
JSON.stringify({
feature: { exports: ['./feature'], modules: ['entry.js'], external: [] },
}),
);
await writeFile(path.join(root, 'entry.js'), entry);
return root;
}
test('a declared browser export builds without running app setup', async (t) => {
const root = await fixture(t);
await writeFile(
path.join(root, 'vite.config.js'),
"throw new Error('must not load app config');",
);
assert.deepEqual(await checkPackageBoundaries(root), [
{ name: 'feature', exports: 1, modules: 1 },
]);
});
test('an unclassified package export fails the boundary gate', async (t) => {
const root = await fixture(t, undefined, { './another': './another.js' });
await assert.rejects(
checkPackageBoundaries(root),
/exactly one boundary group/,
);
});
test('even an unused import of application code violates ownership', async (t) => {
const root = await fixture(
t,
"import { app } from './startup.js'; export const value = 1;",
);
await writeFile(path.join(root, 'startup.js'), 'export const app = 2;');
await assert.rejects(checkPackageBoundaries(root), /unowned module.*startup/);
});
test('Node builtins cannot enter a browser export', async (t) => {
const root = await fixture(
t,
"import fs from 'node:fs'; export const value = fs;",
);
await assert.rejects(checkPackageBoundaries(root), /unowned module|browser/);
});
test('dynamic imports obey the same component ownership rule', async (t) => {
const root = await fixture(
t,
"export const load = () => import('./startup.js');",
);
await writeFile(path.join(root, 'startup.js'), 'export const app = 2;');
await assert.rejects(checkPackageBoundaries(root), /unowned module.*startup/);
});