@starci/eslint-canon-be 3.4.24 → 3.4.25

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,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.4.25 - 2026-10-21
4
+ - Added: Sonar way at lint time: every rule of the Sonar way TypeScript profile with an ESLint equivalent is on at error over the files the scan judges (a unit or end-to-end spec is outside), from the shared `scripts/lib/sonar-way.mjs`: eslint-plugin-sonarjs' Sonar way set (cognitive complexity 15, super-linear and slow regex, sort compare, nested template literals, ...) and the rules SonarSource builds on (`max-params` 7, `no-duplicate-imports`, `@typescript-eslint/prefer-optional-chain`, unicorn's `prefer-native-coercion-functions`, `prefer-includes`, `prefer-set-has` and the other S77xx rules). New exact dependencies eslint-plugin-sonarjs 4.2.1 and eslint-plugin-unicorn 65.0.0 (the last line for ESLint 9).
5
+ - Changed: the bundled canon-pins copy follows the 1.7.17 cohort (cli 1.7.17, hfs 4.7.17).
6
+
3
7
  ## 3.4.24 - 2026-10-21
4
8
  - Changed: the bundled canon-pins copy follows the 1.7.16 cohort (cli 1.7.16, hfs 4.7.16); lint behavior is unchanged.
5
9
 
package/lib/config.mjs CHANGED
@@ -9,13 +9,16 @@
9
9
  * The factory owns everything else: typed linting on every tracked TypeScript and JavaScript file
10
10
  * (`parserOptions.projectService`), the ignores (`dist/`, `coverage/`, `node_modules/` and nothing more), the
11
11
  * linter options that make a disable comment impossible, the borrowed typescript-eslint rules, every canon rule at
12
- * `error`, and `settings.starci.hfs` - the slot view each path-scoped rule asks instead of a path pattern.
12
+ * `error`, the Sonar way rules over the files the scan judges (scripts/lib/sonar-way.mjs: eslint-plugin-sonarjs and unicorn), and
13
+ * `settings.starci.hfs` - the slot view each path-scoped rule asks instead of a path pattern.
13
14
  *
14
15
  * NO RULE IS OFF OR WARN. The recommendation itself ships `error`; the factory refuses one that carries anything
15
16
  * else, so a rule cannot be published weakened and still look adopted. The factory takes no rule overrides, globs
16
17
  * or ignores: there is no parameter through which a repository could weaken the canon.
17
18
  */
18
19
  import tsParser from "@typescript-eslint/parser"
20
+ import { OUTSIDE_THE_SCAN_FILES } from "../runtime/scripts/lib/sonar-syntax-rules.mjs"
21
+ import { sonarWayRules } from "../runtime/scripts/lib/sonar-way.mjs"
19
22
 
20
23
  /** A disable directive is a weakening of lint, and one that suppresses nothing is dead weight. */
21
24
  export const linterOptions = Object.freeze({ noInlineConfig: true, reportUnusedDisableDirectives: "error" })
@@ -56,7 +59,7 @@ const levelOf = (setting) => (Array.isArray(setting) ? setting[0] : setting)
56
59
  * @param {object} input.hfs - The HFS view of the repository (`loadHfs(import.meta.url)`).
57
60
  * @param {object} input.plugin - The canon plugin.
58
61
  * @param {Record<string, unknown>} input.recommended - The canon levels.
59
- * @returns {Promise<Array<object>>} The flat config (ESLint awaits an exported promise): one ignore block and one rule block.
62
+ * @returns {Promise<Array<object>>} The flat config (ESLint awaits an exported promise): one ignore block, one rule block and the Sonar way block.
60
63
  */
