@starci/eslint-canon-fe 8.4.28 → 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 +9 -0
- package/index.mjs +28 -1
- package/lib/config.mjs +68 -2
- package/package.json +1 -1
- package/runtime/knowledge/hfs/canon-pins.yaml +11 -11
- package/runtime/knowledge/hfs/slots.yaml +20 -1
- package/runtime/knowledge/sonar-gate.yaml +12 -12
- package/runtime/packages/hfs/templates/app/package-scripts/package.json +0 -1
- package/runtime/scripts/hfs/architecture/automatic-gates.mjs +1 -1
- package/runtime/scripts/hfs/slots.mjs +3 -2
- package/runtime/scripts/lib/sonar-way.mjs +26 -14
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
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
|
+
|
|
9
|
+
## 8.4.29 - 2026-10-22
|
|
10
|
+
- Changed: the bundled canon pins select @starci/hfs 4.7.23 and @starci/cli 1.7.23.
|
|
11
|
+
|
|
3
12
|
## 8.4.28 - 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/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
|
-
/**
|
|
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.
|
|
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.
|
|
32
|
+
version: 3.4.31
|
|
33
33
|
group: starci
|
|
34
34
|
install: registry
|
|
35
35
|
side: be
|
|
36
36
|
source: packages/eslint/be/package.json
|
|
37
37
|
why: 'the back-end ESLint canon: `loadHfs(import.meta.url)` of be/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the be side, and the project graph is built per side; its bundled runtime copies (slots, canon-pins, failure codes, the architecture machine) put .starcistacks and .sops.yaml at the app root; `starciBeConfig({ hfs: loadHfs(import.meta.url) })` is the typed factory and the BE-CONVENTION laws are its rules.'
|
|
38
38
|
'@starci/eslint-canon-fe':
|
|
39
|
-
version: 8.4.
|
|
39
|
+
version: 8.4.30
|
|
40
40
|
group: starci
|
|
41
41
|
install: registry
|
|
42
42
|
side: fe
|
|
@@ -50,40 +50,40 @@ pins:
|
|
|
50
50
|
source: packages/stylelint/package.json
|
|
51
51
|
why: 'the front-end stylelint canon: no-class-selector refuses a compound class selector (`div.card`); the brand layer accepts `--font-sans` and `--font-mono`; status-contrast measures the HeroUI soft pairs per theme; brand-layer-shape reads the shared :root,.light,.dark block and the info soft pair; appTokens come from loadAppTokens(import.meta.url) for the managed one-line stylelint.config.mjs.'
|
|
52
52
|
'@starci/tsconfig':
|
|
53
|
-
version: 2.0.
|
|
53
|
+
version: 2.0.7
|
|
54
54
|
group: starci
|
|
55
55
|
install: registry
|
|
56
56
|
side: both
|
|
57
57
|
source: packages/tsconfig/package.json
|
|
58
58
|
'@starci/prettier-config':
|
|
59
|
-
version: 1.0.
|
|
59
|
+
version: 1.0.4
|
|
60
60
|
group: starci
|
|
61
61
|
install: registry
|
|
62
62
|
side: both
|
|
63
63
|
source: packages/prettier-config/package.json
|
|
64
64
|
'@starci/jest-preset':
|
|
65
|
-
version: 2.3.
|
|
65
|
+
version: 2.3.5
|
|
66
66
|
group: starci
|
|
67
67
|
install: registry
|
|
68
68
|
side: be
|
|
69
69
|
source: packages/jest-preset/package.json
|
|
70
70
|
why: 'the Jest preset of a back end: the world runner runs up to min(--maxWorkers, slots) world files at once, each in its own process bound to one data slot of the test world (STARCI_TEST_WORLD_SLOT), and reads test-world state protocol 2 only: it pairs exactly with @starci/test-world, and any other state file is JEST_PRESET_WORLD_PAIR_MISMATCH. The unit run writes the lcov Sonar and Codecov import (services only); the integration, e2e and contract projects run every spec file in a worker process of its own (world-runner.cjs), so a process-global framework registry (@nestjs/graphql type metadata) never leaks between e2e files; it ships the unit kit (mockEntityManager, fakeTransaction, fakeIds, FakeClock, Outcome matchers) and the recordingOutbox claim side.'
|
|
71
71
|
'@starci/test-world':
|
|
72
|
-
version: 1.2.
|
|
72
|
+
version: 1.2.3
|
|
73
73
|
group: starci
|
|
74
74
|
install: registry
|
|
75
75
|
side: be
|
|
76
76
|
source: packages/test-world/package.json
|
|
77
77
|
why: 'the shared e2e library of every back end (R47, R48), a devDependency of every back end: the warm stack behind toxiproxy, the network-edge fakes, the Nest boot, the typed useTestWorld handle, useSandbox for contract specs and the outage lock. Kafka is real own infra (the one digest-pinned apache/kafka KRaft image, a broker listener and proxy per slot, slot-prefixed topics, consumer groups and client ids) and Postgres is schema-per-context (a schema and a login role per context in a shared database). Each worker has its own data slot (<ns>_w<k>) with its own databases, Keycloak realm, leased Redis DB, bucket/collection/topic prefixes, fakes host, toxiproxy proxies and outage lock; a run provisions min(jest workers, workers cap, default 2) slots, and state file protocol 2 pairs exactly with @starci/jest-preset (TEST_WORLD_PAIR_MISMATCH otherwise). A modules world boots real peer apps beside its modules, world.apps.<name>.during(fn) is the outage of a peer app, w.keycloak.clientSecret(client) answers the run-generated secret of a confidential realm client, world.keycloak.events/sessions read the realm user events and live sessions, world.infra.postgresql.connection(name) takes one database connection down while the others serve, and world.resolve and scope.resolve take any Nest token. Every path a declaration names (`stack`, seeds, the realm, Dockerfiles) resolves from the app root (the directory of hfs.json); the globalSetup registers the path aliases as TypeScript resolves them. Its CLI main is exported at ./cli for starci app stack.'
|
|
78
78
|
'@starci/cli':
|
|
79
|
-
version: 1.7.
|
|
79
|
+
version: 1.7.24
|
|
80
80
|
group: starci
|
|
81
81
|
install: registry
|
|
82
82
|
side: both
|
|
83
83
|
source: packages/cli/package.json
|
|
84
84
|
why: 'the only StarCi command package; product repositories install it for starci app, while @starci/hfs remains its pinned transitive implementation.'
|
|
85
85
|
'@starci/hfs':
|
|
86
|
-
version: 4.7.
|
|
86
|
+
version: 4.7.24
|
|
87
87
|
group: starci
|
|
88
88
|
install: registry
|
|
89
89
|
side: both
|
|
@@ -465,7 +465,7 @@ pins:
|
|
|
465
465
|
version: 13.4.5
|
|
466
466
|
group: framework
|
|
467
467
|
side: be
|
|
468
|
-
why: 'the GraphQL transport of every back end (be.transport.graphql) that the front end calls; the
|
|
468
|
+
why: 'the GraphQL transport of every back end (be.transport.graphql) that the front end calls; the full-app resolution on 2026-10-13. Stays on 13.x for alpha.18: the npm-audit advisory fixed by 14.0.3 (semver major) is a documented accepted exception, upgrade deferred to alpha.19 (owner ruling graphql-13-advisory-accepted-until-a19, 2026-10-16).'
|
|
469
469
|
'@graphql-tools/utils':
|
|
470
470
|
version: 12.1.0
|
|
471
471
|
group: framework
|
|
@@ -475,12 +475,12 @@ pins:
|
|
|
475
475
|
version: 13.4.5
|
|
476
476
|
group: framework
|
|
477
477
|
side: be
|
|
478
|
-
why: 'the Apollo driver of @nestjs/graphql; the
|
|
478
|
+
why: 'the Apollo driver of @nestjs/graphql; the full-app resolution on 2026-10-13. Stays on 13.x with @nestjs/graphql (accepted advisory, major deferred to alpha.19).'
|
|
479
479
|
'@apollo/server':
|
|
480
480
|
version: 5.5.1
|
|
481
481
|
group: framework
|
|
482
482
|
side: be
|
|
483
|
-
why: 'the Apollo server behind @nestjs/apollo; the
|
|
483
|
+
why: 'the Apollo server behind @nestjs/apollo; the full-app resolution on 2026-10-13.'
|
|
484
484
|
'@nestjs/websockets':
|
|
485
485
|
version: 11.2.5
|
|
486
486
|
group: framework
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
# catalog READ this file through scripts/hfs/slots.mjs; none of them hardcodes a path.
|
|
8
8
|
# Shape: modules/schemas/hfs-slots.schema.yaml. The repository side is hfs.json: modules/schemas/hfs-repo.schema.yaml.
|
|
9
9
|
schema: starci/hfs-slots@3
|
|
10
|
-
version: 3.
|
|
10
|
+
version: 3.5.0
|
|
11
11
|
|
|
12
12
|
versioning:
|
|
13
13
|
# MAJOR.MINOR.PATCH of this manifest. A repository pins only the MAJOR in hfs.json ("hfs": 3).
|
|
@@ -39,6 +39,8 @@ versioning:
|
|
|
39
39
|
# when the file key is absent; .starcistacks owns custody. Capability <capability>.config.ts roles stay unchanged.
|
|
40
40
|
# 3.3.0 is a minor (alpha.16): it ADDS the optional app.ci-stack-deploy slot, the managed workflow of the starci app stack verbs.
|
|
41
41
|
# 3.4.0 is a minor (alpha.25): it ADDS the opt-in be.contract.mcp-tools slot, the committed MCP tool-catalog snapshot of an app.
|
|
42
|
+
# 3.5.0 is a minor (alpha.27): it ADDS the opt-in app.browser-config slot, the managed browser/tsconfig.json that type-checks the browser
|
|
43
|
+
# journey (enabled with app.browser by `browser: true`).
|
|
42
44
|
|
|
43
45
|
# Field meaning, per slot.
|
|
44
46
|
# id stable name, never changes meaning inside a major.
|
|
@@ -617,6 +619,23 @@ slots:
|
|
|
617
619
|
tests: none
|
|
618
620
|
requires: [playwright.config.ts, journeys/]
|
|
619
621
|
allows: [playwright.config.ts, stack.mjs, "journeys/*.ts"]
|
|
622
|
+
- id: app.browser-config
|
|
623
|
+
since: 3.5.0
|
|
624
|
+
profiles: [app]
|
|
625
|
+
editionWhy: "ruling:R212/case-2 (with the Work boundary amended by ruling:R212/case-4): browser journeys belong to full; lite proves app boot through R272 wiring."
|
|
626
|
+
# The program that type-checks the browser journey: Playwright only transpiles the config and the journeys, so without it nothing
|
|
627
|
+
# type-checks them and the op gate finds no tsconfig.json owning a changed journey (GATE_TSC_CONFIG_MISSING). It is managed (rendered
|
|
628
|
+
# by starci app sync from templates/app/browser-config, extending @starci/tsconfig/e2e.json), never authored, and enabled with
|
|
629
|
+
# app.browser by `browser: true` in hfs.json.
|
|
630
|
+
path: browser/tsconfig.json
|
|
631
|
+
presence: opt-in
|
|
632
|
+
litePresence: forbidden
|
|
633
|
+
goesTo: "the full edition: a lite app has no browser journey to type-check"
|
|
634
|
+
tracked: tracked
|
|
635
|
+
tier: none
|
|
636
|
+
tests: none
|
|
637
|
+
managedBy: browser-config
|
|
638
|
+
rules: [HFS_MANAGED_FILE_DRIFT]
|
|
620
639
|
- id: app.github-meta
|
|
621
640
|
since: 3.1.0
|
|
622
641
|
profiles: [app]
|
|
@@ -8,7 +8,7 @@ purpose: |
|
|
|
8
8
|
conditions and selects it for the project, and (2) judges an op's slice - the lines it changed - against
|
|
9
9
|
the same numbers. Code-writing ops cannot settle done while the slice is red
|
|
10
10
|
(scripts/kernel/sonar-settle.mjs). Changing a number here changes every repository on the next scan.
|
|
11
|
-
The gate has two parts: `newCode` (the recent work: coverage, duplication,
|
|
11
|
+
The gate has two parts: `newCode` (the recent work: coverage, duplication, issues of every severity, hotspots) and
|
|
12
12
|
`overall` (the whole code: coverage, no imported HFS, ESLint or stylelint finding, no bug, code smell or
|
|
13
13
|
vulnerability, every hotspot reviewed, no duplicated-lines excess, no cognitive-complexity issue). The one Sonar
|
|
14
14
|
mechanism of the HFS canon is packages/hfs/README.md section "Sonar".
|
|
@@ -26,15 +26,15 @@ newCode:
|
|
|
26
26
|
coverage:
|
|
27
27
|
metric: new_coverage
|
|
28
28
|
minPercent: 100
|
|
29
|
-
# SonarQube's ignoreSmallChanges:
|
|
30
|
-
ignoreBelowChangedLines:
|
|
29
|
+
# SonarQube's ignoreSmallChanges floor: 0, so a change of any size is held to duplication.
|
|
30
|
+
ignoreBelowChangedLines: 0
|
|
31
31
|
duplication:
|
|
32
32
|
metric: new_duplicated_lines_density
|
|
33
33
|
maxPercent: 3
|
|
34
34
|
issues:
|
|
35
|
-
# An open issue of
|
|
36
|
-
blockingSeverities: [BLOCKER, CRITICAL]
|
|
37
|
-
metrics: {BLOCKER: new_blocker_violations, CRITICAL: new_critical_violations}
|
|
35
|
+
# An open issue of any severity on a changed line fails the slice: there is no lesser severity that is only listed.
|
|
36
|
+
blockingSeverities: [BLOCKER, CRITICAL, MAJOR, MINOR, INFO]
|
|
37
|
+
metrics: {BLOCKER: new_blocker_violations, CRITICAL: new_critical_violations, MAJOR: new_major_violations, MINOR: new_minor_violations, INFO: new_info_violations}
|
|
38
38
|
max: 0
|
|
39
39
|
hotspots:
|
|
40
40
|
metric: new_security_hotspots_reviewed
|
|
@@ -92,13 +92,13 @@ lite:
|
|
|
92
92
|
coverage:
|
|
93
93
|
metric: new_coverage
|
|
94
94
|
minPercent: 100
|
|
95
|
-
ignoreBelowChangedLines:
|
|
95
|
+
ignoreBelowChangedLines: 0
|
|
96
96
|
duplication:
|
|
97
97
|
metric: new_duplicated_lines_density
|
|
98
98
|
maxPercent: 3
|
|
99
99
|
issues:
|
|
100
|
-
blockingSeverities: [BLOCKER, CRITICAL]
|
|
101
|
-
metrics: {BLOCKER: new_blocker_violations, CRITICAL: new_critical_violations}
|
|
100
|
+
blockingSeverities: [BLOCKER, CRITICAL, MAJOR, MINOR, INFO]
|
|
101
|
+
metrics: {BLOCKER: new_blocker_violations, CRITICAL: new_critical_violations, MAJOR: new_major_violations, MINOR: new_minor_violations, INFO: new_info_violations}
|
|
102
102
|
max: 0
|
|
103
103
|
hotspots:
|
|
104
104
|
metric: new_security_hotspots_reviewed
|
|
@@ -139,9 +139,9 @@ reasoning: |
|
|
|
139
139
|
lives there, the feature tier is thin (R203) and proven by integration and e2e, and the front end has no unit tests.
|
|
140
140
|
duplication 3: the Sonar way value; the worst project today is 2.1, so 3 rejects a real copy-paste block and
|
|
141
141
|
passes ordinary work.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
142
|
+
issues 0 at every severity (blocker, critical, major, minor, info), owner ruling 2026-10-22 (no loosening): a lesser severity is not a
|
|
143
|
+
lesser finding. The SLICE verdict reads only changed lines and never main's backlog; the overall condition holds the whole project.
|
|
144
|
+
A change of any size is held to duplication (ignoreBelowChangedLines 0).
|
|
145
145
|
overall issues 0 and overall duplication 3: the server gate is also the owner's definition of done for the HFS canon:
|
|
146
146
|
a repository that carries any open ESLint, stylelint or HFS finding is not green, whatever the age of the line. The
|
|
147
147
|
op slice keeps judging changed lines (an op cannot fix a repository's whole backlog); the dashboard and CI show the
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
{{> app/package-scripts/core.json}}
|
|
6
6
|
"typecheck:tests": "tsc -p be/src/tests/tsconfig.json",
|
|
7
7
|
"test": "cd be && jest --selectProjects unit --coverage",
|
|
8
|
-
"test:affected": "cd be && jest --selectProjects unit --passWithNoTests",
|
|
9
8
|
"test:integration": "npm run typecheck:tests && cd be && jest --selectProjects integration",
|
|
10
9
|
"test:e2e": "npm run typecheck:tests && cd be && jest --selectProjects e2e",
|
|
11
10
|
"test:contract": "npm run typecheck:tests && cd be && jest --selectProjects contract",
|
|
@@ -10,7 +10,7 @@ const E2E_COMMAND = new RegExp([
|
|
|
10
10
|
String.raw`src\/tests\/(?:world|integration|e2e|contract)\b|`,
|
|
11
11
|
String.raw`jest[^\n|&;]*(?:e2e|integration|contract)`,
|
|
12
12
|
].join(''), 'u');
|
|
13
|
-
const UNIT_RUN_SCRIPTS = ['test', 'test:unit', 'test:ci', 'test:
|
|
13
|
+
const UNIT_RUN_SCRIPTS = ['test', 'test:unit', 'test:ci', 'test:coverage', 'test:cov'];
|
|
14
14
|
|
|
15
15
|
const runsE2e = text => E2E_COMMAND.test(String(text).replace(/--ignore-pattern[= ]+(?:"[^"]*"|'[^']*'|\S+)/gu, ''));
|
|
16
16
|
const withoutComments = text => text.split('\n').filter(line => !/^\s*#/u.test(line)).join('\n');
|
|
@@ -153,9 +153,10 @@ export function resolveRepoDeclaration(manifest, declaration, { file = HFS_DECLA
|
|
|
153
153
|
edition,
|
|
154
154
|
side: null,
|
|
155
155
|
profile: APP_SCOPE,
|
|
156
|
-
// The root declares no app or connection of its own (each side does); its
|
|
156
|
+
// The root declares no app or connection of its own (each side does); its opt-in slots are the browser journey and the managed
|
|
157
|
+
// tsconfig.json that type-checks it, both switched on by `browser: true`.
|
|
157
158
|
apps: Object.freeze([]),
|
|
158
|
-
optionalSlots: Object.freeze(declaration.browser === true ? ['app.browser'] : []),
|
|
159
|
+
optionalSlots: Object.freeze(declaration.browser === true ? ['app.browser', 'app.browser-config'] : []),
|
|
159
160
|
connections: Object.freeze([]),
|
|
160
161
|
providers,
|
|
161
162
|
reads: Object.freeze([]),
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// the one mapping both lint canons (@starci/eslint-canon-be and -fe) publish, so `starci app lint` in the op loop refuses what the scan would
|
|
3
3
|
// raise while the code is written. The base is eslint-plugin-sonarjs's recommended config, SonarSource's own port of Sonar way (each rule's
|
|
4
4
|
// docs url names its S-id); its rules the canons already publish in another form stay with that owner (SONAR_WAY_OWNED), and the Sonar way
|
|
5
|
-
// rules SonarSource builds on other ESLint rules are SONAR_WAY_EXTRA. Every rule is `error
|
|
5
|
+
// rules SonarSource builds on other ESLint rules are SONAR_WAY_EXTRA. Every rule is `error`; no rule is left to the server because its port
|
|
6
|
+
// disagrees with it (a port that disagrees is fixed in the source it reports, never switched off). A unit or end-to-end spec is outside the scan,
|
|
6
7
|
// so outside these rules: the canons give this block the ignores of sonar-syntax-rules.mjs OUTSIDE_THE_SCAN_FILES. The runtime's own
|
|
7
8
|
// `sonar-rules` gate reads the two limits below.
|
|
8
9
|
|
|
@@ -11,31 +12,36 @@ export const COGNITIVE_COMPLEXITY_LIMIT = 15;
|
|
|
11
12
|
export const MAX_PARAMETERS = 7;
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
|
-
* sonarjs rules of Sonar way the local lint does not run
|
|
15
|
-
*
|
|
16
|
-
* (Sonar way, 2026-10-21) while these ports reported 130 findings on it; a rule that cannot match the server is not a lint error.
|
|
15
|
+
* sonarjs rules of Sonar way the local lint does not run under their sonarjs name because another owner enforces the same Sonar id at error:
|
|
16
|
+
* a canon syntax rule, a borrowed typescript-eslint rule or the TypeScript compiler. Each owner is asserted in packages/eslint/be/sonar-way.spec.mjs and packages/eslint/fe/config.spec.mjs.
|
|
17
17
|
*/
|
|
18
18
|
const SONAR_WAY_OWNED = Object.freeze({
|
|
19
19
|
'sonarjs/no-nested-conditional': 'S3358, the canon syntax rule no-nested-conditional',
|
|
20
20
|
'sonarjs/unused-import': 'S1128, the canon syntax rule no-unused-import',
|
|
21
21
|
'sonarjs/void-use': 'S3735, the canon syntax rule no-void-operator',
|
|
22
22
|
'sonarjs/deprecation': 'S1874, the borrowed @typescript-eslint/no-deprecated',
|
|
23
|
-
'sonarjs/no-unused-vars': 'S1481, the TypeScript compiler (noUnusedLocals in @starci/tsconfig)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
'sonarjs/no-unused-vars': 'S1481, the TypeScript compiler (noUnusedLocals and noUnusedParameters in @starci/tsconfig)',
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Sonar way rules with no ESLint form that applies to the module format of a side. S7785 prefers top-level await, which a CommonJS output
|
|
28
|
+
* (the back end: `module: nodenext` without `type: module`) cannot contain; the front end is ESM and runs the rule.
|
|
29
|
+
*/
|
|
30
|
+
export const SONAR_WAY_NOT_APPLICABLE = Object.freeze({
|
|
31
|
+
be: Object.freeze({ S7785: 'top-level await does not exist in the CommonJS output of a back end' }),
|
|
32
|
+
fe: Object.freeze({}),
|
|
27
33
|
});
|
|
28
34
|
|
|
29
35
|
/**
|
|
30
36
|
* The Sonar way rules SonarSource implements on an ESLint rule outside eslint-plugin-sonarjs: `{ sonar, rule, options? }`. The canon syntax
|
|
31
37
|
* rules carry S107 (`max-params`, which spares the constructor of an injectable class as the server does) and S4084 (`media-has-caption`,
|
|
32
|
-
* front end)
|
|
33
|
-
* S6582 (typescript-eslint's optional-chain rule reports guards whose optional chain would change the value's type, which the server accepts).
|
|
38
|
+
* front end); S6582 is typescript-eslint's `prefer-optional-chain`, which needs the type information the canons' typed block provides.
|
|
34
39
|
*/
|
|
35
40
|
export const SONAR_WAY_EXTRA = Object.freeze([
|
|
36
41
|
{ sonar: 'S3863', rule: 'no-duplicate-imports', options: [{ allowSeparateTypeImports: true }] },
|
|
42
|
+
{ sonar: 'S6582', rule: '@typescript-eslint/prefer-optional-chain' },
|
|
37
43
|
{ sonar: 'S4138', rule: 'unicorn/no-for-loop' },
|
|
38
|
-
{ sonar: 'S7721', rule: 'unicorn/consistent-function-scoping'
|
|
44
|
+
{ sonar: 'S7721', rule: 'unicorn/consistent-function-scoping' },
|
|
39
45
|
{ sonar: 'S7732', rule: 'unicorn/no-instanceof-builtins' },
|
|
40
46
|
{ sonar: 'S7740', rule: 'unicorn/no-this-assignment' },
|
|
41
47
|
{ sonar: 'S7744', rule: 'unicorn/no-useless-fallback-in-spread' },
|
|
@@ -50,14 +56,20 @@ export const SONAR_WAY_EXTRA = Object.freeze([
|
|
|
50
56
|
{ sonar: 'S7784', rule: 'unicorn/prefer-structured-clone' },
|
|
51
57
|
]);
|
|
52
58
|
|
|
59
|
+
/** The ESM-only Sonar way rules: S7785 prefers top-level await (see SONAR_WAY_NOT_APPLICABLE). */
|
|
60
|
+
const SONAR_WAY_ESM = Object.freeze([{ sonar: 'S7785', rule: 'unicorn/prefer-top-level-await' }]);
|
|
61
|
+
|
|
53
62
|
/** The Sonar id a sonarjs rule documents (its docs url ends in `#/rspec/S<id>/javascript`), or null. */
|
|
54
63
|
const sonarIdOf = (rule) => /\/rspec\/#\/rspec\/(S\d+)\//u.exec(rule?.meta?.docs?.url ?? '')?.[1] ?? null;
|
|
55
64
|
|
|
56
65
|
/**
|
|
57
66
|
* The Sonar way rules, `{ rules, mapped }`: `rules` the flat-config rule levels (sonarjs' recommended rules at error minus the owned ones,
|
|
58
|
-
* with S3776's limit, then SONAR_WAY_EXTRA), `mapped` every `{ sonar, rule }` row.
|
|
67
|
+
* with S3776's limit, then SONAR_WAY_EXTRA, then the ESM-only rules unless `side` is `be`), `mapped` every `{ sonar, rule }` row.
|
|
68
|
+
*
|
|
69
|
+
* @param {object} sonarjs - The eslint-plugin-sonarjs plugin.
|
|
70
|
+
* @param {'be' | 'fe'} [side] - The side the rules lint; the back end compiles to CommonJS.
|
|
59
71
|
*/
|
|
60
|
-
export function sonarWayRules(sonarjs) {
|
|
72
|
+
export function sonarWayRules(sonarjs, side = 'fe') {
|
|
61
73
|
const mapped = [];
|
|
62
74
|
const rules = {};
|
|
63
75
|
for (const [name, setting] of Object.entries(sonarjs.configs.recommended.rules)) {
|
|
@@ -67,7 +79,7 @@ export function sonarWayRules(sonarjs) {
|
|
|
67
79
|
rules[name] = ['error', ...options];
|
|
68
80
|
mapped.push({ sonar: sonarIdOf(sonarjs.rules[name.slice('sonarjs/'.length)]), rule: name });
|
|
69
81
|
}
|
|
70
|
-
for (const entry of SONAR_WAY_EXTRA) {
|
|
82
|
+
for (const entry of [...SONAR_WAY_EXTRA, ...SONAR_WAY_ESM.filter(row => !SONAR_WAY_NOT_APPLICABLE[side][row.sonar])]) {
|
|
71
83
|
rules[entry.rule] = ['error', ...(entry.options ?? [])];
|
|
72
84
|
mapped.push({ sonar: entry.sonar, rule: entry.rule });
|
|
73
85
|
}
|