@starci/eslint-canon-be 3.4.29 → 3.4.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/lib/config.mjs +6 -1
- package/package.json +1 -1
- package/runtime/knowledge/hfs/canon-pins.yaml +11 -11
- package/runtime/knowledge/hfs/rules.yaml +17 -17
- package/runtime/knowledge/hfs/slots.yaml +20 -1
- package/runtime/knowledge/sonar-gate.yaml +12 -12
- package/runtime/packages/hfs/templates/app/package-scripts/package.json +0 -1
- package/runtime/scripts/hfs/architecture/automatic-gates.mjs +1 -1
- package/runtime/scripts/hfs/slots.mjs +3 -2
- package/runtime/scripts/lib/sonar-way.mjs +26 -14
- package/spec-no-skip.mjs +8 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.4.31 - 2026-10-23
|
|
4
|
+
- Changed: the `spec-no-skip` rule text says nothing skips, the contract layer included: a missing sandbox is a failing test of `sandbox.describe` from `@starci/test-world` 1.2.3.
|
|
5
|
+
- Changed: the bundled canon pins select @starci/hfs 4.7.24 and @starci/cli 1.7.24.
|
|
6
|
+
- Changed: no Sonar way rule is left to the server (owner ruling 2026-10-22, no loosening): S3403 `different-types-comparison`, S3800 `function-return-type`, S3782 `argument-type` and S6582 `@typescript-eslint/prefer-optional-chain` run at error, `unicorn/consistent-function-scoping` runs with its default options (arrow functions included). S7785 (top-level await) is recorded as not applicable to a CommonJS back end (`SONAR_WAY_NOT_APPLICABLE`).
|
|
7
|
+
- Added: the `@typescript-eslint/no-unsafe-assignment`, `-member-access`, `-call`, `-return` and `-argument` rules at error.
|
|
8
|
+
|
|
9
|
+
## 3.4.30 - 2026-10-22
|
|
10
|
+
- Changed: the bundled canon pins select @starci/hfs 4.7.23 and @starci/cli 1.7.23.
|
|
11
|
+
|
|
3
12
|
## 3.4.29 - 2026-10-22
|
|
4
13
|
- Changed: the bundled canon pins select @starci/hfs 4.7.21 and @starci/cli 1.7.21.
|
|
5
14
|
|
package/lib/config.mjs
CHANGED
|
@@ -47,6 +47,11 @@ export const BORROWED = Object.freeze({
|
|
|
47
47
|
"@typescript-eslint/no-deprecated": "error",
|
|
48
48
|
"@typescript-eslint/no-base-to-string": "error",
|
|
49
49
|
"@typescript-eslint/require-await": "error",
|
|
50
|
+
"@typescript-eslint/no-unsafe-assignment": "error",
|
|
51
|
+
"@typescript-eslint/no-unsafe-member-access": "error",
|
|
52
|
+
"@typescript-eslint/no-unsafe-call": "error",
|
|
53
|
+
"@typescript-eslint/no-unsafe-return": "error",
|
|
54
|
+
"@typescript-eslint/no-unsafe-argument": "error",
|
|
50
55
|
"no-console": "error",
|
|
51
56
|
})
|
|
52
57
|
|
|
@@ -90,6 +95,6 @@ export const buildBeConfig = async ({ hfs, plugin, recommended }) => {
|
|
|
90
95
|
rules: { ...rules, ...BORROWED },
|
|
91
96
|
},
|
|
92
97
|
// Sonar way: what the scan raises is refused while the code is written; a spec is outside the scan, so outside this block.
|
|
93
|
-
{ files: [...SOURCE_FILES], ignores: [...OUTSIDE_THE_SCAN_FILES], plugins: { sonarjs, unicorn }, rules: sonarWayRules(sonarjs).rules },
|
|
98
|
+
{ files: [...SOURCE_FILES], ignores: [...OUTSIDE_THE_SCAN_FILES], plugins: { sonarjs, unicorn }, rules: sonarWayRules(sonarjs, "be").rules },
|
|
94
99
|
]
|
|
95
100
|
}
|
package/package.json
CHANGED
|
@@ -29,14 +29,14 @@ pins:
|
|
|
29
29
|
source: packages/grammar/package.json
|
|
30
30
|
why: 'the grammar front ends read: the brand layer sets `--font-sans` and `--font-mono` and the grammar reads them; it carries the Input tel kind, the copyable and read-only Input, the IconButton disclosure props and count badge, ChatBubble, FillViewport, Textarea mentions and Enter-to-send, the leading PrimaryRailLayout rail, the plate-less Image, ListRow, the soft Alert, Progress steps, the floating primary IconButton, FlowGraph, the STATE-8 loading views and the bordered selectable TagGroup chips. The runtime source version wins for a @starci package.'
|
|
31
31
|
'@starci/eslint-canon-be':
|
|
32
|
-
version: 3.4.
|
|
32
|
+
version: 3.4.31
|
|
33
33
|
group: starci
|
|
34
34
|
install: registry
|
|
35
35
|
side: be
|
|
36
36
|
source: packages/eslint/be/package.json
|
|
37
37
|
why: 'the back-end ESLint canon: `loadHfs(import.meta.url)` of be/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the be side, and the project graph is built per side; its bundled runtime copies (slots, canon-pins, failure codes, the architecture machine) put .starcistacks and .sops.yaml at the app root; `starciBeConfig({ hfs: loadHfs(import.meta.url) })` is the typed factory and the BE-CONVENTION laws are its rules.'
|
|
38
38
|
'@starci/eslint-canon-fe':
|
|
39
|
-
version: 8.4.
|
|
39
|
+
version: 8.4.30
|
|
40
40
|
group: starci
|
|
41
41
|
install: registry
|
|
42
42
|
side: fe
|
|
@@ -50,40 +50,40 @@ pins:
|
|
|
50
50
|
source: packages/stylelint/package.json
|
|
51
51
|
why: 'the front-end stylelint canon: no-class-selector refuses a compound class selector (`div.card`); the brand layer accepts `--font-sans` and `--font-mono`; status-contrast measures the HeroUI soft pairs per theme; brand-layer-shape reads the shared :root,.light,.dark block and the info soft pair; appTokens come from loadAppTokens(import.meta.url) for the managed one-line stylelint.config.mjs.'
|
|
52
52
|
'@starci/tsconfig':
|
|
53
|
-
version: 2.0.
|
|
53
|
+
version: 2.0.7
|
|
54
54
|
group: starci
|
|
55
55
|
install: registry
|
|
56
56
|
side: both
|
|
57
57
|
source: packages/tsconfig/package.json
|
|
58
58
|
'@starci/prettier-config':
|
|
59
|
-
version: 1.0.
|
|
59
|
+
version: 1.0.4
|
|
60
60
|
group: starci
|
|
61
61
|
install: registry
|
|
62
62
|
side: both
|
|
63
63
|
source: packages/prettier-config/package.json
|
|
64
64
|
'@starci/jest-preset':
|
|
65
|
-
version: 2.3.
|
|
65
|
+
version: 2.3.5
|
|
66
66
|
group: starci
|
|
67
67
|
install: registry
|
|
68
68
|
side: be
|
|
69
69
|
source: packages/jest-preset/package.json
|
|
70
70
|
why: 'the Jest preset of a back end: the world runner runs up to min(--maxWorkers, slots) world files at once, each in its own process bound to one data slot of the test world (STARCI_TEST_WORLD_SLOT), and reads test-world state protocol 2 only: it pairs exactly with @starci/test-world, and any other state file is JEST_PRESET_WORLD_PAIR_MISMATCH. The unit run writes the lcov Sonar and Codecov import (services only); the integration, e2e and contract projects run every spec file in a worker process of its own (world-runner.cjs), so a process-global framework registry (@nestjs/graphql type metadata) never leaks between e2e files; it ships the unit kit (mockEntityManager, fakeTransaction, fakeIds, FakeClock, Outcome matchers) and the recordingOutbox claim side.'
|
|
71
71
|
'@starci/test-world':
|
|
72
|
-
version: 1.2.
|
|
72
|
+
version: 1.2.3
|
|
73
73
|
group: starci
|
|
74
74
|
install: registry
|
|
75
75
|
side: be
|
|
76
76
|
source: packages/test-world/package.json
|
|
77
77
|
why: 'the shared e2e library of every back end (R47, R48), a devDependency of every back end: the warm stack behind toxiproxy, the network-edge fakes, the Nest boot, the typed useTestWorld handle, useSandbox for contract specs and the outage lock. Kafka is real own infra (the one digest-pinned apache/kafka KRaft image, a broker listener and proxy per slot, slot-prefixed topics, consumer groups and client ids) and Postgres is schema-per-context (a schema and a login role per context in a shared database). Each worker has its own data slot (<ns>_w<k>) with its own databases, Keycloak realm, leased Redis DB, bucket/collection/topic prefixes, fakes host, toxiproxy proxies and outage lock; a run provisions min(jest workers, workers cap, default 2) slots, and state file protocol 2 pairs exactly with @starci/jest-preset (TEST_WORLD_PAIR_MISMATCH otherwise). A modules world boots real peer apps beside its modules, world.apps.<name>.during(fn) is the outage of a peer app, w.keycloak.clientSecret(client) answers the run-generated secret of a confidential realm client, world.keycloak.events/sessions read the realm user events and live sessions, world.infra.postgresql.connection(name) takes one database connection down while the others serve, and world.resolve and scope.resolve take any Nest token. Every path a declaration names (`stack`, seeds, the realm, Dockerfiles) resolves from the app root (the directory of hfs.json); the globalSetup registers the path aliases as TypeScript resolves them. Its CLI main is exported at ./cli for starci app stack.'
|
|
78
78
|
'@starci/cli':
|
|
79
|
-
version: 1.7.
|
|
79
|
+
version: 1.7.24
|
|
80
80
|
group: starci
|
|
81
81
|
install: registry
|
|
82
82
|
side: both
|
|
83
83
|
source: packages/cli/package.json
|
|
84
84
|
why: 'the only StarCi command package; product repositories install it for starci app, while @starci/hfs remains its pinned transitive implementation.'
|
|
85
85
|
'@starci/hfs':
|
|
86
|
-
version: 4.7.
|
|
86
|
+
version: 4.7.24
|
|
87
87
|
group: starci
|
|
88
88
|
install: registry
|
|
89
89
|
side: both
|
|
@@ -465,7 +465,7 @@ pins:
|
|
|
465
465
|
version: 13.4.5
|
|
466
466
|
group: framework
|
|
467
467
|
side: be
|
|
468
|
-
why: 'the GraphQL transport of every back end (be.transport.graphql) that the front end calls; the
|
|
468
|
+
why: 'the GraphQL transport of every back end (be.transport.graphql) that the front end calls; the full-app resolution on 2026-10-13. Stays on 13.x for alpha.18: the npm-audit advisory fixed by 14.0.3 (semver major) is a documented accepted exception, upgrade deferred to alpha.19 (owner ruling graphql-13-advisory-accepted-until-a19, 2026-10-16).'
|
|
469
469
|
'@graphql-tools/utils':
|
|
470
470
|
version: 12.1.0
|
|
471
471
|
group: framework
|
|
@@ -475,12 +475,12 @@ pins:
|
|
|
475
475
|
version: 13.4.5
|
|
476
476
|
group: framework
|
|
477
477
|
side: be
|
|
478
|
-
why: 'the Apollo driver of @nestjs/graphql; the
|
|
478
|
+
why: 'the Apollo driver of @nestjs/graphql; the full-app resolution on 2026-10-13. Stays on 13.x with @nestjs/graphql (accepted advisory, major deferred to alpha.19).'
|
|
479
479
|
'@apollo/server':
|
|
480
480
|
version: 5.5.1
|
|
481
481
|
group: framework
|
|
482
482
|
side: be
|
|
483
|
-
why: 'the Apollo server behind @nestjs/apollo; the
|
|
483
|
+
why: 'the Apollo server behind @nestjs/apollo; the full-app resolution on 2026-10-13.'
|
|
484
484
|
'@nestjs/websockets':
|
|
485
485
|
version: 11.2.5
|
|
486
486
|
group: framework
|
|
@@ -56,7 +56,7 @@ rules:
|
|
|
56
56
|
- {path: packages/eslint/fe/slot-ownership.spec.mjs, editions: [lite, full], codes: [HFS_SLOT_UNDECLARED, HFS_SLOT_AMBIGUOUS, HFS_SLOT_NOT_ENABLED]}
|
|
57
57
|
exemplifiedBy:
|
|
58
58
|
- {path: examples/lite-app/be/src/features/webhooks/calendar/transport/http/calendar.webhook.ts, edition: lite}
|
|
59
|
-
- {path: examples/
|
|
59
|
+
- {path: examples/full-app/be/src/features/webhooks/sepay/transport/http/sepay.webhook.ts, edition: full}
|
|
60
60
|
witness: {status: unreachable, reason: "owner:no-runtime-specs (2026-10-17): the mutation witness was a deleted runtime spec or fixture", date: "2026-10-17"}
|
|
61
61
|
specNa: "owner:no-runtime-specs (2026-10-17): the runtime has no specs of its own; the package specs named in proves keep proving the lint enforcers, the runtime-code enforcers lost their deleted runtime spec"
|
|
62
62
|
- id: R02
|
|
@@ -846,7 +846,7 @@ rules:
|
|
|
846
846
|
exemplifiedBy:
|
|
847
847
|
- {path: packages/hfs/templates/be/inbox/service.spec.ts.tpl, editions: [lite, full]}
|
|
848
848
|
witness: {status: unreachable, reason: "owner:no-runtime-specs (2026-10-17): the mutation witness was a deleted runtime spec or fixture", date: "2026-10-17"}
|
|
849
|
-
law: "One jest config, projects `unit`, `integration`, `e2e` and `contract`, live by folder, `diagnostics: false`; unit specs are `<name>.<role>.spec.ts` beside the file they test: required for the roles of `ruleParams.be.unitRoles` (`<name>.service.spec.ts` beside a `<name>.service.ts`, `<name>.cli.spec.ts` beside a cli command `<name>.cli.ts` of `src/features/cli`), optional for any other logic role (`ruleParams.be.logicRoles`) inside a slot whose `coverage` is required, except that the commit adding a measured logic file stages its `<name>.<role>.spec.ts` beside it (owner 2026-10-20, judged by `starci app hygiene` in the pre-commit hook), and nowhere else but beside a webhook or realtime door that has no service of its own (`<provider>.webhook.spec.ts`, `<channel>.gateway.spec.ts`, `<x>.subscription.spec.ts`) (every unit-tested subject has exactly one; any other `*.spec.ts` outside `be/src/tests/{world,integration,e2e,contract}` is a finding, including specs in apps), and the unit project collects coverage from the logic roles of the measured roots only (the scope derived from the slot manifest, R204) with a per-file threshold of 100 on lines, branches, functions and statements (`test` runs `--coverage`; `importHelpers` and `tslib`); `be/src/tests/world/` is the ONLY test infrastructure: `global-setup.ts` starts the app's own stack (`.starcistacks/<env>`, named by `stack` in `test-world.config.ts`, the declaration of the test-world library in its one named form `export const { useTestWorld, useSandbox } = defineTestWorld({...})` (a default export is not read): every service it declares runs REAL, behind toxiproxy, images and versions read from the stack at run time, never spelled in test source) or attaches to a warm one that answers, and runs the migrations once over the cli app's connections (the runner of the cli migrate command); `global-teardown.ts` stops only what its setup started; `use-test-world.ts` exports `useTestWorld({ apps } | { modules })` (`world.apps.<name>.api`, `world.db.<connection>`, `world.infra.<service>` with `latency(ms)`, `cut()` and `restore()` on the real service, `world.fake.<provider>`, `world.waitFor`) and `fakes/<provider>/` holds the network-edge fakes (`failNext`, `replayWebhook`, `delay`, payload fixtures) of external SaaS the team does not operate ONLY: a fake of a service the stack declares is refused, except stateless compute that needs special hardware or an external model (GPU inference, a self-hosted embedding model), whose `stacks` entry in `test-world.config.ts` declares `{ fakedBy: <fake>, reason }` with a non-empty `reason` (a stateful service, or one with a persistent volume, is never accepted); it alone owns containers, DataSource, migrations and `process.env`; nothing under `be/src/tests` overrides a provider or DI token; `be/src/tests/integration/<capability>/*.integration-spec.ts` (`{ modules }`, with the real peer apps its client calls beside them as `apps`; real infrastructure, no HTTP door of ours), `be/src/tests/e2e/<area>/*.e2e-spec.ts` (`{ apps }`) and `be/src/tests/contract/<provider>/*.contract-spec.ts` (provider sandboxes, skipped without sandbox config, run only by `test:contract`, never by `test` or `test:e2e`) agree folder with suffix; an integration or e2e spec passes `useTestWorld` one object literal with plain keys (no variable, spread or computed key) and calls it; the contract layer
|
|
849
|
+
law: "One jest config, projects `unit`, `integration`, `e2e` and `contract`, live by folder, `diagnostics: false`; unit specs are `<name>.<role>.spec.ts` beside the file they test: required for the roles of `ruleParams.be.unitRoles` (`<name>.service.spec.ts` beside a `<name>.service.ts`, `<name>.cli.spec.ts` beside a cli command `<name>.cli.ts` of `src/features/cli`), optional for any other logic role (`ruleParams.be.logicRoles`) inside a slot whose `coverage` is required, except that the commit adding a measured logic file stages its `<name>.<role>.spec.ts` beside it (owner 2026-10-20, judged by `starci app hygiene` in the pre-commit hook), and nowhere else but beside a webhook or realtime door that has no service of its own (`<provider>.webhook.spec.ts`, `<channel>.gateway.spec.ts`, `<x>.subscription.spec.ts`) (every unit-tested subject has exactly one; any other `*.spec.ts` outside `be/src/tests/{world,integration,e2e,contract}` is a finding, including specs in apps), and the unit project collects coverage from the logic roles of the measured roots only (the scope derived from the slot manifest, R204) with a per-file threshold of 100 on lines, branches, functions and statements (`test` runs `--coverage`; `importHelpers` and `tslib`); `be/src/tests/world/` is the ONLY test infrastructure: `global-setup.ts` starts the app's own stack (`.starcistacks/<env>`, named by `stack` in `test-world.config.ts`, the declaration of the test-world library in its one named form `export const { useTestWorld, useSandbox } = defineTestWorld({...})` (a default export is not read): every service it declares runs REAL, behind toxiproxy, images and versions read from the stack at run time, never spelled in test source) or attaches to a warm one that answers, and runs the migrations once over the cli app's connections (the runner of the cli migrate command); `global-teardown.ts` stops only what its setup started; `use-test-world.ts` exports `useTestWorld({ apps } | { modules })` (`world.apps.<name>.api`, `world.db.<connection>`, `world.infra.<service>` with `latency(ms)`, `cut()` and `restore()` on the real service, `world.fake.<provider>`, `world.waitFor`) and `fakes/<provider>/` holds the network-edge fakes (`failNext`, `replayWebhook`, `delay`, payload fixtures) of external SaaS the team does not operate ONLY: a fake of a service the stack declares is refused, except stateless compute that needs special hardware or an external model (GPU inference, a self-hosted embedding model), whose `stacks` entry in `test-world.config.ts` declares `{ fakedBy: <fake>, reason }` with a non-empty `reason` (a stateful service, or one with a persistent volume, is never accepted); it alone owns containers, DataSource, migrations and `process.env`; nothing under `be/src/tests` overrides a provider or DI token; `be/src/tests/integration/<capability>/*.integration-spec.ts` (`{ modules }`, with the real peer apps its client calls beside them as `apps`; real infrastructure, no HTTP door of ours), `be/src/tests/e2e/<area>/*.e2e-spec.ts` (`{ apps }`) and `be/src/tests/contract/<provider>/*.contract-spec.ts` (provider sandboxes, skipped without sandbox config, run only by `test:contract`, never by `test` or `test:e2e`) agree folder with suffix; an integration or e2e spec passes `useTestWorld` one object literal with plain keys (no variable, spread or computed key) and calls it; the contract layer has no skip either: a contract spec writes `sandbox.describe(...)` from the test-world library's `useSandbox(...)`, and when the sandbox config is absent that describe registers one failing test naming the missing keys (the body never runs), and every `fakes/<provider>/` that serves payload fixtures (`payloads/*.json`) has a `contract/<provider>/*.contract-spec.ts` that references those fixtures and asserts `shapeOf(real)` equals `shapeOf(fixture)` (the helper `shapeOf` is named by `ruleParams.be.contractShape` of the slot manifest; fakes the test-world library provides are guarded by the library); no `.test.ts`, `int-spec` or `harness-spec`, no `live/` and no `be/src/tests/e2e/world`."
|
|
850
850
|
kinds: [check, codemod, lint]
|
|
851
851
|
gates: [land, ci]
|
|
852
852
|
failureCodes: ["BE_TEST_TOPOLOGY", "BE_CONTRACT_UNGUARDED", "BE_UNIT_SPEC_NOT_STAGED"]
|
|
@@ -2540,7 +2540,7 @@ rules:
|
|
|
2540
2540
|
enforcers:
|
|
2541
2541
|
- {kind: hfs, id: pattern-spec-missing, at: scripts/hfs/rules/event-bus.mjs, editions: [full]}
|
|
2542
2542
|
- {kind: hfs, id: lite-proof-lane, at: scripts/hfs/rules/lite-proof-lane.mjs, editions: [lite]}
|
|
2543
|
-
editionWhy: "
|
|
2543
|
+
editionWhy: "each enforcer judges the edition it emits for (pattern-spec-missing returns no findings for lite, lite-proof-lane is lite's emitter), so the split adds lite's emitter and drops no finding; ruling:R212/case-2 (with the Work boundary amended by ruling:R212/case-4) gives lite its integration proof lane while full retains e2e or integration proofs."
|
|
2544
2544
|
docs: [docs/quality-bar.md#r155]
|
|
2545
2545
|
templateNa: "Pattern scenarios depend on application behavior and providers; no universal runnable scenario proof is scaffolded."
|
|
2546
2546
|
specNa: "owner:no-runtime-specs (2026-10-17): the runtime has no specs of its own; this code is enforced by runtime code and its spec proof was a deleted runtime spec"
|
|
@@ -3282,7 +3282,7 @@ rules:
|
|
|
3282
3282
|
- {path: packages/hfs/templates/app/ci-workflows-lite/github/workflows/ci.yml, edition: lite}
|
|
3283
3283
|
- {path: packages/hfs/templates/app/ci-workflows/github/workflows/ci.yml, edition: full}
|
|
3284
3284
|
witness: {status: unreachable, reason: "owner:no-runtime-specs (2026-10-17): the mutation witness was a deleted runtime spec or fixture", date: "2026-10-17"}
|
|
3285
|
-
law: "A coverage upload of a CI workflow never skips or fails silently: every step that uses codecov/codecov-action, in a root workflow, an example workflow or a template workflow of packages/hfs, authenticates by OIDC (`use_oidc: true`, with `permissions: id-token: write` on the job or the workflow), sets `fail_ci_if_error: true`, has no `continue-on-error`, and has no `if` that reads a secret or CODECOV_TOKEN, so a missing token can never turn the upload into a skipped step."
|
|
3285
|
+
law: "A coverage upload of a CI workflow never skips or fails silently: every step that uses codecov/codecov-action, in a root workflow, an example workflow or a template workflow of packages/hfs, authenticates by OIDC (`use_oidc: true`, with `permissions: id-token: write` on the job or the workflow), sets `fail_ci_if_error: true`, has no `continue-on-error`, and has no `if` that reads a secret or CODECOV_TOKEN, so a missing token can never turn the upload into a skipped step. Every step that uses SonarSource/* has no `if` that reads a secret or SONAR_TOKEN (a missing token fails the job), and no step or job of a workflow sets `continue-on-error`."
|
|
3286
3286
|
scope: runtime
|
|
3287
3287
|
kinds: [check]
|
|
3288
3288
|
gates: [runtime]
|
|
@@ -3754,7 +3754,7 @@ rules:
|
|
|
3754
3754
|
- {path: packages/eslint/be/sonar-parity-editions.spec.mjs, edition: full}
|
|
3755
3755
|
- {path: packages/eslint/fe/sonar-parity-editions.spec.mjs, edition: lite}
|
|
3756
3756
|
- {path: packages/eslint/fe/sonar-parity-editions.spec.mjs, edition: full}
|
|
3757
|
-
law: "Code carries none of the smells SonarCloud measures that a lint rule can decide: no `await` inside a loop (S9382; an endless loop is spared), no `charCodeAt`/`fromCharCode` (S7758), no string that only escapes backslashes (S7780; a Next `matcher` is spared, it is read statically), no nested conditional expression (S3358), no `void` on anything but a call (S3735), no unused import (S1128), no import that is only handed on instead of `export ... from` (S7763), no prop of a component's props type the component never reads (S6767, front end), no `<video>` or `<audio>` without a captions track unless muted (S4084, front end), and, through typescript-eslint with type information, no deprecated API (S1874), no object turned into text (S6551) and no `async` function that never awaits and returns no promise (S7503). No function takes more than seven parameters (S107), except the constructor of an injectable class, whose parameters are its injected dependencies. Every other rule of the Sonar way TypeScript profile with an ESLint equivalent that reports what the server reports on the same source (scripts/lib/sonar-way.mjs: eslint-plugin-sonarjs' Sonar way set and the core and unicorn rules it builds on) is on at error in both canons; a port that
|
|
3757
|
+
law: "Code carries none of the smells SonarCloud measures that a lint rule can decide: no `await` inside a loop (S9382; an endless loop is spared), no `charCodeAt`/`fromCharCode` (S7758), no string that only escapes backslashes (S7780; a Next `matcher` is spared, it is read statically), no nested conditional expression (S3358), no `void` on anything but a call (S3735), no unused import (S1128), no import that is only handed on instead of `export ... from` (S7763), no prop of a component's props type the component never reads (S6767, front end), no `<video>` or `<audio>` without a captions track unless muted (S4084, front end), and, through typescript-eslint with type information, no deprecated API (S1874), no object turned into text (S6551) and no `async` function that never awaits and returns no promise (S7503). No function takes more than seven parameters (S107), except the constructor of an injectable class, whose parameters are its injected dependencies. Every other rule of the Sonar way TypeScript profile with an ESLint equivalent that reports what the server reports on the same source (scripts/lib/sonar-way.mjs: eslint-plugin-sonarjs' Sonar way set and the core and unicorn rules it builds on) is on at error in both canons (S7785, top-level await, runs in the front end only: a back end compiles to CommonJS); a port that disagrees with the server is fixed in the source it reports, never left out. The files a unit or an end-to-end spec names are outside the scan, so outside these rules."
|
|
3758
3758
|
kinds: [lint, codemod]
|
|
3759
3759
|
gates: [pre-commit, settle, land, ci]
|
|
3760
3760
|
failureCodes: ["SCAN_SMELL"]
|
|
@@ -3781,10 +3781,10 @@ rules:
|
|
|
3781
3781
|
code: "RT_GATE_LOOSENING"
|
|
3782
3782
|
editions: [lite, full]
|
|
3783
3783
|
docs: [docs/debugging.md#r236]
|
|
3784
|
-
templateNa: "Runtime release
|
|
3784
|
+
templateNa: "Runtime release history is judged against the committed gates of the last release; product scaffolding has no runtime release history to generate."
|
|
3785
3785
|
specNa: "owner:no-runtime-specs (2026-10-17): the runtime has no specs of its own; this code is enforced by runtime code and its spec proof was a deleted runtime spec"
|
|
3786
3786
|
witness: {status: unreachable, reason: "owner:no-runtime-specs (2026-10-17): the mutation witness was a deleted runtime spec or fixture", date: "2026-10-17"}
|
|
3787
|
-
law: "A change that loosens a gate or check is
|
|
3787
|
+
law: "A change that loosens a gate or check is always red: a check removed from a gate file's list, a floor lowered or a ceiling raised there, an allowlist entry added, a spec deleted or weakened or skipped together with product code (modules/kernel/gate-loosening.yaml). It never lands and never passes the check; there is no approval route, the owner's included (owner:loosening-always-red), and the fix is to restore the gate. An entry that only moves with a git rename of the paths it names is no loosening."
|
|
3788
3788
|
scope: runtime
|
|
3789
3789
|
kinds: [check]
|
|
3790
3790
|
gates: [runtime]
|
|
@@ -4254,7 +4254,7 @@ rules:
|
|
|
4254
4254
|
- {"path":"packages/hfs/templates/be/inbox/webhook-door.ts.tpl","editions":["lite","full"]}
|
|
4255
4255
|
- {"path":"packages/hfs/templates/be/inbox/service.ts.tpl","editions":["lite","full"]}
|
|
4256
4256
|
- {"path":"examples/lite-app/be/src/features/webhooks/calendar/transport/http/calendar.webhook.ts","edition":"lite"}
|
|
4257
|
-
- {"path":"examples/
|
|
4257
|
+
- {"path":"examples/full-app/be/src/features/webhooks/sepay/transport/http/sepay.webhook.ts","edition":"full"}
|
|
4258
4258
|
- {"path":"packages/eslint/be/webhook-handoff-inbox.spec.mjs","editions":["lite","full"],"codes":["BE_WEBHOOK_INBOX_KEY"]}
|
|
4259
4259
|
proves:
|
|
4260
4260
|
- {"path":"packages/eslint/be/webhook-handoff-scope.spec.mjs","editions":["lite","full"],"codes":["BE_WEBHOOK_SCOPE"]}
|
|
@@ -4279,7 +4279,7 @@ rules:
|
|
|
4279
4279
|
exemplifiedBy:
|
|
4280
4280
|
- {"path":"packages/hfs/templates/be/skeleton/src/features/api/system-health/application/check-liveness.contracts.ts","editions":["lite","full"]}
|
|
4281
4281
|
- {"path":"examples/lite-app/be/src/features/api/bookings/application/create-booking.contracts.ts","edition":"lite"}
|
|
4282
|
-
- {"path":"examples/
|
|
4282
|
+
- {"path":"examples/full-app/be/src/features/api/health/application/check-health.contracts.ts","edition":"full"}
|
|
4283
4283
|
proves:
|
|
4284
4284
|
- {"path":"packages/eslint/be/feature-thin.spec.mjs","editions":["lite","full"],"codes":["HFS_FEATURE_LOGIC_ROLE"]}
|
|
4285
4285
|
witness:
|
|
@@ -4301,7 +4301,7 @@ rules:
|
|
|
4301
4301
|
- {"path":"packages/hfs/templates/be/skeleton-lite/apps/api/src/compose/platform.ts","edition":"lite"}
|
|
4302
4302
|
- {"path":"packages/hfs/templates/be/skeleton/apps/__app__/src/compose/platform.ts","edition":"full"}
|
|
4303
4303
|
- {"path":"examples/lite-app/be/apps/api/src/compose/platform.ts","edition":"lite"}
|
|
4304
|
-
- {"path":"examples/
|
|
4304
|
+
- {"path":"examples/full-app/be/apps/order/src/compose/platform.ts","edition":"full"}
|
|
4305
4305
|
proves:
|
|
4306
4306
|
- {"path":"packages/eslint/be/app-composition.spec.mjs","editions":["lite","full"],"codes":["BE_COMPOSITION_UNTYPED"]}
|
|
4307
4307
|
witness:
|
|
@@ -4453,7 +4453,7 @@ rules:
|
|
|
4453
4453
|
exemplifiedBy:
|
|
4454
4454
|
- {"path":"packages/hfs/templates/be/tool-config/jest.config.js","editions":["lite","full"]}
|
|
4455
4455
|
- {"path":"examples/lite-app/be/jest.config.js","edition":"lite"}
|
|
4456
|
-
- {"path":"examples/
|
|
4456
|
+
- {"path":"examples/full-app/be/jest.config.js","edition":"full"}
|
|
4457
4457
|
specNa: "owner:no-runtime-specs (2026-10-17): the runtime has no specs of its own; this code is enforced by runtime code and its spec proof was a deleted runtime spec"
|
|
4458
4458
|
|
|
4459
4459
|
witness: {status: unreachable, reason: "owner:no-runtime-specs (2026-10-17): the mutation witness was a deleted runtime spec or fixture", date: "2026-10-17"}
|
|
@@ -4794,7 +4794,7 @@ rules:
|
|
|
4794
4794
|
- {path: packages/eslint/fe/function-budget.spec.mjs, editions: [lite, full], codes: [HFS_FUNCTION_BUDGET]}
|
|
4795
4795
|
- {path: packages/hfs/templates/be/patterns/api/graphql.resolver.ts.tpl, editions: [lite, full]}
|
|
4796
4796
|
- {path: examples/lite-app/be/src/features/api/bookings/application/create-booking.handler.ts, editions: [lite]}
|
|
4797
|
-
- {path: examples/
|
|
4797
|
+
- {path: examples/full-app/be/src/features/api/checkout/application/add-cart-item.handler.ts, editions: [full]}
|
|
4798
4798
|
proves:
|
|
4799
4799
|
- {path: packages/eslint/be/function-budget.spec.mjs, editions: [lite, full], codes: [HFS_FUNCTION_BUDGET]}
|
|
4800
4800
|
- {path: packages/eslint/fe/function-budget.spec.mjs, editions: [lite, full], codes: [HFS_FUNCTION_BUDGET]}
|
|
@@ -4815,7 +4815,7 @@ rules:
|
|
|
4815
4815
|
exemplifiedBy:
|
|
4816
4816
|
- {path: packages/hfs/templates/be/patterns/api, editions: [lite, full]}
|
|
4817
4817
|
- {path: examples/lite-app/be/src/features/api/bookings, editions: [lite]}
|
|
4818
|
-
- {path: examples/
|
|
4818
|
+
- {path: examples/full-app/be/src/features/api/checkout, editions: [full]}
|
|
4819
4819
|
specNa: "owner:no-runtime-specs (2026-10-17): the runtime has no specs of its own; this code is enforced by runtime code and its spec proof was a deleted runtime spec"
|
|
4820
4820
|
|
|
4821
4821
|
witness: {status: unreachable, reason: "owner:no-runtime-specs (2026-10-17): the mutation witness was a deleted runtime spec or fixture", date: "2026-10-17"}
|
|
@@ -4876,7 +4876,7 @@ rules:
|
|
|
4876
4876
|
- {path: packages/hfs/templates/be/patterns/api/feature.module-definition.ts.tpl, edition: lite}
|
|
4877
4877
|
- {path: packages/hfs/templates/be/patterns/api/graphql.module-definition.ts.tpl, edition: lite}
|
|
4878
4878
|
- {path: examples/lite-app/be/src/features/api/bookings/transport/graphql/bookings-graphql.module-definition.ts, edition: lite}
|
|
4879
|
-
- {path: examples/
|
|
4879
|
+
- {path: examples/full-app/be/src/features/api/checkout/checkout.module-definition.ts, edition: full}
|
|
4880
4880
|
- {path: packages/eslint/be/module-shape.spec.mjs, editions: [lite, full]}
|
|
4881
4881
|
proves:
|
|
4882
4882
|
- {path: packages/eslint/be/module-shape.spec.mjs, editions: [lite, full], codes: [BE_MODULE_DEFINITION_MISSING]}
|
|
@@ -4895,7 +4895,7 @@ rules:
|
|
|
4895
4895
|
- {path: docs/application-stacks.md#r296, editions: [lite, full]}
|
|
4896
4896
|
exemplifiedBy:
|
|
4897
4897
|
- {path: examples/lite-app/.starcistacks/application-stacks.yaml, edition: lite}
|
|
4898
|
-
- {path: examples/
|
|
4898
|
+
- {path: examples/full-app/.starcistacks/application-stacks.yaml, edition: full}
|
|
4899
4899
|
- {path: examples/shape-slot/.starcistacks/application-stacks.yaml, edition: full}
|
|
4900
4900
|
- {path: packages/hfs/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml, edition: lite}
|
|
4901
4901
|
- {path: packages/hfs/templates/app/skeleton/.starcistacks/application-stacks.yaml, edition: full}
|
|
@@ -4914,7 +4914,7 @@ rules:
|
|
|
4914
4914
|
- {path: docs/application-stacks.md#r297, editions: [lite, full]}
|
|
4915
4915
|
exemplifiedBy:
|
|
4916
4916
|
- {path: examples/lite-app/.starcistacks/application-stacks.yaml, edition: lite}
|
|
4917
|
-
- {path: examples/
|
|
4917
|
+
- {path: examples/full-app/.starcistacks/application-stacks.yaml, edition: full}
|
|
4918
4918
|
- {path: examples/shape-slot/.starcistacks/application-stacks.yaml, edition: full}
|
|
4919
4919
|
- {path: packages/hfs/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml, edition: lite}
|
|
4920
4920
|
- {path: packages/hfs/templates/app/skeleton/.starcistacks/application-stacks.yaml, edition: full}
|
|
@@ -4933,7 +4933,7 @@ rules:
|
|
|
4933
4933
|
- {path: docs/application-stacks.md#r298, editions: [lite, full]}
|
|
4934
4934
|
exemplifiedBy:
|
|
4935
4935
|
- {path: examples/lite-app/.starcistacks/application-stacks.yaml, edition: lite}
|
|
4936
|
-
- {path: examples/
|
|
4936
|
+
- {path: examples/full-app/.starcistacks/application-stacks.yaml, edition: full}
|
|
4937
4937
|
- {path: examples/shape-slot/.starcistacks/application-stacks.yaml, edition: full}
|
|
4938
4938
|
- {path: packages/hfs/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml, edition: lite}
|
|
4939
4939
|
- {path: packages/hfs/templates/app/skeleton/.starcistacks/application-stacks.yaml, edition: full}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
# catalog READ this file through scripts/hfs/slots.mjs; none of them hardcodes a path.
|
|
8
8
|
# Shape: modules/schemas/hfs-slots.schema.yaml. The repository side is hfs.json: modules/schemas/hfs-repo.schema.yaml.
|
|
9
9
|
schema: starci/hfs-slots@3
|
|
10
|
-
version: 3.
|
|
10
|
+
version: 3.5.0
|
|
11
11
|
|
|
12
12
|
versioning:
|
|
13
13
|
# MAJOR.MINOR.PATCH of this manifest. A repository pins only the MAJOR in hfs.json ("hfs": 3).
|
|
@@ -39,6 +39,8 @@ versioning:
|
|
|
39
39
|
# when the file key is absent; .starcistacks owns custody. Capability <capability>.config.ts roles stay unchanged.
|
|
40
40
|
# 3.3.0 is a minor (alpha.16): it ADDS the optional app.ci-stack-deploy slot, the managed workflow of the starci app stack verbs.
|
|
41
41
|
# 3.4.0 is a minor (alpha.25): it ADDS the opt-in be.contract.mcp-tools slot, the committed MCP tool-catalog snapshot of an app.
|
|
42
|
+
# 3.5.0 is a minor (alpha.27): it ADDS the opt-in app.browser-config slot, the managed browser/tsconfig.json that type-checks the browser
|
|
43
|
+
# journey (enabled with app.browser by `browser: true`).
|
|
42
44
|
|
|
43
45
|
# Field meaning, per slot.
|
|
44
46
|
# id stable name, never changes meaning inside a major.
|
|
@@ -617,6 +619,23 @@ slots:
|
|
|
617
619
|
tests: none
|
|
618
620
|
requires: [playwright.config.ts, journeys/]
|
|
619
621
|
allows: [playwright.config.ts, stack.mjs, "journeys/*.ts"]
|
|
622
|
+
- id: app.browser-config
|
|
623
|
+
since: 3.5.0
|
|
624
|
+
profiles: [app]
|
|
625
|
+
editionWhy: "ruling:R212/case-2 (with the Work boundary amended by ruling:R212/case-4): browser journeys belong to full; lite proves app boot through R272 wiring."
|
|
626
|
+
# The program that type-checks the browser journey: Playwright only transpiles the config and the journeys, so without it nothing
|
|
627
|
+
# type-checks them and the op gate finds no tsconfig.json owning a changed journey (GATE_TSC_CONFIG_MISSING). It is managed (rendered
|
|
628
|
+
# by starci app sync from templates/app/browser-config, extending @starci/tsconfig/e2e.json), never authored, and enabled with
|
|
629
|
+
# app.browser by `browser: true` in hfs.json.
|
|
630
|
+
path: browser/tsconfig.json
|
|
631
|
+
presence: opt-in
|
|
632
|
+
litePresence: forbidden
|
|
633
|
+
goesTo: "the full edition: a lite app has no browser journey to type-check"
|
|
634
|
+
tracked: tracked
|
|
635
|
+
tier: none
|
|
636
|
+
tests: none
|
|
637
|
+
managedBy: browser-config
|
|
638
|
+
rules: [HFS_MANAGED_FILE_DRIFT]
|
|
620
639
|
- id: app.github-meta
|
|
621
640
|
since: 3.1.0
|
|
622
641
|
profiles: [app]
|
|
@@ -8,7 +8,7 @@ purpose: |
|
|
|
8
8
|
conditions and selects it for the project, and (2) judges an op's slice - the lines it changed - against
|
|
9
9
|
the same numbers. Code-writing ops cannot settle done while the slice is red
|
|
10
10
|
(scripts/kernel/sonar-settle.mjs). Changing a number here changes every repository on the next scan.
|
|
11
|
-
The gate has two parts: `newCode` (the recent work: coverage, duplication,
|
|
11
|
+
The gate has two parts: `newCode` (the recent work: coverage, duplication, issues of every severity, hotspots) and
|
|
12
12
|
`overall` (the whole code: coverage, no imported HFS, ESLint or stylelint finding, no bug, code smell or
|
|
13
13
|
vulnerability, every hotspot reviewed, no duplicated-lines excess, no cognitive-complexity issue). The one Sonar
|
|
14
14
|
mechanism of the HFS canon is packages/hfs/README.md section "Sonar".
|
|
@@ -26,15 +26,15 @@ newCode:
|
|
|
26
26
|
coverage:
|
|
27
27
|
metric: new_coverage
|
|
28
28
|
minPercent: 100
|
|
29
|
-
# SonarQube's ignoreSmallChanges:
|
|
30
|
-
ignoreBelowChangedLines:
|
|
29
|
+
# SonarQube's ignoreSmallChanges floor: 0, so a change of any size is held to duplication.
|
|
30
|
+
ignoreBelowChangedLines: 0
|
|
31
31
|
duplication:
|
|
32
32
|
metric: new_duplicated_lines_density
|
|
33
33
|
maxPercent: 3
|
|
34
34
|
issues:
|
|
35
|
-
# An open issue of
|
|
36
|
-
blockingSeverities: [BLOCKER, CRITICAL]
|
|
37
|
-
metrics: {BLOCKER: new_blocker_violations, CRITICAL: new_critical_violations}
|
|
35
|
+
# An open issue of any severity on a changed line fails the slice: there is no lesser severity that is only listed.
|
|
36
|
+
blockingSeverities: [BLOCKER, CRITICAL, MAJOR, MINOR, INFO]
|
|
37
|
+
metrics: {BLOCKER: new_blocker_violations, CRITICAL: new_critical_violations, MAJOR: new_major_violations, MINOR: new_minor_violations, INFO: new_info_violations}
|
|
38
38
|
max: 0
|
|
39
39
|
hotspots:
|
|
40
40
|
metric: new_security_hotspots_reviewed
|
|
@@ -92,13 +92,13 @@ lite:
|
|
|
92
92
|
coverage:
|
|
93
93
|
metric: new_coverage
|
|
94
94
|
minPercent: 100
|
|
95
|
-
ignoreBelowChangedLines:
|
|
95
|
+
ignoreBelowChangedLines: 0
|
|
96
96
|
duplication:
|
|
97
97
|
metric: new_duplicated_lines_density
|
|
98
98
|
maxPercent: 3
|
|
99
99
|
issues:
|
|
100
|
-
blockingSeverities: [BLOCKER, CRITICAL]
|
|
101
|
-
metrics: {BLOCKER: new_blocker_violations, CRITICAL: new_critical_violations}
|
|
100
|
+
blockingSeverities: [BLOCKER, CRITICAL, MAJOR, MINOR, INFO]
|
|
101
|
+
metrics: {BLOCKER: new_blocker_violations, CRITICAL: new_critical_violations, MAJOR: new_major_violations, MINOR: new_minor_violations, INFO: new_info_violations}
|
|
102
102
|
max: 0
|
|
103
103
|
hotspots:
|
|
104
104
|
metric: new_security_hotspots_reviewed
|
|
@@ -139,9 +139,9 @@ reasoning: |
|
|
|
139
139
|
lives there, the feature tier is thin (R203) and proven by integration and e2e, and the front end has no unit tests.
|
|
140
140
|
duplication 3: the Sonar way value; the worst project today is 2.1, so 3 rejects a real copy-paste block and
|
|
141
141
|
passes ordinary work.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
142
|
+
issues 0 at every severity (blocker, critical, major, minor, info), owner ruling 2026-10-22 (no loosening): a lesser severity is not a
|
|
143
|
+
lesser finding. The SLICE verdict reads only changed lines and never main's backlog; the overall condition holds the whole project.
|
|
144
|
+
A change of any size is held to duplication (ignoreBelowChangedLines 0).
|
|
145
145
|
overall issues 0 and overall duplication 3: the server gate is also the owner's definition of done for the HFS canon:
|
|
146
146
|
a repository that carries any open ESLint, stylelint or HFS finding is not green, whatever the age of the line. The
|
|
147
147
|
op slice keeps judging changed lines (an op cannot fix a repository's whole backlog); the dashboard and CI show the
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
{{> app/package-scripts/core.json}}
|
|
6
6
|
"typecheck:tests": "tsc -p be/src/tests/tsconfig.json",
|
|
7
7
|
"test": "cd be && jest --selectProjects unit --coverage",
|
|
8
|
-
"test:affected": "cd be && jest --selectProjects unit --passWithNoTests",
|
|
9
8
|
"test:integration": "npm run typecheck:tests && cd be && jest --selectProjects integration",
|
|
10
9
|
"test:e2e": "npm run typecheck:tests && cd be && jest --selectProjects e2e",
|
|
11
10
|
"test:contract": "npm run typecheck:tests && cd be && jest --selectProjects contract",
|
|
@@ -10,7 +10,7 @@ const E2E_COMMAND = new RegExp([
|
|
|
10
10
|
String.raw`src\/tests\/(?:world|integration|e2e|contract)\b|`,
|
|
11
11
|
String.raw`jest[^\n|&;]*(?:e2e|integration|contract)`,
|
|
12
12
|
].join(''), 'u');
|
|
13
|
-
const UNIT_RUN_SCRIPTS = ['test', 'test:unit', 'test:ci', 'test:
|
|
13
|
+
const UNIT_RUN_SCRIPTS = ['test', 'test:unit', 'test:ci', 'test:coverage', 'test:cov'];
|
|
14
14
|
|
|
15
15
|
const runsE2e = text => E2E_COMMAND.test(String(text).replace(/--ignore-pattern[= ]+(?:"[^"]*"|'[^']*'|\S+)/gu, ''));
|
|
16
16
|
const withoutComments = text => text.split('\n').filter(line => !/^\s*#/u.test(line)).join('\n');
|
|
@@ -153,9 +153,10 @@ export function resolveRepoDeclaration(manifest, declaration, { file = HFS_DECLA
|
|
|
153
153
|
edition,
|
|
154
154
|
side: null,
|
|
155
155
|
profile: APP_SCOPE,
|
|
156
|
-
// The root declares no app or connection of its own (each side does); its
|
|
156
|
+
// The root declares no app or connection of its own (each side does); its opt-in slots are the browser journey and the managed
|
|
157
|
+
// tsconfig.json that type-checks it, both switched on by `browser: true`.
|
|
157
158
|
apps: Object.freeze([]),
|
|
158
|
-
optionalSlots: Object.freeze(declaration.browser === true ? ['app.browser'] : []),
|
|
159
|
+
optionalSlots: Object.freeze(declaration.browser === true ? ['app.browser', 'app.browser-config'] : []),
|
|
159
160
|
connections: Object.freeze([]),
|
|
160
161
|
providers,
|
|
161
162
|
reads: Object.freeze([]),
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// the one mapping both lint canons (@starci/eslint-canon-be and -fe) publish, so `starci app lint` in the op loop refuses what the scan would
|
|
3
3
|
// raise while the code is written. The base is eslint-plugin-sonarjs's recommended config, SonarSource's own port of Sonar way (each rule's
|
|
4
4
|
// docs url names its S-id); its rules the canons already publish in another form stay with that owner (SONAR_WAY_OWNED), and the Sonar way
|
|
5
|
-
// rules SonarSource builds on other ESLint rules are SONAR_WAY_EXTRA. Every rule is `error
|
|
5
|
+
// rules SonarSource builds on other ESLint rules are SONAR_WAY_EXTRA. Every rule is `error`; no rule is left to the server because its port
|
|
6
|
+
// disagrees with it (a port that disagrees is fixed in the source it reports, never switched off). A unit or end-to-end spec is outside the scan,
|
|
6
7
|
// so outside these rules: the canons give this block the ignores of sonar-syntax-rules.mjs OUTSIDE_THE_SCAN_FILES. The runtime's own
|
|
7
8
|
// `sonar-rules` gate reads the two limits below.
|
|
8
9
|
|
|
@@ -11,31 +12,36 @@ export const COGNITIVE_COMPLEXITY_LIMIT = 15;
|
|
|
11
12
|
export const MAX_PARAMETERS = 7;
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
|
-
* sonarjs rules of Sonar way the local lint does not run
|
|
15
|
-
*
|
|
16
|
-
* (Sonar way, 2026-10-21) while these ports reported 130 findings on it; a rule that cannot match the server is not a lint error.
|
|
15
|
+
* sonarjs rules of Sonar way the local lint does not run under their sonarjs name because another owner enforces the same Sonar id at error:
|
|
16
|
+
* a canon syntax rule, a borrowed typescript-eslint rule or the TypeScript compiler. Each owner is asserted in packages/eslint/be/sonar-way.spec.mjs and packages/eslint/fe/config.spec.mjs.
|
|
17
17
|
*/
|
|
18
18
|
const SONAR_WAY_OWNED = Object.freeze({
|
|
19
19
|
'sonarjs/no-nested-conditional': 'S3358, the canon syntax rule no-nested-conditional',
|
|
20
20
|
'sonarjs/unused-import': 'S1128, the canon syntax rule no-unused-import',
|
|
21
21
|
'sonarjs/void-use': 'S3735, the canon syntax rule no-void-operator',
|
|
22
22
|
'sonarjs/deprecation': 'S1874, the borrowed @typescript-eslint/no-deprecated',
|
|
23
|
-
'sonarjs/no-unused-vars': 'S1481, the TypeScript compiler (noUnusedLocals in @starci/tsconfig)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
'sonarjs/no-unused-vars': 'S1481, the TypeScript compiler (noUnusedLocals and noUnusedParameters in @starci/tsconfig)',
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Sonar way rules with no ESLint form that applies to the module format of a side. S7785 prefers top-level await, which a CommonJS output
|
|
28
|
+
* (the back end: `module: nodenext` without `type: module`) cannot contain; the front end is ESM and runs the rule.
|
|
29
|
+
*/
|
|
30
|
+
export const SONAR_WAY_NOT_APPLICABLE = Object.freeze({
|
|
31
|
+
be: Object.freeze({ S7785: 'top-level await does not exist in the CommonJS output of a back end' }),
|
|
32
|
+
fe: Object.freeze({}),
|
|
27
33
|
});
|
|
28
34
|
|
|
29
35
|
/**
|
|
30
36
|
* The Sonar way rules SonarSource implements on an ESLint rule outside eslint-plugin-sonarjs: `{ sonar, rule, options? }`. The canon syntax
|
|
31
37
|
* rules carry S107 (`max-params`, which spares the constructor of an injectable class as the server does) and S4084 (`media-has-caption`,
|
|
32
|
-
* front end)
|
|
33
|
-
* S6582 (typescript-eslint's optional-chain rule reports guards whose optional chain would change the value's type, which the server accepts).
|
|
38
|
+
* front end); S6582 is typescript-eslint's `prefer-optional-chain`, which needs the type information the canons' typed block provides.
|
|
34
39
|
*/
|
|
35
40
|
export const SONAR_WAY_EXTRA = Object.freeze([
|
|
36
41
|
{ sonar: 'S3863', rule: 'no-duplicate-imports', options: [{ allowSeparateTypeImports: true }] },
|
|
42
|
+
{ sonar: 'S6582', rule: '@typescript-eslint/prefer-optional-chain' },
|
|
37
43
|
{ sonar: 'S4138', rule: 'unicorn/no-for-loop' },
|
|
38
|
-
{ sonar: 'S7721', rule: 'unicorn/consistent-function-scoping'
|
|
44
|
+
{ sonar: 'S7721', rule: 'unicorn/consistent-function-scoping' },
|
|
39
45
|
{ sonar: 'S7732', rule: 'unicorn/no-instanceof-builtins' },
|
|
40
46
|
{ sonar: 'S7740', rule: 'unicorn/no-this-assignment' },
|
|
41
47
|
{ sonar: 'S7744', rule: 'unicorn/no-useless-fallback-in-spread' },
|
|
@@ -50,14 +56,20 @@ export const SONAR_WAY_EXTRA = Object.freeze([
|
|
|
50
56
|
{ sonar: 'S7784', rule: 'unicorn/prefer-structured-clone' },
|
|
51
57
|
]);
|
|
52
58
|
|
|
59
|
+
/** The ESM-only Sonar way rules: S7785 prefers top-level await (see SONAR_WAY_NOT_APPLICABLE). */
|
|
60
|
+
const SONAR_WAY_ESM = Object.freeze([{ sonar: 'S7785', rule: 'unicorn/prefer-top-level-await' }]);
|
|
61
|
+
|
|
53
62
|
/** The Sonar id a sonarjs rule documents (its docs url ends in `#/rspec/S<id>/javascript`), or null. */
|
|
54
63
|
const sonarIdOf = (rule) => /\/rspec\/#\/rspec\/(S\d+)\//u.exec(rule?.meta?.docs?.url ?? '')?.[1] ?? null;
|
|
55
64
|
|
|
56
65
|
/**
|
|
57
66
|
* The Sonar way rules, `{ rules, mapped }`: `rules` the flat-config rule levels (sonarjs' recommended rules at error minus the owned ones,
|
|
58
|
-
* with S3776's limit, then SONAR_WAY_EXTRA), `mapped` every `{ sonar, rule }` row.
|
|
67
|
+
* with S3776's limit, then SONAR_WAY_EXTRA, then the ESM-only rules unless `side` is `be`), `mapped` every `{ sonar, rule }` row.
|
|
68
|
+
*
|
|
69
|
+
* @param {object} sonarjs - The eslint-plugin-sonarjs plugin.
|
|
70
|
+
* @param {'be' | 'fe'} [side] - The side the rules lint; the back end compiles to CommonJS.
|
|
59
71
|
*/
|
|
60
|
-
export function sonarWayRules(sonarjs) {
|
|
72
|
+
export function sonarWayRules(sonarjs, side = 'fe') {
|
|
61
73
|
const mapped = [];
|
|
62
74
|
const rules = {};
|
|
63
75
|
for (const [name, setting] of Object.entries(sonarjs.configs.recommended.rules)) {
|
|
@@ -67,7 +79,7 @@ export function sonarWayRules(sonarjs) {
|
|
|
67
79
|
rules[name] = ['error', ...options];
|
|
68
80
|
mapped.push({ sonar: sonarIdOf(sonarjs.rules[name.slice('sonarjs/'.length)]), rule: name });
|
|
69
81
|
}
|
|
70
|
-
for (const entry of SONAR_WAY_EXTRA) {
|
|
82
|
+
for (const entry of [...SONAR_WAY_EXTRA, ...SONAR_WAY_ESM.filter(row => !SONAR_WAY_NOT_APPLICABLE[side][row.sonar])]) {
|
|
71
83
|
rules[entry.rule] = ['error', ...(entry.options ?? [])];
|
|
72
84
|
mapped.push({ sonar: entry.sonar, rule: entry.rule });
|
|
73
85
|
}
|
package/spec-no-skip.mjs
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
* The rule that keeps every back-end spec running (catalog R48 `BE_SPEC_QUALITY`, owner test policy 2026-09-30).
|
|
3
3
|
*
|
|
4
4
|
* A unit, integration, e2e or contract spec never skips, focuses, marks as todo or picks its own runner: a test that does
|
|
5
|
-
* not run is a claim nobody checks.
|
|
6
|
-
*
|
|
7
|
-
* the
|
|
8
|
-
* of the test tree (`be.tests.*` slots: world, fixtures, kit), so a
|
|
5
|
+
* not run is a claim nobody checks. There is no legitimate skip, the contract layer included: a contract spec writes
|
|
6
|
+
* `sandbox.describe(...)` from `useSandbox(...)` of `@starci/test-world`, and when the provider sandbox is not configured
|
|
7
|
+
* that describe registers one FAILING test naming the missing keys (the body never runs). No file of the repository writes a
|
|
8
|
+
* skip: the rule judges every spec AND every file of the test tree (`be.tests.*` slots: world, fixtures, kit), so a
|
|
9
|
+
* repo-local helper cannot become a skip path.
|
|
9
10
|
*
|
|
10
11
|
* Refused in a spec: `it|test|describe` followed by `.skip`, `.skipIf`, `.runIf`, `.todo` or `.only`; `xit`, `xtest`,
|
|
11
12
|
* `xdescribe`, `fit`, `ftest`, `fdescribe`; and a runner used as a value (a conditional `(cond ? describe : describe.skip)`,
|
|
@@ -34,11 +35,11 @@ const resolve = (scope, name) => {
|
|
|
34
35
|
export const specNoSkip = {
|
|
35
36
|
meta: {
|
|
36
37
|
type: "problem",
|
|
37
|
-
docs: { description: "No spec or test-tree file skips, focuses, marks todo or selects its runner conditionally;
|
|
38
|
+
docs: { description: "No spec or test-tree file skips, focuses, marks todo or selects its runner conditionally; a contract spec's missing sandbox is a failing test of the test-world library's `sandbox.describe`, never a skip." },
|
|
38
39
|
schema: [],
|
|
39
40
|
messages: {
|
|
40
|
-
skip: "`{{what}}` keeps a test from running. A spec runs every test it declares: delete it or write it.
|
|
41
|
-
select: "A test runner (`{{what}}`) is used as a value here, so the spec picks or hides what runs. Call `describe`/`it`/`test` directly; a contract spec writes `sandbox.describe(...)` from `useSandbox(...)` of `@starci/test-world`, which
|
|
41
|
+
skip: "`{{what}}` keeps a test from running. A spec runs every test it declares: delete it or write it. Nothing skips: a contract spec calls `useSandbox(...)` of `@starci/test-world` and writes `sandbox.describe(...)`, which fails naming the missing keys when the sandbox is not configured.",
|
|
42
|
+
select: "A test runner (`{{what}}`) is used as a value here, so the spec picks or hides what runs. Call `describe`/`it`/`test` directly; a contract spec writes `sandbox.describe(...)` from `useSandbox(...)` of `@starci/test-world`, which fails naming the missing keys when the sandbox is not configured.",
|
|
42
43
|
},
|
|
43
44
|
},
|
|
44
45
|
create(context) {
|