@starci/eslint-canon-fe 8.4.29 → 8.4.30

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,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 8.4.30 - 2026-10-23
4
+ - Changed: the bundled canon pins select @starci/hfs 4.7.24 and @starci/cli 1.7.24.
5
+ - Added: the type-aware rules the back end already borrowed, at error: `@typescript-eslint/no-floating-promises`, `no-misused-promises`, `switch-exhaustiveness-check` and the `no-unsafe-assignment`, `-member-access`, `-call`, `-return`, `-argument` family.
6
+ - Changed: no Sonar way rule is left to the server (owner ruling 2026-10-22, no loosening): S3403, S3800, S3782 and S6582 run at error, `unicorn/consistent-function-scoping` runs with its default options, and S7785 (`unicorn/prefer-top-level-await`) runs in the front end (ESM).
7
+ - Added: `starciPlainSourceConfig({ root })` for a TypeScript front-end tree that is no HFS app (a runtime package, the harness UI): typed linting, the inline-directive fence, the React Hooks, type-aware and Sonar way rules and every canon rule that needs no slot view (`SLOT_FREE_RULES`), all at error.
8
+
3
9
  ## 8.4.29 - 2026-10-22
4
10
  - Changed: the bundled canon pins select @starci/hfs 4.7.23 and @starci/cli 1.7.23.
5
11
 
package/index.mjs CHANGED
@@ -49,7 +49,7 @@ import { recommended as typographyRecommended, rules as typographyRules } from "
49
49
  import { recommended as vendorRecommended, rules as vendorRules } from "./vendor-boundary.mjs"
50
50
  import { recommended as projectGraphRecommended, rules as projectGraphRules } from "./project-graph.mjs"
51
51
  import { recommended as sonarParityRecommended, rules as sonarParityRules } from "./sonar-parity.mjs"
52
- import { buildFeConfig } from "./lib/config.mjs"
52
+ import { buildFeConfig, buildPlainSourceConfig } from "./lib/config.mjs"
53
53
  import { why } from "./lib/why.mjs"
54
54
 
55
55
  /** Each law's contribution, kept separate so a duplicate name is detectable rather than silent. */
@@ -246,6 +246,33 @@ export const starciFeConfig = ({ hfs } = {}) => {
246
246
  })
247
247
  }
248
248
 
249
+ /**
250
+ * The flat config of a TypeScript front-end tree that is no HFS app (a runtime package, the harness UI):
251
+ * `export default starciPlainSourceConfig({ root: import.meta.dirname })`. It lints the tree's `src/` with types, with the inline-directive fence, the
252
+ * React Hooks rules, the type-aware rules and the Sonar way rules, and with every canon rule that needs no slot view, all at `error`.
253
+ *
254
+ * @param {{ root: string }} input - The absolute directory holding the tree's `tsconfig.json` and `src/`.
255
+ * @returns {object[]} The flat config: ignores, the typed source block and the Sonar way block.
256
+ */
257
+ export const starciPlainSourceConfig = ({ root } = {}) => {
258
+ const reactHooks = loadReactHooks()
259
+ return buildPlainSourceConfig({ root, plugin, hooks: reactHooksRules(reactHooks), reactHooks, tsPlugin: loadTsPlugin(), sonar: loadSonarPlugins() })
260
+ }
261
+
262
+ /**
263
+ * The flat config of a TypeScript front-end tree that adopts the typed half of the canon first: the inline-directive fence, typed linting, the
264
+ * React Hooks rules, the type-aware typescript-eslint rules and the Sonar way rules over the tree's `src/`, all at `error`. It is
265
+ * `starciPlainSourceConfig` without the canon's own product-structure rules (`SLOT_FREE_RULES`: jsdoc on exports, arrow exports, English-only
266
+ * source, grammar controls, ...), for a tree that has not been brought to them yet; a tree that has uses `starciPlainSourceConfig`.
267
+ *
268
+ * @param {{ root: string }} input - The absolute directory holding the tree's `tsconfig.json` and `src/`.
269
+ * @returns {object[]} The flat config: ignores, the typed source block and the Sonar way block.
270
+ */
271
+ export const starciTypedSourceConfig = ({ root } = {}) => {
272
+ const reactHooks = loadReactHooks()
273
+ return buildPlainSourceConfig({ root, plugin, hooks: reactHooksRules(reactHooks), reactHooks, tsPlugin: loadTsPlugin(), sonar: loadSonarPlugins(), canon: false })
274
+ }
275
+
249
276
  // A21 append-only registration: keep the already exported plugin and recommendation live.