61
64
  export const buildBeConfig = async ({ hfs, plugin, recommended }) => {
62
65
  if (!hfs || typeof hfs.slotOf !== "function" || typeof hfs.repoRoot !== "string") {
@@ -70,6 +73,7 @@ export const buildBeConfig = async ({ hfs, plugin, recommended }) => {
70
73
  if (weak.length > 0) throw new Error(`starciBeConfig refuses a recommendation with rules not at error: ${weak.join(", ")}`)
71
74
  // Loaded here, not at import: the rule catalog check imports this package to list its rules and needs no linter plugin.
72
75
  const { default: tsPlugin } = await import("@typescript-eslint/eslint-plugin")
76
+ const [{ default: sonarjs }, { default: unicorn }] = await Promise.all([import("eslint-plugin-sonarjs"), import("eslint-plugin-unicorn")])
73
77
  return [
74
78
  { ignores: [...IGNORED] },
75
79
  {
@@ -85,5 +89,7 @@ export const buildBeConfig = async ({ hfs, plugin, recommended }) => {
85
89
  settings: { starci: { hfs } },
86
90
  rules: { ...rules, ...BORROWED },
87
91
  },
92
+ // 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 },
88
94
  ]
89
95
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starci/eslint-canon-be",
3
- "version": "3.4.24",
3
+ "version": "3.4.25",
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",
@@ -41,6 +41,8 @@
41
41
  ],
42
42
  "dependencies": {
43
43
  "@typescript-eslint/eslint-plugin": "^8.70.0",
44
- "@typescript-eslint/parser": "^8.70.0"
44
+ "@typescript-eslint/parser": "^8.70.0",
45
+ "eslint-plugin-sonarjs": "4.2.1",
46
+ "eslint-plugin-unicorn": "65.0.0"
45
47
  }
46
48
  }
@@ -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.24
32
+ version: 3.4.25
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.23
39
+ version: 8.4.24
40
40
  group: starci
41
41
  install: registry
42
42
  side: fe
@@ -62,7 +62,7 @@ pins:
62
62
  side: both
63
63
  source: packages/prettier-config/package.json
64
64
  '@starci/jest-preset':
65
- version: 2.3.2
65
+ version: 2.3.3
66
66
  group: starci
67
67
  install: registry
68
68
  side: be
@@ -76,14 +76,14 @@ pins:
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.16
79
+ version: 1.7.17
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.16
86
+ version: 4.7.17
87
87
  group: starci
88
88
  install: registry
89
89
  side: both
@@ -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), 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). 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). Every other rule of the Sonar way TypeScript profile with an ESLint equivalent (scripts/lib/sonar-way.mjs: eslint-plugin-sonarjs' Sonar way set, the core, typescript-eslint and unicorn rules it builds on) is on at error in both canons. 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"]
@@ -3774,6 +3774,7 @@ rules:
3774
3774
  - {kind: eslint-fe, id: no-unused-import}
3775
3775
  - {kind: eslint-fe, id: prefer-export-from}
3776
3776
  - {kind: eslint-fe, id: no-unused-prop-types}
3777
+ - {kind: eslint-fe, id: media-has-caption}
3777
3778
  - id: R236
3778
3779
  code: "RT_GATE_LOOSENING"
3779
3780
  editions: [lite, full]
@@ -38,7 +38,7 @@ versioning:
38
38
  # or environment reads (BE_APP_OPTIONS_COMPOSITION). Secrets use EnvSource.secret(): <NAME>_FILE first, plain <NAME> only
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
- # 3.4.0 is a minor (alpha.24): it ADDS the opt-in be.contract.mcp-tools slot, the committed MCP tool-catalog snapshot of an app.
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
42
 
43
43
  # Field meaning, per slot.
44
44
  # id stable name, never changes meaning inside a major.
@@ -206,8 +206,28 @@ const noUnusedPropTypes = {
206
206
  },
207
207
  };
208
208
 
