@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starci/eslint-canon-be",
3
- "version": "3.4.29",
3
+ "version": "3.4.31",
4
4
  "description": "The StarCi back-end lint canon: every rule its laws publish, gathered as one flat-config plugin.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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.29
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.28
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.5
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.3
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.4
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.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.21
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.21
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 ecommerce-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).'
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 ecommerce-app resolution on 2026-10-13. Stays on 13.x with @nestjs/graphql (accepted advisory, major deferred to alpha.19).'
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 ecommerce-app resolution on 2026-10-13.'
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/ecommerce-app/be/src/features/webhooks/sepay/transport/http/sepay.webhook.ts, edition: full}
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 skips only through the test-world library's `useSandbox(...)` when the sandbox config is absent (`sandbox.describe(...)`), 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`."
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: "owner:gate-loosening-e77ca91cded7 approves the per-edition enforcer split; 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."
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 does not match the server is left to the server. The files a unit or an end-to-end spec names are outside the scan, so outside these rules."
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-history approval is checked against committed gates and owner rulings; product scaffolding has no runtime release history to generate."
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 owner-class: 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 lands, and passes the check, only when the tree it is judged against already holds the owner's approval of exactly that loosening, an owner-rulings entry gate-loosening-<fingerprint>."
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/ecommerce-app/be/src/features/webhooks/sepay/transport/http/sepay.webhook.ts","edition":"full"}
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/ecommerce-app/be/src/features/api/health/application/check-health.contracts.ts","edition":"full"}
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/ecommerce-app/be/apps/order/src/compose/platform.ts","edition":"full"}
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/ecommerce-app/be/jest.config.js","edition":"full"}
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/ecommerce-app/be/src/features/api/checkout/application/add-cart-item.handler.ts, editions: [full]}
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/ecommerce-app/be/src/features/api/checkout, editions: [full]}
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/ecommerce-app/be/src/features/api/checkout/checkout.module-definition.ts, edition: full}
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/ecommerce-app/.starcistacks/application-stacks.yaml, edition: full}
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/ecommerce-app/.starcistacks/application-stacks.yaml, edition: full}
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/ecommerce-app/.starcistacks/application-stacks.yaml, edition: full}
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.4.0
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, blocker/critical issues, hotspots) and
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: fewer changed lines than this are not held to duplication.
30
- ignoreBelowChangedLines: 20
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 these severities on a changed line fails the slice; lesser ones are listed, never blocking.
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: 20
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
- blocker/critical 0: the two severities that are defects, not style. main carries old debt of both kinds
143
- (one product repository carries 3 blocker and 137 critical), so the SLICE verdict reads only changed lines and never main's backlog;
144
- major and lower are reported on the summary and never fail a slice.
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:affected', 'test:coverage', 'test:cov'];
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 one opt-in slot is the browser journey (`browser: true`).
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`. A unit or end-to-end spec is outside the scan,
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: another canon owner reports the same Sonar id, or the port does not match what the
15
- * server reports on the same source and is left to the server. Parity is measured, not assumed: a product scan with 0 issues on the server
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); the port also reports a rest-sibling omit the server accepts',
24
- 'sonarjs/different-types-comparison': 'S3403, the server: the port reads control-flow-narrowed types (`x !== null` after an aliased guard) the server does not report',
25
- 'sonarjs/function-return-type': 'S3800, the server: the port reports nullable and union returns the server accepts',
26
- 'sonarjs/argument-type': 'S3782, the server: the port reports optional arguments the server accepts',
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). Left out: S7785 (top-level await; an app's TypeScript compiles to CommonJS, `module: nodenext` without `type: module`) and
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', options: [{ checkArrowFunctions: false }] },
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. The ONE legitimate skip is the contract layer's own, and it lives in the test-world
6
- * library: `useSandbox(...)` of `@starci/test-world` skips itself when the provider sandbox is not configured and hands
7
- * the spec `sandbox.describe(...)`. No file of the repository writes a skip: the rule judges every spec AND every file
8
- * of the test tree (`be.tests.*` slots: world, fixtures, kit), so a repo-local helper cannot become a second skip path.
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; only the test-world library's `useSandbox` skips." },
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. The only skip is the test-world library's own: a contract spec calls `useSandbox(...)` of `@starci/test-world` and writes `sandbox.describe(...)`.",
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 decides the skip.",
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) {