250
277
  import { recommended as functionBudgetRecommended, rules as functionBudgetRules } from "./function-budget.mjs"
251
278
  CONTRIBUTIONS.push({ law: "function-budget", rules: functionBudgetRules, recommended: functionBudgetRecommended })
package/lib/config.mjs CHANGED
@@ -25,8 +25,20 @@ export const SOURCE_FILES = Object.freeze(["apps/*/src/**/*.{ts,tsx}", "packages
25
25
  /** Build output, installed packages, generated wire types and the harness's report directory are never linted. */
26
26
  export const IGNORED = Object.freeze(["**/node_modules/**", "**/.next/**", "**/dist/**", "**/coverage/**", "**/__generated__/**", ".starci/**"])
27
27
 
28
- /** The type-aware typescript-eslint rules the canon borrows for Sonar S1874 (a deprecated API), S6551 (an object turned into text) and S7503 (an `async` function that never awaits), all `error`. */
28
+ /**
29
+ * The type-aware typescript-eslint rules the canon borrows, all `error` and the same set the back-end canon borrows for the type-aware laws: a promise is awaited or
30
+ * handled (`no-floating-promises`, `no-misused-promises`), a `switch` over a union names every member, nothing flows out of an `any` (the `no-unsafe-*` family),
31
+ * and the Sonar rules that need the checker: S1874 (a deprecated API), S6551 (an object turned into text) and S7503 (an `async` function that never awaits).
32
+ */
29
33
  export const BORROWED = Object.freeze({
34
+ "@typescript-eslint/no-floating-promises": "error",
35
+ "@typescript-eslint/no-misused-promises": "error",
36
+ "@typescript-eslint/switch-exhaustiveness-check": "error",
37
+ "@typescript-eslint/no-unsafe-assignment": "error",
38
+ "@typescript-eslint/no-unsafe-member-access": "error",
39
+ "@typescript-eslint/no-unsafe-call": "error",
40
+ "@typescript-eslint/no-unsafe-return": "error",
41
+ "@typescript-eslint/no-unsafe-argument": "error",
30
42
  "@typescript-eslint/no-deprecated": "error",
31
43
  "@typescript-eslint/no-base-to-string": "error",
32
44
  "@typescript-eslint/require-await": "error",
@@ -71,6 +83,60 @@ export const buildFeConfig = ({ hfs, plugin, source, reactHooks, tsPlugin, sonar
71
83
  rules: { ...source, ...BORROWED },
72
84
  },
73
85
  // Sonar way: what the scan raises is refused while the code is written; a spec is outside the scan, so outside this block.
74
- { files: [...SOURCE_FILES], ignores: [...OUTSIDE_THE_SCAN_FILES], plugins: { sonarjs: sonar.sonarjs, unicorn: sonar.unicorn }, rules: sonarWayRules(sonar.sonarjs).rules },
86
+ { files: [...SOURCE_FILES], ignores: [...OUTSIDE_THE_SCAN_FILES], plugins: { sonarjs: sonar.sonarjs, unicorn: sonar.unicorn }, rules: sonarWayRules(sonar.sonarjs, "fe").rules },
87
+ ]
88
+ }
89
+
90
+ /**
91
+ * The canon rules that need no slot view: they judge a node and its types, never where the file sits in an app. A tree that is no HFS front end (a
92
+ * runtime package, the harness UI) is linted by exactly these plus the type-aware, React Hooks and Sonar way rules. The set is closed by
93
+ * config.spec.mjs: every other rule of the plugin refuses to run without `settings.starci.hfs`, so a rule added to the plugin is either listed here
94
+ * or proven slot-bound there.
95
+ */
96
+ export const SLOT_FREE_RULES = Object.freeze([
97
+ "client-no-server-import", "no-server-action", "no-dangerous-html", "response-cookie-attributes", "require-export-jsdoc", "no-vietnamese-in-source",
98
+ "no-hardcoded-endpoint-fallback", "no-runtime-namespace", "list-item-has-key", "no-index-key", "no-inline-literal-prop-in-list", "prefer-arrow-export",
99
+ "handler-on-prefix", "no-native-form-control", "no-native-img", "image-has-size", "no-second-i18n-stack", "html-lang-from-locale", "no-null-suspense-fallback",
100
+ "no-native-anchor", "no-inline-parameter-type", "no-hand-rolled-polling", "status-text-uses-soft-foreground", "no-copy-in-call", "no-alert", "no-failure-collapse",
101
+ "outcome-kinds-exhaustive", "no-await-in-loop", "prefer-code-point", "prefer-string-raw", "no-nested-conditional", "no-void-operator", "no-unused-import",
102
+ "prefer-export-from", "no-unused-prop-types", "media-has-caption", "max-params",
103
+ ])
104
+
105
+ /** The source of a plain front-end tree: its `src/`. */
106
+ export const PLAIN_SOURCE_FILES = Object.freeze(["src/**/*.{ts,tsx}"])
107
+
108
+ /**
109
+ * The flat config of a TypeScript front-end tree that is no HFS app: the same fences as an app (typed linting, no inline directive, every rule at
110
+ * `error`) over the rules that need no slot view. It takes the root of the tree and nothing else.
111
+ *
112
+ * @param {object} input - Attachment options.
113
+ * @param {string} input.root - The absolute directory holding the tree's `tsconfig.json` and `src/`.
114
+ * @param {object} input.plugin - The canon plugin.
115
+ * @param {Record<string, unknown>} input.hooks - The React Hooks rules at error.
116
+ * @param {object} input.reactHooks - The React Hooks plugin.
117
+ * @param {object} input.tsPlugin - The typescript-eslint plugin.
118
+ * @param {{ sonarjs: object, unicorn: object }} input.sonar - The plugins of the Sonar way rules.
119
+ * @returns {Array<object>} The flat config: one ignore block, one typed source block and the Sonar way block.
120
+ */
121
+ export const buildPlainSourceConfig = ({ root, plugin, hooks, reactHooks, tsPlugin, sonar, canon = true }) => {
122
+ if (typeof root !== "string" || root === "") throw new Error("starciPlainSourceConfig needs { root } - the directory of the tree's tsconfig.json and src/")
123
+ const canonRules = canon ? Object.fromEntries(SLOT_FREE_RULES.map((name) => [`starci-fe/${name}`, "error"])) : {}
124
+ const missing = SLOT_FREE_RULES.filter((name) => !(name in plugin.rules))
125
+ if (missing.length > 0) throw new Error(`starciPlainSourceConfig lists rules the canon plugin does not publish: ${missing.join(", ")}`)
126
+ return [
127
+ { ignores: [...IGNORED] },
128
+ {
129
+ files: [...PLAIN_SOURCE_FILES],
130
+ languageOptions: {
131
+ parser: tsParser,
132
+ ecmaVersion: "latest",
133
+ sourceType: "module",
134
+ parserOptions: { projectService: true, tsconfigRootDir: root, ecmaFeatures: { jsx: true } },
135
+ },
136
+ linterOptions: { ...linterOptions },
137
+ plugins: { "starci-fe": plugin, "react-hooks": reactHooks, "@typescript-eslint": tsPlugin },
138
+ rules: { ...canonRules, ...hooks, ...BORROWED },
139
+ },
140
+ { files: [...PLAIN_SOURCE_FILES], ignores: [...OUTSIDE_THE_SCAN_FILES], plugins: { sonarjs: sonar.sonarjs, unicorn: sonar.unicorn }, rules: sonarWayRules(sonar.sonarjs, "fe").rules },
75
141
  ]
76
142
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starci/eslint-canon-fe",
3
- "version": "8.4.29",
3
+ "version": "8.4.30",
4
4
  "description": "The StarCi front-end lint canon: every rule its laws publish, gathered as one flat-config plugin and attached by one factory.",
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.30
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.29
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.23
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.23
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
@@ -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
  }