209
- /** The files the example scans leave out: a unit spec and an end-to-end spec. */
209
+ /** S4084: a `<video>` or `<audio>` element plays captions: a `<track kind="captions">` child, or it is `muted`. */
210
+ const MEDIA = new Set(['video', 'audio']);
211
+ const jsxName = (node) => (node?.name?.type === 'JSXIdentifier' ? node.name.name : null);
212
+ const attributeOf = (opening, name) => opening.attributes.find((attribute) => attribute.type === 'JSXAttribute' && attribute.name?.name === name);
213
+ const isCaptionTrack = (child) => child.type === 'JSXElement' && jsxName(child.openingElement) === 'track'
214
+ && attributeOf(child.openingElement, 'kind')?.value?.value === 'captions';
215
+ const mediaHasCaption = {
216
+ meta: { type: 'problem', schema: [], messages: { caption: 'S4084: give this <{{ name }}> a <track kind="captions"> child (or make it muted) so it can be followed without sound.' } },
217
+ create(context) {
218
+ return {
219
+ JSXElement(node) {
220
+ const name = jsxName(node.openingElement);
221
+ if (!MEDIA.has(name) || attributeOf(node.openingElement, 'muted') || node.children.some(isCaptionTrack)) return;
222
+ context.report({ node: node.openingElement, messageId: 'caption', data: { name } });
223
+ },
224
+ };
225
+ },
226
+ };
227
+
228
+ /** The files the example scans leave out: a unit spec and an end-to-end spec (the regex, and the flat-config globs of the same set). */
210
229
  const OUTSIDE_THE_SCAN = /\.(?:e2e-)?spec\.[cm]?[jt]sx?$/;
230
+ export const OUTSIDE_THE_SCAN_FILES = Object.freeze(['**/*.spec.{js,mjs,cjs,ts,mts,cts,jsx,tsx}', '**/*.e2e-spec.{js,mjs,cjs,ts,mts,cts,jsx,tsx}']);
211
231
 
212
232
  /** The rule judging only the files the scan judges. */
213
233
  const scanned = (rule) => ({ ...rule, create: (context) => (OUTSIDE_THE_SCAN.test(context.filename) ? {} : rule.create(context)) });
@@ -222,4 +242,5 @@ export const SONAR_SYNTAX_RULES = Object.freeze({
222
242
  'no-unused-import': scanned(noUnusedImport),
223
243
  'prefer-export-from': scanned(preferExportFrom),
224
244
  'no-unused-prop-types': scanned(noUnusedPropTypes),
245
+ 'media-has-caption': scanned(mediaHasCaption),
225
246
  });
