clone

an offline map explorer where each world, from a game map to a city's open data, ships as one .atlas file

feat/atlas-next-horizon
atlas / eslint.config.mjs
eslint.config.mjs 8.2K
1// The TypeScript half of the guardrails (issue #5 §9).2//3// Same boundaries as tools/depcheck, where they cross into TS: analysis/4// imports no DOM and nothing app-shaped; render/ imports only analysis/ and5// itself; fetch lives in render's data layer; console lives in the log module.6// Most of these rules exist to say "we don't do that here" — the message is7// the point, so every one names the contract and cites the issue section.8//9// The lanes it polices (analysis/, render/) consume it through the root10// workspace's `npm run lint`. To run it:11//12// make lint-lanes # this file, over both lanes13// make analysis-lane # and the boundary rules with the suite behind them14// make render-lane15//16// Two of the three things this file once left for later are here now: the17// analysis package's import specifier is declared in the allowlist below, and18// the seam's authored line budget is a warning `render/tools/lines.mjs` prints19// on every run of the render lane. Type-aware linting is the one still open —20// the tsconfigs exist and `npm run typecheck` reads them, so what is missing is21// only the wiring that would let a rule here read a type.2223import tseslint from "typescript-eslint";2425const cite = (violation, section, why) => `${violation} (issue #5 §${section}): ${why}`;2627/** Browser globals the analysis lane must not reach for. */28const domGlobals = [29 "document",30 "window",31 "navigator",32 "customElements",33 "HTMLElement",34 "Element",35 "Node",36 "localStorage",37 "sessionStorage",38 "requestAnimationFrame",39 "getComputedStyle",40];4142/** Everything that reaches the network. */43const networkGlobals = ["fetch", "XMLHttpRequest", "EventSource", "WebSocket"];4445const noDom = domGlobals.map((name) => ({46 name,47 message: cite(48 `analysis/ must not touch the DOM (${name})`,49 "5.4",50 "an analysis is a pure transformation of its declared inputs — a Ground descriptor and a volume, never the page it happens to be running in; that is what lets a system be tested headlessly and reused by a renderer nobody has written yet",51 ),52}));5354const noNetwork = (lane, why) =>55 networkGlobals.map((name) => ({56 name,57 message: cite(`${lane} must not reach the network (${name})`, "9", why),58 }));5960const consoleRule = {61 selector: "MemberExpression[object.name='console']",62 message: cite(63 "bare console.* is not the log stream",64 "9",65 "the system narrates itself through one leveled, structured stream; the TS log module is the browser end of it, and a headless driver captures it — a failing e2e step ships its console context for free only if everything goes through the module",66 ),67};6869export default tseslint.config(70 {71 // The corpus is data, not source: it is compared byte for byte, and a72 // linter that reformatted a file would be breaking the test that reads it.73 ignores: ["testdata/corpus/**", "**/node_modules/**", "**/dist/**", "**/build/**"],74 },7576 {77 files: ["analysis/**/*.ts", "render/**/*.ts"],78 extends: [tseslint.configs.recommended],79 languageOptions: {80 ecmaVersion: 2023,81 sourceType: "module",82 },83 rules: {84 "@typescript-eslint/no-explicit-any": [85 "error",86 { fixToUnknown: false, ignoreRestArgs: false },87 ],88 "no-restricted-syntax": ["error", consoleRule],89 },90 },9192 {93 files: ["analysis/**/*.ts"],94 rules: {95 "no-restricted-globals": ["error", ...noDom, ...noNetwork(96 "analysis/",97 "an analysis declares its inputs; fetching them is the consumer's adapter concern, not the lane's",98 )],99 "no-restricted-imports": [100 "error",101 {102 patterns: [103 {104 group: ["**/render/**", "render", "render/*", "ol", "ol/*", "globe.gl", "three", "three/*"],105 message: cite(106 "analysis/ must not import the renderer or its libraries",107 "3.2",108 "analysis depends only on its own math and the published attribute vocabulary; renderers adapt its tokens, and no renderer owns a cell rule",109 ),110 },111 {112 group: ["**/internal/**", "**/format/**", "**/tests/**", "htmx*", "**/app/**"],113 message: cite(114 "analysis/ must not import anything app-shaped",115 "3.2",116 "the lane is a TypeScript package a third party could use: app state, session shape and server contracts are not its inputs",117 ),118 },119 ],120 },121 ],122 },123 },124125 {126 files: ["render/**/*.ts"],127 rules: {128 "no-restricted-imports": [129 "error",130 {131 patterns: [132 {133 // The allowlist, in full. The analysis lane's specifier is its134 // workspace name, `@atlas/analysis`; the135 // pinned dependency surface (OpenLayers, globe.gl / three,136 // s2js through analysis) is the rest of it, and it grows only137 // by a deliberate, reviewed decision. Relative imports are allowed at138 // any depth — the lane is one package and its own directories139 // are not a boundary.140 //141 // The patterns are gitignore-shaped, which is why each allowed142 // scope is un-denied twice: gitignore cannot re-include a path143 // whose parent directory is excluded, so `!ol` has to precede144 // `!ol/**` for `ol/style/Style.js` to be admitted at all.145 group: [146 "*",147 "!.",148 "!./**",149 "!..",150 "!../**",151 "!@atlas",152 "!@atlas/**",153 "!ol",154 "!ol/**",155 "!globe.gl",156 "!three",157 "!three/**",158 "!s2js",159 ],160 message: cite(161 "render/ imports only analysis/, itself, and its pinned dependency surface",162 "5.5",163 "the seam stands on exactly three published documents — the /data plane, the scene description, the analysis API — which is what makes it rewritable from its contracts alone",164 ),165 },166 {167 group: ["**/internal/**", "**/format/**", "**/tests/**", "**/app/**", "htmx*"],168 message: cite(169 "render/ must not import the application",170 "5.5",171 "data flows one way — server to scene description to seam — and only two things flow back: the atlas:pick event and the debounced camera report",172 ),173 },174 ],175 },176 ],177 },178 },179180 {181 // The seam's own tests run in Node, against the corpus. They may182 // reach for the standard library to read a file and hash bytes; they are183 // not shipped, and every other boundary still holds over them.184 files: ["render/test/**/*.ts"],185 rules: {186 "no-restricted-imports": [187 "error",188 {189 patterns: [190 {191 group: [192 "*", "!.", "!./**", "!..", "!../**", "!@atlas", "!@atlas/**",193 "!node:*", "!ol", "!ol/**", "!globe.gl", "!three", "!three/**", "!s2js",194 ],195 message: cite(196 "even the seam's tests import only the lane, the analysis package, its pinned surface and the standard library",197 "5.5",198 "a test that reached into the application would be proving the seam against the thing it is supposed to be independent of",199 ),200 },201 ],202 },203 ],204 },205 },206207 {208 // Fetch is a data-layer concern: the seam reads /data and tiles in one209 // place, so the offline invariant and the cache rules have one owner.210 files: ["render/**/*.ts"],211 ignores: ["render/data/**/*.ts"],212 rules: {213 "no-restricted-globals": ["error", ...noNetwork(214 "render/",215 "the seam's network access lives in its data layer, where the /data plane's URL shapes, the immutable cache rules and the zero-copy ATLASLOC unpack already live",216 )],217 },218 },219220 {221 // The one module allowed to be the console.222 files: ["analysis/log.ts", "render/log.ts", "**/log/*.ts"],223 rules: {224 "no-restricted-syntax": "off",225 },226 },227);