Gates the folder move had narrowed or left open: the MenuCheck contract scans the whole app again (it saw only components/); local/layers refuses an import of an entry (main.tsx, the /watch page) from any layer, and reads a backtick import() as the path it is; the /watch contract forbids the shell, the modules and the features by folder (its one path to App had gone stale); chunkGuard's own test gets the vitest focus/skip rules. The root tests' exemption from local/layers is in the gate register, with its three imports. Placement: Plex's chip builder moves into lib beside the feed's, the folder names into features/folders, and three files only one page uses into that page. ui/FilterRail takes its reset from useRail instead of a page id. The channel page reads the feed's view and builds "Show in feed" itself, and the feed's scroll key is the feed's hook, so the shell holds no feed logic. The floating header re-measures when one page's pills replace another's in the same commit (the claim count stayed equal, so only the observer caught it). A Plex view applied and edited now stays across a visit to another page, as the feed's does: pinned by a test. docs/development.md says what a page with a rail adds in lib.
1516 lines
61 KiB
JSON
1516 lines
61 KiB
JSON
{
|
||
"eslint": {
|
||
"design-lab": {
|
||
"languageOptions": {
|
||
"ecmaVersion": 2022,
|
||
"globals": "1169 names, sha1 aee621727a03",
|
||
"parser": "typescript-eslint/parser",
|
||
"parserOptions": { "projectService": true, "tsconfigRootDir": "." },
|
||
"sourceType": "module"
|
||
},
|
||
"linterOptions": { "reportUnusedDisableDirectives": 2 },
|
||
"notApplied": [],
|
||
"plugins": [
|
||
"@",
|
||
"@eslint-community/eslint-comments",
|
||
"@typescript-eslint",
|
||
"local",
|
||
"react-hooks",
|
||
"react-refresh"
|
||
],
|
||
"rules": { "local/literal-a11y-text": [0] },
|
||
"settings": null
|
||
},
|
||
"e2e": {
|
||
"languageOptions": {
|
||
"ecmaVersion": 2022,
|
||
"globals": "82 names, sha1 29467d490e30",
|
||
"parser": "typescript-eslint/parser",
|
||
"parserOptions": { "projectService": true, "tsconfigRootDir": "." },
|
||
"sourceType": "module"
|
||
},
|
||
"linterOptions": { "reportUnusedDisableDirectives": 2 },
|
||
"notApplied": [
|
||
"local/button-primitive",
|
||
"local/glyph-link",
|
||
"local/input-primitive",
|
||
"local/jsx-comment-text",
|
||
"local/layers",
|
||
"local/literal-a11y-text",
|
||
"local/locale-format",
|
||
"local/sortable-a11y",
|
||
"local/surface-role",
|
||
"no-console",
|
||
"no-restricted-globals",
|
||
"no-restricted-properties",
|
||
"no-restricted-syntax",
|
||
"react-hooks/capitalized-calls",
|
||
"react-hooks/component-hook-factories",
|
||
"react-hooks/config",
|
||
"react-hooks/error-boundaries",
|
||
"react-hooks/exhaustive-deps",
|
||
"react-hooks/gating",
|
||
"react-hooks/globals",
|
||
"react-hooks/hooks",
|
||
"react-hooks/immutability",
|
||
"react-hooks/incompatible-library",
|
||
"react-hooks/invariant",
|
||
"react-hooks/memo-dependencies",
|
||
"react-hooks/no-deriving-state-in-effects",
|
||
"react-hooks/preserve-manual-memoization",
|
||
"react-hooks/purity",
|
||
"react-hooks/refs",
|
||
"react-hooks/rules-of-hooks",
|
||
"react-hooks/set-state-in-effect",
|
||
"react-hooks/set-state-in-render",
|
||
"react-hooks/static-components",
|
||
"react-hooks/syntax",
|
||
"react-hooks/todo",
|
||
"react-hooks/unsupported-syntax",
|
||
"react-hooks/use-memo",
|
||
"react-hooks/void-use-memo",
|
||
"react-refresh/only-export-components"
|
||
],
|
||
"plugins": ["@", "@eslint-community/eslint-comments", "@typescript-eslint", "playwright"],
|
||
"rules": {
|
||
"@typescript-eslint/no-unused-expressions": [
|
||
2,
|
||
{ "allowShortCircuit": false, "allowTaggedTemplates": false, "allowTernary": false }
|
||
],
|
||
"playwright/no-focused-test": [2],
|
||
"playwright/no-skipped-test": [2]
|
||
},
|
||
"settings": null
|
||
},
|
||
"js configs": {
|
||
"languageOptions": {
|
||
"ecmaVersion": 2022,
|
||
"globals": "82 names, sha1 29467d490e30",
|
||
"parser": "(unnamed parser)",
|
||
"parserOptions": { "sourceType": "module" },
|
||
"sourceType": "module"
|
||
},
|
||
"linterOptions": { "reportUnusedDisableDirectives": 2 },
|
||
"notApplied": [
|
||
"@typescript-eslint/ban-ts-comment",
|
||
"@typescript-eslint/no-array-constructor",
|
||
"@typescript-eslint/no-deprecated",
|
||
"@typescript-eslint/no-duplicate-enum-values",
|
||
"@typescript-eslint/no-empty-object-type",
|
||
"@typescript-eslint/no-explicit-any",
|
||
"@typescript-eslint/no-extra-non-null-assertion",
|
||
"@typescript-eslint/no-floating-promises",
|
||
"@typescript-eslint/no-misused-new",
|
||
"@typescript-eslint/no-namespace",
|
||
"@typescript-eslint/no-non-null-asserted-optional-chain",
|
||
"@typescript-eslint/no-require-imports",
|
||
"@typescript-eslint/no-this-alias",
|
||
"@typescript-eslint/no-unnecessary-type-constraint",
|
||
"@typescript-eslint/no-unsafe-declaration-merging",
|
||
"@typescript-eslint/no-unsafe-function-type",
|
||
"@typescript-eslint/no-unused-expressions",
|
||
"@typescript-eslint/no-unused-vars",
|
||
"@typescript-eslint/no-wrapper-object-types",
|
||
"@typescript-eslint/prefer-as-const",
|
||
"@typescript-eslint/prefer-namespace-keyword",
|
||
"@typescript-eslint/triple-slash-reference",
|
||
"local/button-primitive",
|
||
"local/glyph-link",
|
||
"local/input-primitive",
|
||
"local/jsx-comment-text",
|
||
"local/layers",
|
||
"local/literal-a11y-text",
|
||
"local/locale-format",
|
||
"local/sortable-a11y",
|
||
"local/surface-role",
|
||
"no-array-constructor",
|
||
"no-console",
|
||
"no-new-symbol",
|
||
"no-restricted-globals",
|
||
"no-restricted-properties",
|
||
"no-restricted-syntax",
|
||
"no-unused-expressions",
|
||
"no-var",
|
||
"prefer-const",
|
||
"prefer-rest-params",
|
||
"prefer-spread",
|
||
"react-hooks/capitalized-calls",
|
||
"react-hooks/component-hook-factories",
|
||
"react-hooks/config",
|
||
"react-hooks/error-boundaries",
|
||
"react-hooks/exhaustive-deps",
|
||
"react-hooks/gating",
|
||
"react-hooks/globals",
|
||
"react-hooks/hooks",
|
||
"react-hooks/immutability",
|
||
"react-hooks/incompatible-library",
|
||
"react-hooks/invariant",
|
||
"react-hooks/memo-dependencies",
|
||
"react-hooks/no-deriving-state-in-effects",
|
||
"react-hooks/preserve-manual-memoization",
|
||
"react-hooks/purity",
|
||
"react-hooks/refs",
|
||
"react-hooks/rules-of-hooks",
|
||
"react-hooks/set-state-in-effect",
|
||
"react-hooks/set-state-in-render",
|
||
"react-hooks/static-components",
|
||
"react-hooks/syntax",
|
||
"react-hooks/todo",
|
||
"react-hooks/unsupported-syntax",
|
||
"react-hooks/use-memo",
|
||
"react-hooks/void-use-memo",
|
||
"react-refresh/only-export-components"
|
||
],
|
||
"plugins": ["@", "@eslint-community/eslint-comments"],
|
||
"rules": {
|
||
"constructor-super": [2],
|
||
"getter-return": [2, { "allowImplicit": false }],
|
||
"no-class-assign": [2],
|
||
"no-const-assign": [2],
|
||
"no-dupe-args": [2],
|
||
"no-dupe-class-members": [2],
|
||
"no-dupe-keys": [2],
|
||
"no-func-assign": [2],
|
||
"no-import-assign": [2],
|
||
"no-new-native-nonconstructor": [2],
|
||
"no-obj-calls": [2],
|
||
"no-redeclare": [2, { "builtinGlobals": true }],
|
||
"no-setter-return": [2],
|
||
"no-this-before-super": [2],
|
||
"no-undef": [2, { "typeof": false }],
|
||
"no-unreachable": [2],
|
||
"no-unsafe-negation": [2, { "enforceForOrderingRelations": false }],
|
||
"no-unused-vars": [2],
|
||
"no-with": [2]
|
||
},
|
||
"settings": null
|
||
},
|
||
"lib tests": {
|
||
"languageOptions": {
|
||
"ecmaVersion": 2022,
|
||
"globals": "1169 names, sha1 aee621727a03",
|
||
"parser": "typescript-eslint/parser",
|
||
"parserOptions": { "projectService": true, "tsconfigRootDir": "." },
|
||
"sourceType": "module"
|
||
},
|
||
"linterOptions": { "reportUnusedDisableDirectives": 2 },
|
||
"notApplied": [],
|
||
"plugins": [
|
||
"@",
|
||
"@eslint-community/eslint-comments",
|
||
"@typescript-eslint",
|
||
"local",
|
||
"react-hooks",
|
||
"react-refresh",
|
||
"vitest"
|
||
],
|
||
"rules": {
|
||
"local/glyph-link": [0],
|
||
"local/input-primitive": [0],
|
||
"local/literal-a11y-text": [0],
|
||
"local/surface-role": [0],
|
||
"no-restricted-globals": [
|
||
0,
|
||
{
|
||
"message": "Use the logged request layer: `fetch` belongs to src/lib/http.ts only — a request outside it never reaches the client log.",
|
||
"name": "fetch"
|
||
},
|
||
{
|
||
"message": "Use the logged request layer: `XMLHttpRequest` belongs to src/lib/api/core.ts only — a request outside it never reaches the client log.",
|
||
"name": "XMLHttpRequest"
|
||
},
|
||
{ "message": "No `WebSocket` without a logged owner module.", "name": "WebSocket" },
|
||
{ "message": "No `EventSource` without a logged owner module.", "name": "EventSource" }
|
||
],
|
||
"no-restricted-imports": [
|
||
2,
|
||
{
|
||
"patterns": [
|
||
{
|
||
"message": "lib/ sits below components/: it may not import from components/, not even a type. Move the shared piece down into lib/.",
|
||
"regex": "^(\\.\\./)+components(/|$)"
|
||
}
|
||
]
|
||
}
|
||
],
|
||
"no-restricted-properties": [
|
||
0,
|
||
{ "message": "Use lib/http.ts `send`.", "object": "window", "property": "fetch" },
|
||
{ "message": "Use lib/http.ts `send`.", "object": "globalThis", "property": "fetch" },
|
||
{
|
||
"message": "Use lib/api/core.ts `beacon`.",
|
||
"object": "navigator",
|
||
"property": "sendBeacon"
|
||
}
|
||
],
|
||
"vitest/no-disabled-tests": [2],
|
||
"vitest/no-focused-tests": [2, { "fixable": true }]
|
||
},
|
||
"settings": null
|
||
},
|
||
"lib/api/core.ts": {
|
||
"languageOptions": {
|
||
"ecmaVersion": 2022,
|
||
"globals": "1169 names, sha1 aee621727a03",
|
||
"parser": "typescript-eslint/parser",
|
||
"parserOptions": { "projectService": true, "tsconfigRootDir": "." },
|
||
"sourceType": "module"
|
||
},
|
||
"linterOptions": { "reportUnusedDisableDirectives": 2 },
|
||
"notApplied": [],
|
||
"plugins": [
|
||
"@",
|
||
"@eslint-community/eslint-comments",
|
||
"@typescript-eslint",
|
||
"local",
|
||
"react-hooks",
|
||
"react-refresh"
|
||
],
|
||
"rules": {
|
||
"no-restricted-globals": [
|
||
2,
|
||
{
|
||
"message": "Use the logged request layer: `fetch` belongs to src/lib/http.ts only — a request outside it never reaches the client log.",
|
||
"name": "fetch"
|
||
},
|
||
{ "message": "No `WebSocket` without a logged owner module.", "name": "WebSocket" },
|
||
{ "message": "No `EventSource` without a logged owner module.", "name": "EventSource" }
|
||
],
|
||
"no-restricted-imports": [
|
||
2,
|
||
{
|
||
"patterns": [
|
||
{
|
||
"message": "lib/ sits below components/: it may not import from components/, not even a type. Move the shared piece down into lib/.",
|
||
"regex": "^(\\.\\./)+components(/|$)"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
},
|
||
"settings": null
|
||
},
|
||
"lib/http.ts": {
|
||
"languageOptions": {
|
||
"ecmaVersion": 2022,
|
||
"globals": "1169 names, sha1 aee621727a03",
|
||
"parser": "typescript-eslint/parser",
|
||
"parserOptions": { "projectService": true, "tsconfigRootDir": "." },
|
||
"sourceType": "module"
|
||
},
|
||
"linterOptions": { "reportUnusedDisableDirectives": 2 },
|
||
"notApplied": [],
|
||
"plugins": [
|
||
"@",
|
||
"@eslint-community/eslint-comments",
|
||
"@typescript-eslint",
|
||
"local",
|
||
"react-hooks",
|
||
"react-refresh"
|
||
],
|
||
"rules": {
|
||
"no-restricted-globals": [
|
||
2,
|
||
{
|
||
"message": "Use the logged request layer: `XMLHttpRequest` belongs to src/lib/api/core.ts only — a request outside it never reaches the client log.",
|
||
"name": "XMLHttpRequest"
|
||
},
|
||
{ "message": "No `WebSocket` without a logged owner module.", "name": "WebSocket" },
|
||
{ "message": "No `EventSource` without a logged owner module.", "name": "EventSource" }
|
||
],
|
||
"no-restricted-imports": [
|
||
2,
|
||
{
|
||
"patterns": [
|
||
{
|
||
"message": "lib/ sits below components/: it may not import from components/, not even a type. Move the shared piece down into lib/.",
|
||
"regex": "^(\\.\\./)+components(/|$)"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
},
|
||
"settings": null
|
||
},
|
||
"src": {
|
||
"languageOptions": {
|
||
"ecmaVersion": 2022,
|
||
"globals": "1169 names, sha1 aee621727a03",
|
||
"parser": "typescript-eslint/parser",
|
||
"parserOptions": { "projectService": true, "tsconfigRootDir": "." },
|
||
"sourceType": "module"
|
||
},
|
||
"linterOptions": { "reportUnusedDisableDirectives": 2 },
|
||
"plugins": [
|
||
"@",
|
||
"@eslint-community/eslint-comments",
|
||
"@typescript-eslint",
|
||
"local",
|
||
"react-hooks",
|
||
"react-refresh"
|
||
],
|
||
"rules": {
|
||
"@babel/object-curly-spacing": [0],
|
||
"@babel/semi": [0],
|
||
"@eslint-community/eslint-comments/no-unlimited-disable": [2],
|
||
"@eslint-community/eslint-comments/require-description": [2, { "ignore": [] }],
|
||
"@stylistic/array-bracket-newline": [0],
|
||
"@stylistic/array-bracket-spacing": [0],
|
||
"@stylistic/array-element-newline": [0],
|
||
"@stylistic/arrow-parens": [0],
|
||
"@stylistic/arrow-spacing": [0],
|
||
"@stylistic/block-spacing": [0],
|
||
"@stylistic/brace-style": [0],
|
||
"@stylistic/comma-dangle": [0],
|
||
"@stylistic/comma-spacing": [0],
|
||
"@stylistic/comma-style": [0],
|
||
"@stylistic/computed-property-spacing": [0],
|
||
"@stylistic/dot-location": [0],
|
||
"@stylistic/eol-last": [0],
|
||
"@stylistic/func-call-spacing": [0],
|
||
"@stylistic/function-call-argument-newline": [0],
|
||
"@stylistic/function-call-spacing": [0],
|
||
"@stylistic/function-paren-newline": [0],
|
||
"@stylistic/generator-star-spacing": [0],
|
||
"@stylistic/implicit-arrow-linebreak": [0],
|
||
"@stylistic/indent": [0],
|
||
"@stylistic/indent-binary-ops": [0],
|
||
"@stylistic/js/array-bracket-newline": [0],
|
||
"@stylistic/js/array-bracket-spacing": [0],
|
||
"@stylistic/js/array-element-newline": [0],
|
||
"@stylistic/js/arrow-parens": [0],
|
||
"@stylistic/js/arrow-spacing": [0],
|
||
"@stylistic/js/block-spacing": [0],
|
||
"@stylistic/js/brace-style": [0],
|
||
"@stylistic/js/comma-dangle": [0],
|
||
"@stylistic/js/comma-spacing": [0],
|
||
"@stylistic/js/comma-style": [0],
|
||
"@stylistic/js/computed-property-spacing": [0],
|
||
"@stylistic/js/dot-location": [0],
|
||
"@stylistic/js/eol-last": [0],
|
||
"@stylistic/js/func-call-spacing": [0],
|
||
"@stylistic/js/function-call-argument-newline": [0],
|
||
"@stylistic/js/function-call-spacing": [0],
|
||
"@stylistic/js/function-paren-newline": [0],
|
||
"@stylistic/js/generator-star-spacing": [0],
|
||
"@stylistic/js/implicit-arrow-linebreak": [0],
|
||
"@stylistic/js/indent": [0],
|
||
"@stylistic/js/jsx-quotes": [0],
|
||
"@stylistic/js/key-spacing": [0],
|
||
"@stylistic/js/keyword-spacing": [0],
|
||
"@stylistic/js/linebreak-style": [0],
|
||
"@stylistic/js/lines-around-comment": [0],
|
||
"@stylistic/js/max-len": [0],
|
||
"@stylistic/js/max-statements-per-line": [0],
|
||
"@stylistic/js/multiline-ternary": [0],
|
||
"@stylistic/js/new-parens": [0],
|
||
"@stylistic/js/newline-per-chained-call": [0],
|
||
"@stylistic/js/no-confusing-arrow": [0],
|
||
"@stylistic/js/no-extra-parens": [0],
|
||
"@stylistic/js/no-extra-semi": [0],
|
||
"@stylistic/js/no-floating-decimal": [0],
|
||
"@stylistic/js/no-mixed-operators": [0],
|
||
"@stylistic/js/no-mixed-spaces-and-tabs": [0],
|
||
"@stylistic/js/no-multi-spaces": [0],
|
||
"@stylistic/js/no-multiple-empty-lines": [0],
|
||
"@stylistic/js/no-tabs": [0],
|
||
"@stylistic/js/no-trailing-spaces": [0],
|
||
"@stylistic/js/no-whitespace-before-property": [0],
|
||
"@stylistic/js/nonblock-statement-body-position": [0],
|
||
"@stylistic/js/object-curly-newline": [0],
|
||
"@stylistic/js/object-curly-spacing": [0],
|
||
"@stylistic/js/object-property-newline": [0],
|
||
"@stylistic/js/one-var-declaration-per-line": [0],
|
||
"@stylistic/js/operator-linebreak": [0],
|
||
"@stylistic/js/padded-blocks": [0],
|
||
"@stylistic/js/quote-props": [0],
|
||
"@stylistic/js/quotes": [0],
|
||
"@stylistic/js/rest-spread-spacing": [0],
|
||
"@stylistic/js/semi": [0],
|
||
"@stylistic/js/semi-spacing": [0],
|
||
"@stylistic/js/semi-style": [0],
|
||
"@stylistic/js/space-before-blocks": [0],
|
||
"@stylistic/js/space-before-function-paren": [0],
|
||
"@stylistic/js/space-in-parens": [0],
|
||
"@stylistic/js/space-infix-ops": [0],
|
||
"@stylistic/js/space-unary-ops": [0],
|
||
"@stylistic/js/switch-colon-spacing": [0],
|
||
"@stylistic/js/template-curly-spacing": [0],
|
||
"@stylistic/js/template-tag-spacing": [0],
|
||
"@stylistic/js/wrap-iife": [0],
|
||
"@stylistic/js/wrap-regex": [0],
|
||
"@stylistic/js/yield-star-spacing": [0],
|
||
"@stylistic/jsx-child-element-spacing": [0],
|
||
"@stylistic/jsx-closing-bracket-location": [0],
|
||
"@stylistic/jsx-closing-tag-location": [0],
|
||
"@stylistic/jsx-curly-newline": [0],
|
||
"@stylistic/jsx-curly-spacing": [0],
|
||
"@stylistic/jsx-equals-spacing": [0],
|
||
"@stylistic/jsx-first-prop-new-line": [0],
|
||
"@stylistic/jsx-indent": [0],
|
||
"@stylistic/jsx-indent-props": [0],
|
||
"@stylistic/jsx-max-props-per-line": [0],
|
||
"@stylistic/jsx-newline": [0],
|
||
"@stylistic/jsx-one-expression-per-line": [0],
|
||
"@stylistic/jsx-props-no-multi-spaces": [0],
|
||
"@stylistic/jsx-quotes": [0],
|
||
"@stylistic/jsx-tag-spacing": [0],
|
||
"@stylistic/jsx-wrap-multilines": [0],
|
||
"@stylistic/jsx/jsx-child-element-spacing": [0],
|
||
"@stylistic/jsx/jsx-closing-bracket-location": [0],
|
||
"@stylistic/jsx/jsx-closing-tag-location": [0],
|
||
"@stylistic/jsx/jsx-curly-newline": [0],
|
||
"@stylistic/jsx/jsx-curly-spacing": [0],
|
||
"@stylistic/jsx/jsx-equals-spacing": [0],
|
||
"@stylistic/jsx/jsx-first-prop-new-line": [0],
|
||
"@stylistic/jsx/jsx-indent": [0],
|
||
"@stylistic/jsx/jsx-indent-props": [0],
|
||
"@stylistic/jsx/jsx-max-props-per-line": [0],
|
||
"@stylistic/key-spacing": [0],
|
||
"@stylistic/keyword-spacing": [0],
|
||
"@stylistic/linebreak-style": [0],
|
||
"@stylistic/lines-around-comment": [0],
|
||
"@stylistic/max-len": [0],
|
||
"@stylistic/max-statements-per-line": [0],
|
||
"@stylistic/member-delimiter-style": [0],
|
||
"@stylistic/multiline-ternary": [0],
|
||
"@stylistic/new-parens": [0],
|
||
"@stylistic/newline-per-chained-call": [0],
|
||
"@stylistic/no-confusing-arrow": [0],
|
||
"@stylistic/no-extra-parens": [0],
|
||
"@stylistic/no-extra-semi": [0],
|
||
"@stylistic/no-floating-decimal": [0],
|
||
"@stylistic/no-mixed-operators": [0],
|
||
"@stylistic/no-mixed-spaces-and-tabs": [0],
|
||
"@stylistic/no-multi-spaces": [0],
|
||
"@stylistic/no-multiple-empty-lines": [0],
|
||
"@stylistic/no-tabs": [0],
|
||
"@stylistic/no-trailing-spaces": [0],
|
||
"@stylistic/no-whitespace-before-property": [0],
|
||
"@stylistic/nonblock-statement-body-position": [0],
|
||
"@stylistic/object-curly-newline": [0],
|
||
"@stylistic/object-curly-spacing": [0],
|
||
"@stylistic/object-property-newline": [0],
|
||
"@stylistic/one-var-declaration-per-line": [0],
|
||
"@stylistic/operator-linebreak": [0],
|
||
"@stylistic/padded-blocks": [0],
|
||
"@stylistic/quote-props": [0],
|
||
"@stylistic/quotes": [0],
|
||
"@stylistic/rest-spread-spacing": [0],
|
||
"@stylistic/semi": [0],
|
||
"@stylistic/semi-spacing": [0],
|
||
"@stylistic/semi-style": [0],
|
||
"@stylistic/space-before-blocks": [0],
|
||
"@stylistic/space-before-function-paren": [0],
|
||
"@stylistic/space-in-parens": [0],
|
||
"@stylistic/space-infix-ops": [0],
|
||
"@stylistic/space-unary-ops": [0],
|
||
"@stylistic/switch-colon-spacing": [0],
|
||
"@stylistic/template-curly-spacing": [0],
|
||
"@stylistic/template-tag-spacing": [0],
|
||
"@stylistic/ts/block-spacing": [0],
|
||
"@stylistic/ts/brace-style": [0],
|
||
"@stylistic/ts/comma-dangle": [0],
|
||
"@stylistic/ts/comma-spacing": [0],
|
||
"@stylistic/ts/func-call-spacing": [0],
|
||
"@stylistic/ts/function-call-spacing": [0],
|
||
"@stylistic/ts/indent": [0],
|
||
"@stylistic/ts/key-spacing": [0],
|
||
"@stylistic/ts/keyword-spacing": [0],
|
||
"@stylistic/ts/lines-around-comment": [0],
|
||
"@stylistic/ts/member-delimiter-style": [0],
|
||
"@stylistic/ts/no-extra-parens": [0],
|
||
"@stylistic/ts/no-extra-semi": [0],
|
||
"@stylistic/ts/object-curly-spacing": [0],
|
||
"@stylistic/ts/quotes": [0],
|
||
"@stylistic/ts/semi": [0],
|
||
"@stylistic/ts/space-before-blocks": [0],
|
||
"@stylistic/ts/space-before-function-paren": [0],
|
||
"@stylistic/ts/space-infix-ops": [0],
|
||
"@stylistic/ts/type-annotation-spacing": [0],
|
||
"@stylistic/type-annotation-spacing": [0],
|
||
"@stylistic/type-generic-spacing": [0],
|
||
"@stylistic/type-named-tuple-spacing": [0],
|
||
"@stylistic/wrap-iife": [0],
|
||
"@stylistic/wrap-regex": [0],
|
||
"@stylistic/yield-star-spacing": [0],
|
||
"@typescript-eslint/ban-ts-comment": [2],
|
||
"@typescript-eslint/block-spacing": [0],
|
||
"@typescript-eslint/brace-style": [0],
|
||
"@typescript-eslint/comma-dangle": [0],
|
||
"@typescript-eslint/comma-spacing": [0],
|
||
"@typescript-eslint/func-call-spacing": [0],
|
||
"@typescript-eslint/indent": [0],
|
||
"@typescript-eslint/key-spacing": [0],
|
||
"@typescript-eslint/keyword-spacing": [0],
|
||
"@typescript-eslint/lines-around-comment": [0],
|
||
"@typescript-eslint/member-delimiter-style": [0],
|
||
"@typescript-eslint/no-array-constructor": [2],
|
||
"@typescript-eslint/no-deprecated": [2],
|
||
"@typescript-eslint/no-duplicate-enum-values": [2],
|
||
"@typescript-eslint/no-empty-object-type": [2],
|
||
"@typescript-eslint/no-explicit-any": [2],
|
||
"@typescript-eslint/no-extra-non-null-assertion": [2],
|
||
"@typescript-eslint/no-extra-parens": [0],
|
||
"@typescript-eslint/no-extra-semi": [0],
|
||
"@typescript-eslint/no-floating-promises": [
|
||
2,
|
||
{
|
||
"allowForKnownSafeCalls": [
|
||
{
|
||
"from": "package",
|
||
"name": [
|
||
"invalidateQueries",
|
||
"refetchQueries",
|
||
"cancelQueries",
|
||
"resetQueries",
|
||
"prefetchQuery",
|
||
"prefetchInfiniteQuery"
|
||
],
|
||
"package": "@tanstack/query-core"
|
||
}
|
||
]
|
||
}
|
||
],
|
||
"@typescript-eslint/no-misused-new": [2],
|
||
"@typescript-eslint/no-namespace": [2],
|
||
"@typescript-eslint/no-non-null-asserted-optional-chain": [2],
|
||
"@typescript-eslint/no-require-imports": [2],
|
||
"@typescript-eslint/no-this-alias": [2],
|
||
"@typescript-eslint/no-unnecessary-type-constraint": [2],
|
||
"@typescript-eslint/no-unsafe-declaration-merging": [2],
|
||
"@typescript-eslint/no-unsafe-function-type": [2],
|
||
"@typescript-eslint/no-unused-expressions": [
|
||
2,
|
||
{ "allowShortCircuit": true, "allowTaggedTemplates": false, "allowTernary": true }
|
||
],
|
||
"@typescript-eslint/no-unused-vars": [0],
|
||
"@typescript-eslint/no-wrapper-object-types": [2],
|
||
"@typescript-eslint/object-curly-spacing": [0],
|
||
"@typescript-eslint/prefer-as-const": [2],
|
||
"@typescript-eslint/prefer-namespace-keyword": [2],
|
||
"@typescript-eslint/quotes": [0],
|
||
"@typescript-eslint/semi": [0],
|
||
"@typescript-eslint/space-before-blocks": [0],
|
||
"@typescript-eslint/space-before-function-paren": [0],
|
||
"@typescript-eslint/space-infix-ops": [0],
|
||
"@typescript-eslint/triple-slash-reference": [2],
|
||
"@typescript-eslint/type-annotation-spacing": [0],
|
||
"array-bracket-newline": [0],
|
||
"array-bracket-spacing": [0],
|
||
"array-element-newline": [0],
|
||
"arrow-parens": [0],
|
||
"arrow-spacing": [0],
|
||
"babel/object-curly-spacing": [0],
|
||
"babel/quotes": [0],
|
||
"babel/semi": [0],
|
||
"block-spacing": [0],
|
||
"brace-style": [0],
|
||
"comma-dangle": [0],
|
||
"comma-spacing": [0],
|
||
"comma-style": [0],
|
||
"computed-property-spacing": [0],
|
||
"constructor-super": [0],
|
||
"curly": [0, "all"],
|
||
"dot-location": [0],
|
||
"eol-last": [0],
|
||
"flowtype/boolean-style": [0],
|
||
"flowtype/delimiter-dangle": [0],
|
||
"flowtype/generic-spacing": [0],
|
||
"flowtype/object-type-curly-spacing": [0],
|
||
"flowtype/object-type-delimiter": [0],
|
||
"flowtype/quotes": [0],
|
||
"flowtype/semi": [0],
|
||
"flowtype/space-after-type-colon": [0],
|
||
"flowtype/space-before-generic-bracket": [0],
|
||
"flowtype/space-before-type-colon": [0],
|
||
"flowtype/union-intersection-spacing": [0],
|
||
"for-direction": [2],
|
||
"func-call-spacing": [0],
|
||
"function-call-argument-newline": [0],
|
||
"function-paren-newline": [0],
|
||
"generator-star": [0],
|
||
"generator-star-spacing": [0],
|
||
"getter-return": [0, { "allowImplicit": false }],
|
||
"implicit-arrow-linebreak": [0],
|
||
"indent": [0],
|
||
"indent-legacy": [0],
|
||
"jsx-quotes": [0],
|
||
"key-spacing": [0],
|
||
"keyword-spacing": [0],
|
||
"linebreak-style": [0],
|
||
"lines-around-comment": [0],
|
||
"local/button-primitive": [2],
|
||
"local/glyph-link": [2],
|
||
"local/input-primitive": [2],
|
||
"local/jsx-comment-text": [2],
|
||
"local/layers": [2],
|
||
"local/literal-a11y-text": [2],
|
||
"local/locale-format": [2],
|
||
"local/sortable-a11y": [2],
|
||
"local/surface-role": [2],
|
||
"max-len": [0],
|
||
"max-statements-per-line": [0],
|
||
"multiline-ternary": [0],
|
||
"new-parens": [0],
|
||
"newline-per-chained-call": [0],
|
||
"no-array-constructor": [0],
|
||
"no-arrow-condition": [0],
|
||
"no-async-promise-executor": [2],
|
||
"no-case-declarations": [2],
|
||
"no-class-assign": [0],
|
||
"no-comma-dangle": [0],
|
||
"no-compare-neg-zero": [2],
|
||
"no-cond-assign": [2, "except-parens"],
|
||
"no-confusing-arrow": [0],
|
||
"no-console": [2, {}],
|
||
"no-const-assign": [0],
|
||
"no-constant-binary-expression": [2],
|
||
"no-constant-condition": [2, { "checkLoops": "allExceptWhileTrue" }],
|
||
"no-control-regex": [2],
|
||
"no-debugger": [2],
|
||
"no-delete-var": [2],
|
||
"no-dupe-args": [0],
|
||
"no-dupe-class-members": [0],
|
||
"no-dupe-else-if": [2],
|
||
"no-dupe-keys": [0],
|
||
"no-duplicate-case": [2],
|
||
"no-empty": [2, { "allowEmptyCatch": false }],
|
||
"no-empty-character-class": [2],
|
||
"no-empty-pattern": [2, { "allowObjectPatternsAsParameters": false }],
|
||
"no-empty-static-block": [2],
|
||
"no-ex-assign": [2],
|
||
"no-extra-boolean-cast": [2, {}],
|
||
"no-extra-parens": [0],
|
||
"no-extra-semi": [0],
|
||
"no-fallthrough": [2, { "allowEmptyCase": false, "reportUnusedFallthroughComment": false }],
|
||
"no-floating-decimal": [0],
|
||
"no-func-assign": [0],
|
||
"no-global-assign": [2, { "exceptions": [] }],
|
||
"no-import-assign": [0],
|
||
"no-invalid-regexp": [2, {}],
|
||
"no-irregular-whitespace": [
|
||
2,
|
||
{
|
||
"skipComments": false,
|
||
"skipJSXText": false,
|
||
"skipRegExps": false,
|
||
"skipStrings": true,
|
||
"skipTemplates": false
|
||
}
|
||
],
|
||
"no-loss-of-precision": [2],
|
||
"no-misleading-character-class": [2, { "allowEscape": false }],
|
||
"no-mixed-operators": [0],
|
||
"no-mixed-spaces-and-tabs": [0],
|
||
"no-multi-spaces": [0],
|
||
"no-multiple-empty-lines": [0],
|
||
"no-new-native-nonconstructor": [0],
|
||
"no-new-symbol": [0],
|
||
"no-nonoctal-decimal-escape": [2],
|
||
"no-obj-calls": [0],
|
||
"no-octal": [2],
|
||
"no-prototype-builtins": [2],
|
||
"no-redeclare": [0, { "builtinGlobals": true }],
|
||
"no-regex-spaces": [2],
|
||
"no-reserved-keys": [0],
|
||
"no-restricted-globals": [
|
||
2,
|
||
{
|
||
"message": "Use the logged request layer: `fetch` belongs to src/lib/http.ts only — a request outside it never reaches the client log.",
|
||
"name": "fetch"
|
||
},
|
||
{
|
||
"message": "Use the logged request layer: `XMLHttpRequest` belongs to src/lib/api/core.ts only — a request outside it never reaches the client log.",
|
||
"name": "XMLHttpRequest"
|
||
},
|
||
{ "message": "No `WebSocket` without a logged owner module.", "name": "WebSocket" },
|
||
{ "message": "No `EventSource` without a logged owner module.", "name": "EventSource" }
|
||
],
|
||
"no-restricted-properties": [
|
||
2,
|
||
{ "message": "Use lib/http.ts `send`.", "object": "window", "property": "fetch" },
|
||
{ "message": "Use lib/http.ts `send`.", "object": "globalThis", "property": "fetch" },
|
||
{
|
||
"message": "Use lib/api/core.ts `beacon`.",
|
||
"object": "navigator",
|
||
"property": "sendBeacon"
|
||
}
|
||
],
|
||
"no-restricted-syntax": [
|
||
2,
|
||
{
|
||
"message": "fetchNextPage() cancels and re-sends a page already loading: call fetchNextPage(NEXT_PAGE) (lib/nextPage.ts).",
|
||
"selector": "CallExpression[arguments.length=0]:matches([callee.name='fetchNextPage'], [callee.property.name='fetchNextPage'])"
|
||
},
|
||
{
|
||
"message": "`enabled:hover:` misses a focused FocusSafeButton (aria-disabled): use `act:hover:` (index.css).",
|
||
"selector": "Literal[value=/(^|\\s)enabled:hover:/]"
|
||
},
|
||
{
|
||
"message": "`enabled:hover:` misses a focused FocusSafeButton (aria-disabled): use `act:hover:` (index.css).",
|
||
"selector": "TemplateElement[value.raw=/(^|\\s)enabled:hover:/]"
|
||
},
|
||
{
|
||
"message": "a class split by an interpolation (`${x}hover:…`): Tailwind never sees it whole and emits no CSS; write the class literally (a shared guard is the `act:` variant).",
|
||
"selector": "TemplateLiteral > TemplateElement:not(:first-child)[value.raw=/^([a-z][\\w-]*:[\\w\\[!-]|:[a-z\\[!])/]"
|
||
},
|
||
{
|
||
"message": "No native <select>: use ui/SegmentedToggle for a short fixed set, ui/FilterSelect for a longer one or a user's own list.",
|
||
"selector": "JSXOpeningElement[name.name='select']"
|
||
},
|
||
{
|
||
"message": "No bare <img>: use ui/RemoteImage — no Referer, a retry past the browser's negative cache, and a fallback instead of the broken-image mark.",
|
||
"selector": "JSXOpeningElement[name.name='img']"
|
||
},
|
||
{
|
||
"message": "px-frozen font size doesn't scale with --font-scale: use text-2xs/text-3xs or text-[Nrem], not text-[Npx].",
|
||
"selector": "Literal[value=/text-\\[[\\d.]+px\\]/]"
|
||
},
|
||
{
|
||
"message": "px-frozen font size doesn't scale with --font-scale: use text-2xs/text-3xs or text-[Nrem], not text-[Npx].",
|
||
"selector": "TemplateElement[value.raw=/text-\\[[\\d.]+px\\]/]"
|
||
},
|
||
{
|
||
"message": "px-frozen box size doesn't scale with --font-scale: use the spacing scale (w-37.5, max-w-60) or rem (minmax(9.375rem,1fr)), not w-[Npx] or minmax(Npx,…).",
|
||
"selector": "Literal[value=/(^|[\\s:])(min-|max-)?(w|h|size|left|right|top|bottom|inset|gap|p[xytrbl]?|m[xytrbl]?)-\\[[\\d.]+px\\]|minmax\\([\\d.]+px,/]"
|
||
},
|
||
{
|
||
"message": "px-frozen box size doesn't scale with --font-scale: use the spacing scale (w-37.5, max-w-60) or rem (minmax(9.375rem,1fr)), not w-[Npx] or minmax(Npx,…).",
|
||
"selector": "TemplateElement[value.raw=/(^|[\\s:])(min-|max-)?(w|h|size|left|right|top|bottom|inset|gap|p[xytrbl]?|m[xytrbl]?)-\\[[\\d.]+px\\]|minmax\\([\\d.]+px,/]"
|
||
},
|
||
{
|
||
"message": "hover by a colour swap: use the B3 layer (`state-layer`, Pressable `layer`, `state-filled` on a fill), not hover:bg-card / bg-border / opacity-N.",
|
||
"selector": "Literal[value=/(^|[\\s:])hover:(bg-(card|border|surface|muted)(?![\\w-])|bg-\\(--(hover|press)\\)|opacity-)/]"
|
||
},
|
||
{
|
||
"message": "hover by a colour swap: use the B3 layer (`state-layer`, Pressable `layer`, `state-filled` on a fill), not hover:bg-card / bg-border / opacity-N.",
|
||
"selector": "TemplateElement[value.raw=/(^|[\\s:])hover:(bg-(card|border|surface|muted)(?![\\w-])|bg-\\(--(hover|press)\\)|opacity-)/]"
|
||
},
|
||
{
|
||
"message": "a palette colour: use a semantic token (text-rating, bg-live, bg-media-chrome, text-danger, …; DESIGN.md Tokens), not text-amber-500.",
|
||
"selector": "Literal[value=/(^|[\\s:])(text|bg|border|fill|stroke|from|via|to|ring|outline|decoration|divide|caret|shadow)-(red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|slate|gray|zinc|neutral|stone)-\\d/]"
|
||
},
|
||
{
|
||
"message": "a palette colour: use a semantic token (text-rating, bg-live, bg-media-chrome, text-danger, …; DESIGN.md Tokens), not text-amber-500.",
|
||
"selector": "TemplateElement[value.raw=/(^|[\\s:])(text|bg|border|fill|stroke|from|via|to|ring|outline|decoration|divide|caret|shadow)-(red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|slate|gray|zinc|neutral|stone)-\\d/]"
|
||
}
|
||
],
|
||
"no-self-assign": [2, { "props": true }],
|
||
"no-setter-return": [0],
|
||
"no-shadow-restricted-names": [2, { "reportGlobalThis": false }],
|
||
"no-space-before-semi": [0],
|
||
"no-spaced-func": [0],
|
||
"no-sparse-arrays": [2],
|
||
"no-tabs": [0],
|
||
"no-this-before-super": [0],
|
||
"no-trailing-spaces": [0],
|
||
"no-undef": [0, { "typeof": false }],
|
||
"no-unexpected-multiline": [0],
|
||
"no-unreachable": [0],
|
||
"no-unsafe-finally": [2],
|
||
"no-unsafe-negation": [0, { "enforceForOrderingRelations": false }],
|
||
"no-unsafe-optional-chaining": [2, { "disallowArithmeticOperators": false }],
|
||
"no-unused-expressions": [
|
||
0,
|
||
{
|
||
"allowShortCircuit": false,
|
||
"allowTaggedTemplates": false,
|
||
"allowTernary": false,
|
||
"enforceForJSX": false,
|
||
"ignoreDirectives": false
|
||
}
|
||
],
|
||
"no-unused-labels": [2],
|
||
"no-unused-private-class-members": [2],
|
||
"no-unused-vars": [0],
|
||
"no-useless-backreference": [2],
|
||
"no-useless-catch": [2],
|
||
"no-useless-escape": [2, { "allowRegexCharacters": [] }],
|
||
"no-var": [2],
|
||
"no-whitespace-before-property": [0],
|
||
"no-with": [0],
|
||
"no-wrap-func": [0],
|
||
"nonblock-statement-body-position": [0],
|
||
"object-curly-newline": [0],
|
||
"object-curly-spacing": [0],
|
||
"object-property-newline": [0],
|
||
"one-var-declaration-per-line": [0],
|
||
"operator-linebreak": [0],
|
||
"padded-blocks": [0],
|
||
"prefer-const": [2, { "destructuring": "any", "ignoreReadBeforeAssign": false }],
|
||
"prefer-rest-params": [2],
|
||
"prefer-spread": [2],
|
||
"quote-props": [0],
|
||
"quotes": [0],
|
||
"react-hooks/capitalized-calls": [2],
|
||
"react-hooks/component-hook-factories": [2],
|
||
"react-hooks/config": [2],
|
||
"react-hooks/error-boundaries": [2],
|
||
"react-hooks/exhaustive-deps": [1],
|
||
"react-hooks/gating": [2],
|
||
"react-hooks/globals": [2],
|
||
"react-hooks/hooks": [2],
|
||
"react-hooks/immutability": [2],
|
||
"react-hooks/incompatible-library": [1],
|
||
"react-hooks/invariant": [2],
|
||
"react-hooks/memo-dependencies": [2],
|
||
"react-hooks/no-deriving-state-in-effects": [2],
|
||
"react-hooks/preserve-manual-memoization": [2],
|
||
"react-hooks/purity": [2],
|
||
"react-hooks/refs": [2],
|
||
"react-hooks/rules-of-hooks": [2],
|
||
"react-hooks/set-state-in-effect": [2],
|
||
"react-hooks/set-state-in-render": [2],
|
||
"react-hooks/static-components": [2],
|
||
"react-hooks/syntax": [2],
|
||
"react-hooks/todo": [2],
|
||
"react-hooks/unsupported-syntax": [1],
|
||
"react-hooks/use-memo": [2],
|
||
"react-hooks/void-use-memo": [2],
|
||
"react-refresh/only-export-components": [1, { "allowConstantExport": true }],
|
||
"react/jsx-child-element-spacing": [0],
|
||
"react/jsx-closing-bracket-location": [0],
|
||
"react/jsx-closing-tag-location": [0],
|
||
"react/jsx-curly-newline": [0],
|
||
"react/jsx-curly-spacing": [0],
|
||
"react/jsx-equals-spacing": [0],
|
||
"react/jsx-first-prop-new-line": [0],
|
||
"react/jsx-indent": [0],
|
||
"react/jsx-indent-props": [0],
|
||
"react/jsx-max-props-per-line": [0],
|
||
"react/jsx-newline": [0],
|
||
"react/jsx-one-expression-per-line": [0],
|
||
"react/jsx-props-no-multi-spaces": [0],
|
||
"react/jsx-space-before-closing": [0],
|
||
"react/jsx-tag-spacing": [0],
|
||
"react/jsx-wrap-multilines": [0],
|
||
"require-yield": [2],
|
||
"rest-spread-spacing": [0],
|
||
"semi": [0],
|
||
"semi-spacing": [0],
|
||
"semi-style": [0],
|
||
"space-after-function-name": [0],
|
||
"space-after-keywords": [0],
|
||
"space-before-blocks": [0],
|
||
"space-before-function-paren": [0],
|
||
"space-before-function-parentheses": [0],
|
||
"space-before-keywords": [0],
|
||
"space-in-brackets": [0],
|
||
"space-in-parens": [0],
|
||
"space-infix-ops": [0],
|
||
"space-return-throw-case": [0],
|
||
"space-unary-ops": [0],
|
||
"space-unary-word-ops": [0],
|
||
"standard/array-bracket-even-spacing": [0],
|
||
"standard/computed-property-even-spacing": [0],
|
||
"standard/object-curly-even-spacing": [0],
|
||
"switch-colon-spacing": [0],
|
||
"template-curly-spacing": [0],
|
||
"template-tag-spacing": [0],
|
||
"unicorn/empty-brace-spaces": [0],
|
||
"unicorn/no-nested-ternary": [0],
|
||
"unicorn/number-literal-case": [0],
|
||
"unicorn/template-indent": [0],
|
||
"use-isnan": [2, { "enforceForIndexOf": false, "enforceForSwitchCase": true }],
|
||
"valid-typeof": [2, { "requireStringLiterals": false }],
|
||
"vue/array-bracket-newline": [0],
|
||
"vue/array-bracket-spacing": [0],
|
||
"vue/array-element-newline": [0],
|
||
"vue/arrow-spacing": [0],
|
||
"vue/block-spacing": [0],
|
||
"vue/block-tag-newline": [0],
|
||
"vue/brace-style": [0],
|
||
"vue/comma-dangle": [0],
|
||
"vue/comma-spacing": [0],
|
||
"vue/comma-style": [0],
|
||
"vue/dot-location": [0],
|
||
"vue/func-call-spacing": [0],
|
||
"vue/html-closing-bracket-newline": [0],
|
||
"vue/html-closing-bracket-spacing": [0],
|
||
"vue/html-end-tags": [0],
|
||
"vue/html-indent": [0],
|
||
"vue/html-quotes": [0],
|
||
"vue/html-self-closing": [0],
|
||
"vue/key-spacing": [0],
|
||
"vue/keyword-spacing": [0],
|
||
"vue/max-attributes-per-line": [0],
|
||
"vue/max-len": [0],
|
||
"vue/multiline-html-element-content-newline": [0],
|
||
"vue/multiline-ternary": [0],
|
||
"vue/mustache-interpolation-spacing": [0],
|
||
"vue/no-extra-parens": [0],
|
||
"vue/no-multi-spaces": [0],
|
||
"vue/no-spaces-around-equal-signs-in-attribute": [0],
|
||
"vue/object-curly-newline": [0],
|
||
"vue/object-curly-spacing": [0],
|
||
"vue/object-property-newline": [0],
|
||
"vue/operator-linebreak": [0],
|
||
"vue/quote-props": [0],
|
||
"vue/script-indent": [0],
|
||
"vue/singleline-html-element-content-newline": [0],
|
||
"vue/space-in-parens": [0],
|
||
"vue/space-infix-ops": [0],
|
||
"vue/space-unary-ops": [0],
|
||
"vue/template-curly-spacing": [0],
|
||
"wrap-iife": [0],
|
||
"wrap-regex": [0],
|
||
"yield-star-spacing": [0]
|
||
},
|
||
"settings": null
|
||
},
|
||
"src tests": {
|
||
"languageOptions": {
|
||
"ecmaVersion": 2022,
|
||
"globals": "1169 names, sha1 aee621727a03",
|
||
"parser": "typescript-eslint/parser",
|
||
"parserOptions": { "projectService": true, "tsconfigRootDir": "." },
|
||
"sourceType": "module"
|
||
},
|
||
"linterOptions": { "reportUnusedDisableDirectives": 2 },
|
||
"notApplied": [],
|
||
"plugins": [
|
||
"@",
|
||
"@eslint-community/eslint-comments",
|
||
"@typescript-eslint",
|
||
"local",
|
||
"react-hooks",
|
||
"react-refresh",
|
||
"vitest"
|
||
],
|
||
"rules": {
|
||
"local/glyph-link": [0],
|
||
"local/input-primitive": [0],
|
||
"local/literal-a11y-text": [0],
|
||
"local/surface-role": [0],
|
||
"no-restricted-globals": [
|
||
0,
|
||
{
|
||
"message": "Use the logged request layer: `fetch` belongs to src/lib/http.ts only — a request outside it never reaches the client log.",
|
||
"name": "fetch"
|
||
},
|
||
{
|
||
"message": "Use the logged request layer: `XMLHttpRequest` belongs to src/lib/api/core.ts only — a request outside it never reaches the client log.",
|
||
"name": "XMLHttpRequest"
|
||
},
|
||
{ "message": "No `WebSocket` without a logged owner module.", "name": "WebSocket" },
|
||
{ "message": "No `EventSource` without a logged owner module.", "name": "EventSource" }
|
||
],
|
||
"no-restricted-properties": [
|
||
0,
|
||
{ "message": "Use lib/http.ts `send`.", "object": "window", "property": "fetch" },
|
||
{ "message": "Use lib/http.ts `send`.", "object": "globalThis", "property": "fetch" },
|
||
{
|
||
"message": "Use lib/api/core.ts `beacon`.",
|
||
"object": "navigator",
|
||
"property": "sendBeacon"
|
||
}
|
||
],
|
||
"vitest/no-disabled-tests": [2],
|
||
"vitest/no-focused-tests": [2, { "fixable": true }]
|
||
},
|
||
"settings": null
|
||
},
|
||
"src/lib": {
|
||
"languageOptions": {
|
||
"ecmaVersion": 2022,
|
||
"globals": "1169 names, sha1 aee621727a03",
|
||
"parser": "typescript-eslint/parser",
|
||
"parserOptions": { "projectService": true, "tsconfigRootDir": "." },
|
||
"sourceType": "module"
|
||
},
|
||
"linterOptions": { "reportUnusedDisableDirectives": 2 },
|
||
"notApplied": [],
|
||
"plugins": [
|
||
"@",
|
||
"@eslint-community/eslint-comments",
|
||
"@typescript-eslint",
|
||
"local",
|
||
"react-hooks",
|
||
"react-refresh"
|
||
],
|
||
"rules": {
|
||
"no-restricted-imports": [
|
||
2,
|
||
{
|
||
"patterns": [
|
||
{
|
||
"message": "lib/ sits below components/: it may not import from components/, not even a type. Move the shared piece down into lib/.",
|
||
"regex": "^(\\.\\./)+components(/|$)"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
},
|
||
"settings": null
|
||
},
|
||
"tool tests": {
|
||
"languageOptions": {
|
||
"ecmaVersion": 2022,
|
||
"globals": "82 names, sha1 29467d490e30",
|
||
"parser": "typescript-eslint/parser",
|
||
"parserOptions": { "projectService": true, "tsconfigRootDir": "." },
|
||
"sourceType": "module"
|
||
},
|
||
"linterOptions": { "reportUnusedDisableDirectives": 2 },
|
||
"notApplied": [
|
||
"local/button-primitive",
|
||
"local/glyph-link",
|
||
"local/input-primitive",
|
||
"local/jsx-comment-text",
|
||
"local/layers",
|
||
"local/literal-a11y-text",
|
||
"local/locale-format",
|
||
"local/sortable-a11y",
|
||
"local/surface-role",
|
||
"no-console",
|
||
"no-restricted-globals",
|
||
"no-restricted-properties",
|
||
"no-restricted-syntax",
|
||
"react-hooks/capitalized-calls",
|
||
"react-hooks/component-hook-factories",
|
||
"react-hooks/config",
|
||
"react-hooks/error-boundaries",
|
||
"react-hooks/exhaustive-deps",
|
||
"react-hooks/gating",
|
||
"react-hooks/globals",
|
||
"react-hooks/hooks",
|
||
"react-hooks/immutability",
|
||
"react-hooks/incompatible-library",
|
||
"react-hooks/invariant",
|
||
"react-hooks/memo-dependencies",
|
||
"react-hooks/no-deriving-state-in-effects",
|
||
"react-hooks/preserve-manual-memoization",
|
||
"react-hooks/purity",
|
||
"react-hooks/refs",
|
||
"react-hooks/rules-of-hooks",
|
||
"react-hooks/set-state-in-effect",
|
||
"react-hooks/set-state-in-render",
|
||
"react-hooks/static-components",
|
||
"react-hooks/syntax",
|
||
"react-hooks/todo",
|
||
"react-hooks/unsupported-syntax",
|
||
"react-hooks/use-memo",
|
||
"react-hooks/void-use-memo",
|
||
"react-refresh/only-export-components"
|
||
],
|
||
"plugins": ["@", "@eslint-community/eslint-comments", "@typescript-eslint", "vitest"],
|
||
"rules": {
|
||
"@typescript-eslint/no-unused-expressions": [
|
||
2,
|
||
{ "allowShortCircuit": false, "allowTaggedTemplates": false, "allowTernary": false }
|
||
],
|
||
"vitest/no-disabled-tests": [2],
|
||
"vitest/no-focused-tests": [2, { "fixable": true }]
|
||
},
|
||
"settings": null
|
||
},
|
||
"ts configs": {
|
||
"languageOptions": {
|
||
"ecmaVersion": 2022,
|
||
"globals": "82 names, sha1 29467d490e30",
|
||
"parser": "typescript-eslint/parser",
|
||
"parserOptions": { "projectService": true, "tsconfigRootDir": "." },
|
||
"sourceType": "module"
|
||
},
|
||
"linterOptions": { "reportUnusedDisableDirectives": 2 },
|
||
"notApplied": [
|
||
"local/button-primitive",
|
||
"local/glyph-link",
|
||
"local/input-primitive",
|
||
"local/jsx-comment-text",
|
||
"local/layers",
|
||
"local/literal-a11y-text",
|
||
"local/locale-format",
|
||
"local/sortable-a11y",
|
||
"local/surface-role",
|
||
"no-console",
|
||
"no-restricted-globals",
|
||
"no-restricted-properties",
|
||
"no-restricted-syntax",
|
||
"react-hooks/capitalized-calls",
|
||
"react-hooks/component-hook-factories",
|
||
"react-hooks/config",
|
||
"react-hooks/error-boundaries",
|
||
"react-hooks/exhaustive-deps",
|
||
"react-hooks/gating",
|
||
"react-hooks/globals",
|
||
"react-hooks/hooks",
|
||
"react-hooks/immutability",
|
||
"react-hooks/incompatible-library",
|
||
"react-hooks/invariant",
|
||
"react-hooks/memo-dependencies",
|
||
"react-hooks/no-deriving-state-in-effects",
|
||
"react-hooks/preserve-manual-memoization",
|
||
"react-hooks/purity",
|
||
"react-hooks/refs",
|
||
"react-hooks/rules-of-hooks",
|
||
"react-hooks/set-state-in-effect",
|
||
"react-hooks/set-state-in-render",
|
||
"react-hooks/static-components",
|
||
"react-hooks/syntax",
|
||
"react-hooks/todo",
|
||
"react-hooks/unsupported-syntax",
|
||
"react-hooks/use-memo",
|
||
"react-hooks/void-use-memo",
|
||
"react-refresh/only-export-components"
|
||
],
|
||
"plugins": ["@", "@eslint-community/eslint-comments", "@typescript-eslint"],
|
||
"rules": {
|
||
"@typescript-eslint/no-unused-expressions": [
|
||
2,
|
||
{ "allowShortCircuit": false, "allowTaggedTemplates": false, "allowTernary": false }
|
||
]
|
||
},
|
||
"settings": null
|
||
},
|
||
"ui": {
|
||
"languageOptions": {
|
||
"ecmaVersion": 2022,
|
||
"globals": "1169 names, sha1 aee621727a03",
|
||
"parser": "typescript-eslint/parser",
|
||
"parserOptions": { "projectService": true, "tsconfigRootDir": "." },
|
||
"sourceType": "module"
|
||
},
|
||
"linterOptions": { "reportUnusedDisableDirectives": 2 },
|
||
"notApplied": [],
|
||
"plugins": [
|
||
"@",
|
||
"@eslint-community/eslint-comments",
|
||
"@typescript-eslint",
|
||
"local",
|
||
"react-hooks",
|
||
"react-refresh"
|
||
],
|
||
"rules": {
|
||
"no-restricted-imports": [
|
||
2,
|
||
{
|
||
"patterns": [
|
||
{
|
||
"message": "components/ui/ is domain-free: a platform component may not import a feature component. Invert the dependency (take a prop/render-prop) or keep the component outside ui/.",
|
||
"regex": "^\\.\\./(?!\\.\\./)"
|
||
},
|
||
{
|
||
"message": "components/ui/ is domain-free: a platform component may not import a feature component. Invert the dependency (take a prop/render-prop) or keep the component outside ui/.",
|
||
"regex": "^\\.\\./\\.\\./components/(?!ui/)"
|
||
},
|
||
{
|
||
"message": "components/ui/ is domain-free: a platform component may not call a domain API slice. Take the data as props.",
|
||
"regex": "^\\.\\./\\.\\./lib/api(/|$)"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
},
|
||
"settings": null
|
||
},
|
||
"ui tests": {
|
||
"languageOptions": {
|
||
"ecmaVersion": 2022,
|
||
"globals": "1169 names, sha1 aee621727a03",
|
||
"parser": "typescript-eslint/parser",
|
||
"parserOptions": { "projectService": true, "tsconfigRootDir": "." },
|
||
"sourceType": "module"
|
||
},
|
||
"linterOptions": { "reportUnusedDisableDirectives": 2 },
|
||
"notApplied": [],
|
||
"plugins": [
|
||
"@",
|
||
"@eslint-community/eslint-comments",
|
||
"@typescript-eslint",
|
||
"local",
|
||
"react-hooks",
|
||
"react-refresh",
|
||
"vitest"
|
||
],
|
||
"rules": {
|
||
"local/glyph-link": [0],
|
||
"local/input-primitive": [0],
|
||
"local/literal-a11y-text": [0],
|
||
"local/surface-role": [0],
|
||
"no-restricted-globals": [
|
||
0,
|
||
{
|
||
"message": "Use the logged request layer: `fetch` belongs to src/lib/http.ts only — a request outside it never reaches the client log.",
|
||
"name": "fetch"
|
||
},
|
||
{
|
||
"message": "Use the logged request layer: `XMLHttpRequest` belongs to src/lib/api/core.ts only — a request outside it never reaches the client log.",
|
||
"name": "XMLHttpRequest"
|
||
},
|
||
{ "message": "No `WebSocket` without a logged owner module.", "name": "WebSocket" },
|
||
{ "message": "No `EventSource` without a logged owner module.", "name": "EventSource" }
|
||
],
|
||
"no-restricted-imports": [
|
||
2,
|
||
{
|
||
"patterns": [
|
||
{
|
||
"message": "components/ui/ is domain-free: a platform component may not import a feature component. Invert the dependency (take a prop/render-prop) or keep the component outside ui/.",
|
||
"regex": "^\\.\\./(?!\\.\\./)"
|
||
},
|
||
{
|
||
"message": "components/ui/ is domain-free: a platform component may not import a feature component. Invert the dependency (take a prop/render-prop) or keep the component outside ui/.",
|
||
"regex": "^\\.\\./\\.\\./components/(?!ui/)"
|
||
},
|
||
{
|
||
"message": "components/ui/ is domain-free: a platform component may not call a domain API slice. Take the data as props.",
|
||
"regex": "^\\.\\./\\.\\./lib/api(/|$)"
|
||
}
|
||
]
|
||
}
|
||
],
|
||
"no-restricted-properties": [
|
||
0,
|
||
{ "message": "Use lib/http.ts `send`.", "object": "window", "property": "fetch" },
|
||
{ "message": "Use lib/http.ts `send`.", "object": "globalThis", "property": "fetch" },
|
||
{
|
||
"message": "Use lib/api/core.ts `beacon`.",
|
||
"object": "navigator",
|
||
"property": "sendBeacon"
|
||
}
|
||
],
|
||
"vitest/no-disabled-tests": [2],
|
||
"vitest/no-focused-tests": [2, { "fixable": true }]
|
||
},
|
||
"settings": null
|
||
}
|
||
},
|
||
"lanes": {
|
||
"backend/Dockerfile.test": [
|
||
"FROM python:3.12-slim",
|
||
"WORKDIR /app",
|
||
"RUN apt-get update \\",
|
||
" && apt-get install -y --no-install-recommends ffmpeg \\",
|
||
" && rm -rf /var/lib/apt/lists/*",
|
||
"COPY requirements.lock requirements-dev.txt ./",
|
||
"RUN pip install --no-cache-dir -r requirements.lock -r requirements-dev.txt"
|
||
],
|
||
"backend/docker-compose.test.yml": [
|
||
"services:",
|
||
" testdb:",
|
||
" image: postgres:16-alpine",
|
||
" environment:",
|
||
" POSTGRES_USER: siftlode",
|
||
" POSTGRES_PASSWORD: siftlode",
|
||
" POSTGRES_DB: siftlode_test",
|
||
" tmpfs:",
|
||
" - /var/lib/postgresql/data # ephemeral + fast; the schema is rebuilt per run",
|
||
" command: [\"postgres\", \"-c\", \"max_connections=300\"]",
|
||
" healthcheck:",
|
||
" test: [\"CMD-SHELL\", \"pg_isready -U siftlode -d siftlode_test\"]",
|
||
" interval: 1s",
|
||
" timeout: 3s",
|
||
" retries: 30",
|
||
" test:",
|
||
" build:",
|
||
" context: .",
|
||
" dockerfile: Dockerfile.test",
|
||
" depends_on:",
|
||
" testdb:",
|
||
" condition: service_healthy",
|
||
" environment:",
|
||
" DATABASE_URL: postgresql+psycopg://siftlode:siftlode@testdb:5432/siftlode_test",
|
||
" SIFTLODE_TEST_REQUIRE_DB: \"1\"",
|
||
" volumes:",
|
||
" - ./:/app",
|
||
" working_dir: /app",
|
||
" command: [\"python\", \"-m\", \"pytest\", \"-n\", \"auto\"]"
|
||
],
|
||
"frontend/.prettierignore": [
|
||
"dist",
|
||
"node_modules",
|
||
"playwright-report",
|
||
"test-results",
|
||
"e2e/.auth",
|
||
"package-lock.json"
|
||
],
|
||
"frontend/package.json scripts": {
|
||
"build": "npm run typecheck:app && npm run typecheck:node && vite build",
|
||
"dev": "vite",
|
||
"e2e": "playwright test",
|
||
"format": "prettier --write .",
|
||
"format:check": "prettier --check .",
|
||
"knip": "knip",
|
||
"lint": "eslint . --max-warnings 0",
|
||
"preview": "vite preview",
|
||
"test": "vitest run",
|
||
"test:watch": "vitest",
|
||
"typecheck": "npm run typecheck:app && npm run typecheck:node && npm run typecheck:e2e",
|
||
"typecheck:app": "tsc --noEmit -p tsconfig.json",
|
||
"typecheck:e2e": "tsc --noEmit -p e2e/tsconfig.json",
|
||
"typecheck:node": "tsc -p tsconfig.node.json"
|
||
},
|
||
"frontend/vitest.config.ts": [
|
||
"import { defineConfig } from \"vitest/config\";",
|
||
"import { reactWithCompiler } from \"./reactPlugin\";",
|
||
"export default defineConfig({",
|
||
" plugins: [reactWithCompiler()],",
|
||
" test: {",
|
||
" include: [\"src/**/*.test.{ts,tsx}\", \"*.test.ts\"],",
|
||
" environment: \"node\",",
|
||
" coverage: {",
|
||
" provider: \"v8\",",
|
||
" include: [\"src/**/*.{ts,tsx}\"],",
|
||
" exclude: [\"src/**/*.test.{ts,tsx}\", \"src/**/*.testkit.ts\", \"src/**/*.d.ts\"],",
|
||
" reporter: [\"text-summary\"],",
|
||
" },",
|
||
" },",
|
||
"});"
|
||
],
|
||
"scripts/check.sh": [
|
||
"set -u",
|
||
"cd \"$(dirname \"$0\")/..\" || exit 2",
|
||
"mode=all",
|
||
"brief=0",
|
||
"list=0",
|
||
"only=\"\"",
|
||
"usage() { echo 'usage: scripts/check.sh [lint] [--brief] [--list | --only \"<lane>\"]' >&2; exit 2; }",
|
||
"while [ $# -gt 0 ]; do",
|
||
" case \"$1\" in",
|
||
" all | lint) mode=\"$1\" ;;",
|
||
" --brief) brief=1 ;;",
|
||
" --list) list=1 ;;",
|
||
" --only) [ $# -ge 2 ] || usage; only=\"$2\"; shift ;;",
|
||
" esac",
|
||
" shift",
|
||
"done",
|
||
"[ \"$list\" = 1 ] && [ -n \"$only\" ] && usage",
|
||
"export MSYS_NO_PATHCONV=1 # Git Bash: do not rewrite the in-container paths below",
|
||
"TESTCOMPOSE=backend/docker-compose.test.yml",
|
||
"failed=()",
|
||
"timings=()",
|
||
"ran=0",
|
||
"ms() { date +%s%3N; }",
|
||
"secs() { awk -v t=\"$1\" 'BEGIN { printf \"%.1f\", t / 1000 }'; }",
|
||
"took=\"\"",
|
||
"timed() { # timed <name> <ok|red> <start-ms> — sets $took (no subshell: it must add to $timings)",
|
||
" took=\"$(secs $(($(ms) - $3)))\"",
|
||
" timings+=(\"$(printf '%8s s %s' \"$took\" \"$1\")$([ \"$2\" = red ] && echo ' (red)')\")",
|
||
" if [ -n \"${GATE_TIMES_FILE:-}\" ]; then",
|
||
" printf '%s\\t%s\\t%s\\t%s\\t%s\\t%s\\n' \"$(date -u +%FT%TZ)\" \"${only:+only}${only:-$mode}\" \"$1\" \"$took\" \"$2\" \\",
|
||
" \"$(git rev-parse --short 'HEAD^{tree}' 2>/dev/null)\" >>\"$GATE_TIMES_FILE\"",
|
||
" fi",
|
||
" return 0",
|
||
"}",
|
||
"lane() { # lane <name> <command…> — run it, remember a failure, never stop",
|
||
" local name=\"$1\" t0 result=ok",
|
||
" shift",
|
||
" if [ \"$list\" = 1 ]; then echo \"$name\"; return 0; fi",
|
||
" [ -n \"$only\" ] && [ \"$name\" != \"$only\" ] && return 0",
|
||
" ran=$((ran + 1))",
|
||
" t0=\"$(ms)\"",
|
||
" if [ \"$brief\" = 0 ]; then",
|
||
" echo \">> $name…\"",
|
||
" \"$@\" || { failed+=(\"$name\"); result=red; }",
|
||
" timed \"$name\" \"$result\" \"$t0\"",
|
||
" echo \"<< $name: $took s\"",
|
||
" return 0",
|
||
" fi",
|
||
" local out",
|
||
" out=\"$(mktemp)\"",
|
||
" if \"$@\" >\"$out\" 2>&1; then",
|
||
" timed \"$name\" ok \"$t0\"",
|
||
" echo \">> $name: ok ($took s)\"",
|
||
" else",
|
||
" failed+=(\"$name\")",
|
||
" timed \"$name\" red \"$t0\"",
|
||
" echo \">> $name: FAILED ($took s)\"",
|
||
" brief_excerpt \"$out\"",
|
||
" fi",
|
||
" rm -f \"$out\"",
|
||
"}",
|
||
"brief_excerpt() {",
|
||
" local log hits",
|
||
" log=\"$(grep -v -E '^testdb-[0-9]+ ' \"$1\")\"",
|
||
" if [ \"$(printf '%s\\n' \"$log\" | wc -l)\" -le 60 ]; then",
|
||
" printf '%s\\n' \"$log\"",
|
||
" return",
|
||
" fi",
|
||
" hits=\"$(printf '%s\\n' \"$log\" |",
|
||
" grep -E -i -B 1 -A 2 -m 20 'error|fail|✗|×|warn|would reformat|unused|vulnerab|found [0-9]')\"",
|
||
" if [ -n \"$hits\" ]; then printf '%s\\n' \"$hits\"; else printf '%s\\n' \"$log\" | tail -n 20; fi",
|
||
"}",
|
||
"ruff_pinned() {",
|
||
" local pin host",
|
||
" pin=\"$(sed -n 's/^ruff==\\([0-9.]*\\).*/\\1/p' backend/requirements-dev.txt)\"",
|
||
" host=\"$(python -m ruff --version 2>/dev/null | awk '{print $2}')\"",
|
||
" [ -n \"$pin\" ] && [ \"$host\" = \"$pin\" ] && return 0",
|
||
" echo \"ruff ${host:-missing} on the host, ${pin:-nothing} pinned — python -m pip install ruff==$pin\" >&2",
|
||
" return 1",
|
||
"}",
|
||
"ruff_check() { ruff_pinned && python -m ruff check --config backend/ruff.toml backend scripts; }",
|
||
"ruff_format() { ruff_pinned && python -m ruff format --check --config backend/ruff.toml backend scripts; }",
|
||
"SHELLCHECK_PIN=0.11.0",
|
||
"shellcheck_lane() {",
|
||
" local host",
|
||
" host=\"$(shellcheck --version 2>/dev/null | sed -n 's/^version: //p')\"",
|
||
" if [ \"$host\" != \"$SHELLCHECK_PIN\" ]; then",
|
||
" echo \"shellcheck ${host:-missing} on the host, $SHELLCHECK_PIN pinned — https://github.com/koalaman/shellcheck/releases\" >&2",
|
||
" return 1",
|
||
" fi",
|
||
" git ls-files -z -- '*.sh' | xargs -0 shellcheck",
|
||
"}",
|
||
"basedpyright() {",
|
||
" local before",
|
||
" before=\"$(git hash-object backend/.basedpyright/baseline.json)\"",
|
||
" docker compose -f \"$TESTCOMPOSE\" run --rm --no-deps --build test \\",
|
||
" python -m basedpyright --project pyrightconfig.json || return 1",
|
||
" if [ \"$(git hash-object backend/.basedpyright/baseline.json)\" != \"$before\" ]; then",
|
||
" echo \"this run rewrote backend/.basedpyright/baseline.json (errors were fixed) — commit it\" >&2",
|
||
" return 1",
|
||
" fi",
|
||
" git diff HEAD --quiet -- backend/.basedpyright/baseline.json && return 0",
|
||
" echo \"backend/.basedpyright/baseline.json is uncommitted — commit it\" >&2",
|
||
" return 1",
|
||
"}",
|
||
"frontend() { (cd frontend && \"$@\"); }",
|
||
"gate_t0=\"$(ms)\"",
|
||
"lane \"ruff\" ruff_check",
|
||
"lane \"ruff format --check\" ruff_format",
|
||
"lane \"shellcheck (every shell script)\" shellcheck_lane",
|
||
"lane \"basedpyright (new errors only)\" basedpyright",
|
||
"lane \"lock check (frontend/node_modules == package-lock.json)\" node scripts/check-node-modules.mjs",
|
||
"lane \"tsc (src + configs + e2e)\" frontend npm run --silent typecheck",
|
||
"lane \"eslint (warnings fail too)\" frontend npm run --silent lint",
|
||
"lane \"prettier --check\" frontend npm run --silent format:check",
|
||
"lane \"knip (unused files, exports, dependencies)\" frontend npm run --silent knip",
|
||
"pytest_lane() { # the full suite against a throwaway Postgres; --pull keeps the base image current",
|
||
" docker compose -f \"$TESTCOMPOSE\" build --pull -q test || return 1",
|
||
" local rc=0",
|
||
" docker compose -f \"$TESTCOMPOSE\" up --build --abort-on-container-exit --exit-code-from test || rc=1",
|
||
" docker compose -f \"$TESTCOMPOSE\" down -v >/dev/null 2>&1",
|
||
" return \"$rc\"",
|
||
"}",
|
||
"pip_audit() {",
|
||
" docker compose -f \"$TESTCOMPOSE\" run --rm --no-deps test \\",
|
||
" pip-audit -r requirements.lock --no-deps --disable-pip",
|
||
"}",
|
||
"if [ \"$mode\" = all ]; then",
|
||
" lane \"vitest\" frontend npm run --silent test",
|
||
" lane \"vite build\" frontend npx vite build --logLevel warn",
|
||
" lane \"pytest (pure-logic + DB lane)\" pytest_lane",
|
||
" lane \"pip-audit (backend lock)\" pip_audit",
|
||
" lane \"npm audit (production dependencies)\" frontend npm audit --omit=dev --audit-level=high",
|
||
"fi",
|
||
"[ \"$list\" = 1 ] && exit 0",
|
||
"if [ -n \"$only\" ] && [ \"$ran\" = 0 ]; then",
|
||
" echo \"no lane named '$only' in mode $mode — see: scripts/check.sh $mode --list\" >&2",
|
||
" exit 2",
|
||
"fi",
|
||
"scope=\"gate ($mode)\"",
|
||
"[ -n \"$only\" ] && scope=\"lane '$only' only (not the gate)\"",
|
||
"echo \">> lane times:\"",
|
||
"printf '%s\\n' \"${timings[@]}\"",
|
||
"echo \"$(printf '%8s' \"$(secs $(($(ms) - gate_t0)))\") s total\"",
|
||
"if [ ${#failed[@]} -eq 0 ]; then",
|
||
" echo \">> $scope: green ✓\"",
|
||
" exit 0",
|
||
"fi",
|
||
"echo \">> $scope: RED — ${failed[*]}\" >&2",
|
||
"exit 1"
|
||
]
|
||
}
|
||
}
|