@@ -0,0 +1,68 @@
1
+ // sonar-way.mjs - the Sonar way TypeScript profile (the default quality profile sonar.starci.org and SonarCloud apply) as ESLint rules:
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
+ // 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
+ // 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,
6
+ // 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
+ // `sonar-rules` gate reads the two limits below.
8
+
9
+ /** S3776 cognitive complexity and S107 parameters: the Sonar way thresholds. */
10
+ export const COGNITIVE_COMPLEXITY_LIMIT = 15;
11
+ export const MAX_PARAMETERS = 7;
12
+
13
+ /** sonarjs rules of Sonar way another canon rule already reports: the owner that reports the same Sonar id. */
14
+ const SONAR_WAY_OWNED = Object.freeze({
15
+ 'sonarjs/no-nested-conditional': 'S3358, the canon syntax rule no-nested-conditional',
16
+ 'sonarjs/unused-import': 'S1128, the canon syntax rule no-unused-import',
17
+ 'sonarjs/void-use': 'S3735, the canon syntax rule no-void-operator',
18
+ 'sonarjs/deprecation': 'S1874, the borrowed @typescript-eslint/no-deprecated',
19
+ });
20
+
21
+ /**
22
+ * The Sonar way rules SonarSource implements on an ESLint rule outside eslint-plugin-sonarjs: `{ sonar, rule, options? }`. S4084 (a
23
+ * `<video>`/`<audio>` without captions) is the front-end canon's syntax rule media-has-caption. S7785 (top-level await) is left out: an
24
+ * app's TypeScript compiles to CommonJS (`module: nodenext` without `type: module`), where top-level await does not exist.
25
+ */
26
+ export const SONAR_WAY_EXTRA = Object.freeze([
27
+ { sonar: 'S107', rule: 'max-params', options: [{ max: MAX_PARAMETERS }] },
28
+ { sonar: 'S3863', rule: 'no-duplicate-imports', options: [{ allowSeparateTypeImports: true }] },
29
+ { sonar: 'S6582', rule: '@typescript-eslint/prefer-optional-chain' },
30
+ { sonar: 'S4138', rule: 'unicorn/no-for-loop' },
31
+ { sonar: 'S7721', rule: 'unicorn/consistent-function-scoping', options: [{ checkArrowFunctions: false }] },
32
+ { sonar: 'S7732', rule: 'unicorn/no-instanceof-builtins' },
33
+ { sonar: 'S7740', rule: 'unicorn/no-this-assignment' },
34
+ { sonar: 'S7744', rule: 'unicorn/no-useless-fallback-in-spread' },
35
+ { sonar: 'S7747', rule: 'unicorn/no-useless-spread' },
36
+ { sonar: 'S7750', rule: 'unicorn/prefer-array-find' },
37
+ { sonar: 'S7754', rule: 'unicorn/prefer-array-some' },
38
+ { sonar: 'S7765', rule: 'unicorn/prefer-includes' },
39
+ { sonar: 'S7767', rule: 'unicorn/prefer-math-trunc' },
40
+ { sonar: 'S7770', rule: 'unicorn/prefer-native-coercion-functions' },
41
+ { sonar: 'S7776', rule: 'unicorn/prefer-set-has' },
42
+ { sonar: 'S7778', rule: 'unicorn/prefer-single-call' },
43
+ { sonar: 'S7784', rule: 'unicorn/prefer-structured-clone' },
44
+ ]);
45
+
46
+ /** The Sonar id a sonarjs rule documents (its docs url ends in `#/rspec/S<id>/javascript`), or null. */
47
+ const sonarIdOf = (rule) => /\/rspec\/#\/rspec\/(S\d+)\//u.exec(rule?.meta?.docs?.url ?? '')?.[1] ?? null;
48
+
49
+ /**
50
+ * The Sonar way rules, `{ rules, mapped }`: `rules` the flat-config rule levels (sonarjs' recommended rules at error minus the owned ones,
51
+ * with S3776's limit, then SONAR_WAY_EXTRA), `mapped` every `{ sonar, rule }` row.
52
+ */
53
+ export function sonarWayRules(sonarjs) {
54
+ const mapped = [];
55
+ const rules = {};
56
+ for (const [name, setting] of Object.entries(sonarjs.configs.recommended.rules)) {
57
+ if ((Array.isArray(setting) ? setting[0] : setting) !== 'error' || SONAR_WAY_OWNED[name]) continue;
58
+ const declared = Array.isArray(setting) ? setting.slice(1) : [];
59
+ const options = name === 'sonarjs/cognitive-complexity' ? [COGNITIVE_COMPLEXITY_LIMIT] : declared;
60
+ rules[name] = ['error', ...options];
61
+ mapped.push({ sonar: sonarIdOf(sonarjs.rules[name.slice('sonarjs/'.length)]), rule: name });
62
+ }
63
+ for (const entry of SONAR_WAY_EXTRA) {
64
+ rules[entry.rule] = ['error', ...(entry.options ?? [])];
65
+ mapped.push({ sonar: entry.sonar, rule: entry.rule });
66
+ }
67
+ return { rules, mapped };
68
+ }
package/sonar-parity.mjs CHANGED
@@ -3,7 +3,8 @@
3
3
  *
4
4
  * Each rule is the syntax-tree form of one Sonar rule, shared with the front-end canon (scripts/lib/sonar-syntax-rules.mjs,
5
5
  * bundled under `runtime/`): `no-await-in-loop` (S9382), `prefer-code-point` (S7758), `prefer-string-raw` (S7780),
6
- * `no-nested-conditional` (S3358), `no-void-operator` (S3735), `no-unused-import` (S1128) and `prefer-export-from` (S7763).
6
+ * `no-nested-conditional` (S3358), `no-void-operator` (S3735), `no-unused-import` (S1128) and `prefer-export-from` (S7763); the rest of
7
+ * Sonar way is the factory's Sonar way block (scripts/lib/sonar-way.mjs).
7
8
  * The three Sonar rules that need the type checker, S1874 (a deprecated API), S6551 (an object turned into text) and S7503 (an `async`
8
9
  * function that never awaits and returns no promise), are the typescript-eslint rules `no-deprecated`, `no-base-to-string` and
9
10
  * `require-await` the factory turns on.