@starci/eslint-canon-fe 8.0.9 → 8.1.0

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.
Files changed (149) hide show
  1. package/CHANGELOG.md +2 -1
  2. package/LICENSE +21 -0
  3. package/README.md +2 -1
  4. package/comments.mjs +4 -5
  5. package/docs/lint-escape-hatch.md +4 -4
  6. package/docs/translation.md +2 -2
  7. package/file-layout.mjs +1 -1
  8. package/icon.mjs +2 -2
  9. package/index.mjs +6 -0
  10. package/lib/config.mjs +11 -3
  11. package/lib/types.mjs +1 -1
  12. package/lib/why.mjs +2 -2
  13. package/lint-escape-hatch.mjs +1 -8
  14. package/next-conventions.mjs +4 -4
  15. package/package.json +4 -2
  16. package/runtime/config.example.yaml +187 -0
  17. package/runtime/engine/by-code-unit.mjs +12 -0
  18. package/runtime/engine/config.mjs +247 -273
  19. package/runtime/engine/invalid-config.mjs +5 -5
  20. package/runtime/engine/model-config.mjs +90 -0
  21. package/runtime/engine/orca-config.mjs +3 -1
  22. package/runtime/engine/removed-vocabulary.mjs +28 -0
  23. package/runtime/engine/resources-config.mjs +52 -0
  24. package/runtime/engine/runtime-root.mjs +17 -0
  25. package/runtime/engine/secrets.mjs +134 -0
  26. package/runtime/engine/sonar-config.mjs +21 -0
  27. package/runtime/engine/temp-root.mjs +32 -0
  28. package/runtime/knowledge/hfs/canon-pins.yaml +18 -18
  29. package/runtime/knowledge/hfs/slots.yaml +11 -14
  30. package/runtime/modules/kernel/removed-vocabulary.yaml +168 -0
  31. package/runtime/modules/models/registry.yaml +25 -397
  32. package/runtime/modules/models/runtimes.yaml +92 -262
  33. package/runtime/modules/models/tiers.yaml +65 -0
  34. package/runtime/scripts/api/git/lib.mjs +36 -1
  35. package/runtime/scripts/hfs/allows.mjs +3 -3
  36. package/runtime/scripts/hfs/architecture/ast-walks.mjs +63 -50
  37. package/runtime/scripts/hfs/architecture/automatic-gates.mjs +96 -0
  38. package/runtime/scripts/hfs/architecture/backend.mjs +261 -175
  39. package/runtime/scripts/hfs/architecture/background-unowned.mjs +69 -34
  40. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +55 -54
  41. package/runtime/scripts/hfs/architecture/clones.mjs +179 -106
  42. package/runtime/scripts/hfs/architecture/config-unread.mjs +5 -22
  43. package/runtime/scripts/hfs/architecture/config.mjs +95 -80
  44. package/runtime/scripts/hfs/architecture/connection-map.mjs +233 -141
  45. package/runtime/scripts/hfs/architecture/constructor-deps.mjs +15 -9
  46. package/runtime/scripts/hfs/architecture/context-coupling.mjs +76 -52
  47. package/runtime/scripts/hfs/architecture/context-map.mjs +215 -141
  48. package/runtime/scripts/hfs/architecture/context-owner.mjs +39 -30
  49. package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +31 -17
  50. package/runtime/scripts/hfs/architecture/context-transaction.mjs +38 -25
  51. package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +86 -40
  52. package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +342 -0
  53. package/runtime/scripts/hfs/architecture/contracts.mjs +263 -455
  54. package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +43 -34
  55. package/runtime/scripts/hfs/architecture/dead-exports.mjs +201 -154
  56. package/runtime/scripts/hfs/architecture/default-deny.mjs +118 -93
  57. package/runtime/scripts/hfs/architecture/doc-language.mjs +2 -1
  58. package/runtime/scripts/hfs/architecture/entrypoint.mjs +45 -30
  59. package/runtime/scripts/hfs/architecture/error-codes.mjs +19 -12
  60. package/runtime/scripts/hfs/architecture/error-masked.mjs +39 -26
  61. package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +3 -2
  62. package/runtime/scripts/hfs/architecture/feature-shape.mjs +26 -20
  63. package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
  64. package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +190 -0
  65. package/runtime/scripts/hfs/architecture/frontend-routing.mjs +186 -0
  66. package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +311 -0
  67. package/runtime/scripts/hfs/architecture/frontend-world.mjs +328 -0
  68. package/runtime/scripts/hfs/architecture/frontend.mjs +109 -829
  69. package/runtime/scripts/hfs/architecture/hfs-graph.mjs +14 -2
  70. package/runtime/scripts/hfs/architecture/hfs.mjs +120 -288
  71. package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +46 -26
  72. package/runtime/scripts/hfs/architecture/i18n-keys.mjs +114 -76
  73. package/runtime/scripts/hfs/architecture/index.mjs +158 -102
  74. package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +33 -28
  75. package/runtime/scripts/hfs/architecture/machine-ast.mjs +180 -144
  76. package/runtime/scripts/hfs/architecture/managed-scripts.mjs +8 -2
  77. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +150 -101
  78. package/runtime/scripts/hfs/architecture/next-data-contract.mjs +51 -43
  79. package/runtime/scripts/hfs/architecture/next-data-key.mjs +338 -0
  80. package/runtime/scripts/hfs/architecture/next-data.mjs +179 -427
  81. package/runtime/scripts/hfs/architecture/owners.mjs +27 -13
  82. package/runtime/scripts/hfs/architecture/package-shape.mjs +51 -23
  83. package/runtime/scripts/hfs/architecture/presentation.mjs +165 -0
  84. package/runtime/scripts/hfs/architecture/reachability.mjs +127 -61
  85. package/runtime/scripts/hfs/architecture/register-once.mjs +112 -63
  86. package/runtime/scripts/hfs/architecture/registration.mjs +187 -110
  87. package/runtime/scripts/hfs/architecture/required-files.mjs +100 -68
  88. package/runtime/scripts/hfs/architecture/route-files-thin.mjs +83 -58
  89. package/runtime/scripts/hfs/architecture/schema-owner.mjs +182 -108
  90. package/runtime/scripts/hfs/architecture/source-names-shape.mjs +318 -0
  91. package/runtime/scripts/hfs/architecture/source-names.mjs +83 -248
  92. package/runtime/scripts/hfs/architecture/sql-owner.mjs +175 -118
  93. package/runtime/scripts/hfs/architecture/sql-returning.mjs +39 -28
  94. package/runtime/scripts/hfs/architecture/sql-tokens.mjs +302 -172
  95. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +1 -1
  96. package/runtime/scripts/hfs/architecture/supabase-be.mjs +55 -45
  97. package/runtime/scripts/hfs/architecture/supabase-results.mjs +182 -0
  98. package/runtime/scripts/hfs/architecture/supabase-tables.mjs +20 -12
  99. package/runtime/scripts/hfs/architecture/supabase.mjs +191 -263
  100. package/runtime/scripts/hfs/architecture/symbols.mjs +175 -99
  101. package/runtime/scripts/hfs/architecture/test-world-files.mjs +82 -41
  102. package/runtime/scripts/hfs/architecture/tiers.mjs +120 -69
  103. package/runtime/scripts/hfs/architecture/transport-owner.mjs +84 -57
  104. package/runtime/scripts/hfs/architecture/type-context.mjs +282 -0
  105. package/runtime/scripts/hfs/architecture/typescript.mjs +116 -276
  106. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +84 -62
  107. package/runtime/scripts/hfs/declaration-shape.mjs +77 -36
  108. package/runtime/scripts/hfs/declaration-slots.mjs +1 -1
  109. package/runtime/scripts/hfs/edition-slots.mjs +5 -3
  110. package/runtime/scripts/hfs/linear-text.mjs +30 -0
  111. package/runtime/scripts/hfs/manifest-shape.mjs +164 -63
  112. package/runtime/scripts/hfs/path-findings.mjs +58 -38
  113. package/runtime/scripts/hfs/project-graph.mjs +4 -1
  114. package/runtime/scripts/hfs/repo-identity.mjs +5 -2
  115. package/runtime/scripts/hfs/rule-catalog.mjs +194 -0
  116. package/runtime/scripts/hfs/rule-params-shape.mjs +33 -27
  117. package/runtime/scripts/hfs/rules/fe-no-tests.mjs +36 -16
  118. package/runtime/scripts/hfs/rules/secrets.mjs +4 -3
  119. package/runtime/scripts/hfs/slot-app-view.mjs +102 -0
  120. package/runtime/scripts/hfs/slot-classify.mjs +65 -0
  121. package/runtime/scripts/hfs/slot-errors.mjs +11 -0
  122. package/runtime/scripts/hfs/slot-imports.mjs +76 -0
  123. package/runtime/scripts/hfs/slot-manifest-shape.mjs +73 -0
  124. package/runtime/scripts/hfs/slot-match.mjs +153 -0
  125. package/runtime/scripts/hfs/slot-path.mjs +8 -0
  126. package/runtime/scripts/hfs/slot-required.mjs +76 -0
  127. package/runtime/scripts/hfs/slot-semantic-problems.mjs +148 -0
  128. package/runtime/scripts/hfs/slot-side-problems.mjs +28 -0
  129. package/runtime/scripts/hfs/slots.mjs +72 -693
  130. package/runtime/scripts/hfs/trailing-slashes.mjs +8 -0
  131. package/runtime/scripts/hfs/typescript-programs.mjs +6 -4
  132. package/runtime/scripts/hfs/view.mjs +1 -1
  133. package/runtime/scripts/lib/dockerfile.mjs +56 -33
  134. package/runtime/scripts/lib/git.mjs +2 -2
  135. package/runtime/scripts/lib/i18n.mjs +3 -2
  136. package/runtime/scripts/lib/jsdoc.mjs +35 -0
  137. package/runtime/scripts/lib/language.mjs +3 -3
  138. package/runtime/scripts/lib/list.mjs +8 -1
  139. package/runtime/scripts/lib/mutation-fence.mjs +15 -0
  140. package/runtime/scripts/lib/path-key.mjs +42 -9
  141. package/runtime/scripts/lib/regex.mjs +1 -1
  142. package/runtime/scripts/lib/secret-patterns.mjs +6 -2
  143. package/runtime/scripts/lib/sonar-syntax-rules.mjs +225 -0
  144. package/runtime/scripts/lib/sops-envelope.mjs +103 -2
  145. package/runtime/scripts/lib/stack-services.mjs +1 -2
  146. package/runtime/scripts/lib/ts-ast.mjs +9 -0
  147. package/runtime/scripts/lib/walk.mjs +6 -1
  148. package/sonar-parity.mjs +19 -0
  149. package/translation.mjs +1 -4
package/CHANGELOG.md CHANGED
@@ -1,7 +1,8 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 8.1.0 - 2026-10-08
4
4
 
5
+ - New: the `sonar-parity` law (R235 `SCAN_SMELL`): `no-await-in-loop` (S9382), `prefer-code-point` (S7758), `prefer-string-raw` (S7780), `no-nested-conditional` (S3358), `no-void-operator` (S3735), `no-unused-import` (S1128), `prefer-export-from` (S7763) and `no-unused-prop-types` (S6767), each the syntax-tree form of the Sonar rule SonarCloud flagged in the example apps, shared with the back-end canon through the bundled `runtime/scripts/lib/sonar-syntax-rules.mjs`. The factory also turns on the typescript-eslint rules `no-deprecated` (S1874), `no-base-to-string` (S6551) and `require-await` (S7503); `@typescript-eslint/eslint-plugin` becomes a dependency of this package.
5
6
  - Changed: `one-outcome-union` reads the Outcome home from the slot manifest (`outcomeHome` on `fe.transport.outcome`, `fe.package.api.outcome` and the lite `fe.modules.db.outcome`, i.e. `modules/db/outcome.ts`) instead of two slot ids in code; a lite app's db owner is its transport, so its `modules/db/outcome.ts` is THE union and a second one anywhere else is still refused. Full is unchanged.
6
7
  - Changed: `public-component-signature` accepts a component with no parameter; `no-raw-structural-element` names `Region`, `NavLandmark`, `List` and `ListItem` as owners of section, nav, ul/ol and li; FE-I18N-1 and FE-TYPING-6 canon text (request locale through `next/root-params`, no `void props`); the FE_SWR_KEY_IDENTITY check reads `&&` and `||` gates in its bundled runtime.
7
8
  ## Unreleased (C0 batch)
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) StarCi contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -21,6 +21,7 @@ flat-config blocks and owns everything about the law: which files are linted (fr
21
21
  recommended set lifted to `error`), typed linting, and the inline-directive fence (`noInlineConfig` plus
22
22
  `reportUnusedDisableDirectives`). The repository states nothing.
23
23
 
24
+ - The **Sonar parity** law (R235): the smells SonarCloud measures that a lint rule can decide, `no-await-in-loop`, `prefer-code-point`, `prefer-string-raw`, `no-nested-conditional`, `no-void-operator`, `no-unused-import`, `prefer-export-from` and `no-unused-prop-types`, plus the typescript-eslint rules `no-deprecated`, `no-base-to-string` and `require-await` the factory turns on (they read types, so the source block is typed). A Next `matcher` string keeps its escapes (the segment config is read statically); `*.spec.ts` and `*.e2e-spec.ts` are outside the scan, so outside these rules.
24
25
  - The **ignore** block: `node_modules`, `.next`, `dist`, `coverage`, `__generated__` and `.starci/`.
25
26
  - The **source** block governs `apps/*/src/**` and `packages/*/src/**` with types (`parserOptions.projectService`) and
26
27
  `settings.starci.hfs`, the slot view path-scoped rules ask (`lib/hfs.mjs` `hfsOf`), never a path pattern.
@@ -34,7 +35,7 @@ Each law has a page under [`docs/`](docs) with, for every rule, an invalid and a
34
35
  its Vietnamese why. New in 5.0: [env-owner](docs/env-owner.md), [transport](docs/transport.md),
35
36
  [client-boundary](docs/client-boundary.md), [hooks-folder](docs/hooks-folder.md),
36
37
  [next-conventions](docs/next-conventions.md), [translation](docs/translation.md) (no literal copy at any
37
- tier, no `vn-ok`), [brand-values](docs/brand-values.md), [native-controls](docs/native-controls.md),
38
+ tier), [brand-values](docs/brand-values.md), [native-controls](docs/native-controls.md),
38
39
  [size-and-state-budget](docs/size-and-state-budget.md) and [lint-escape-hatch](docs/lint-escape-hatch.md). New in 5.1: [lists](docs/lists.md), [hygiene](docs/hygiene.md), [formatting](docs/formatting.md) and [type-safety](docs/type-safety.md), plus rules added to next-conventions, client-boundary, native-controls and transport. New in 6.1: `no-vietnamese-in-source` (English-only identifiers, strings, comments and test titles; the i18n fixtures slot `e2e/fixtures/i18n/` is the one exemption). New in 6.0: `effect-subscription-needs-cleanup` (timers, frames, listeners, observers, sockets and subscriptions are released against the same handle), `server-module-marks-server-only` ([client-boundary](docs/client-boundary.md)), `no-data-fetch-in-effect` sees indirect loads, `no-hardcoded-copy` reads templates and sentence values, `no-inline-lint-config` refuses `NOSONAR` and the coverage/formatter switches. Also new in 6.0: [grammar-boundary](docs/grammar-boundary.md) (`no-raw-structural-element`), `one-outcome-union` ([transport](docs/transport.md)), `i18n-stack-in-one-module` ([next-conventions](docs/next-conventions.md)), [size-growth](docs/size-growth.md) (`file-size-growth`: a file over the manifest's line budget is not born large and does not grow; the budget is `ruleParams.fe.fileLines`, read from the manifest copy in `runtime/`).
39
40
 
40
41
  Older laws cover comments, file layout, icons and vendor ownership, landmarks, loading states,
package/comments.mjs CHANGED
@@ -6,8 +6,7 @@
6
6
  * chunks and JSX text alike - everywhere prose can hide in a source file.
7
7
  *
8
8
  * `no-vietnamese-in-source` holds the English-only law: identifiers, string literals, template text, JSX text,
9
- * comments and test titles carry no Vietnamese letter. The old `vn-ok: <reason>` pragma is gone (it was used 490
10
- * times in one repository, and one escape is enough to turn a rule into a comment convention). Detection is
9
+ * comments and test titles carry no Vietnamese letter. There is no exemption marker. Detection is
11
10
  * structural on characters (`scripts/lib/language.mjs`, folded to NFC), never a word list.
12
11
  *
13
12
  * The exceptions that remain are placements, not judgements, and that is on purpose: a locale dictionary
@@ -16,6 +15,7 @@
16
15
  */
17
16
 
18
17
  import { hasSecondLanguage } from "./runtime/scripts/lib/language.mjs"
18
+ import { jsdocBefore } from "./runtime/scripts/lib/jsdoc.mjs"
19
19
  import { fileOf, inSlot } from "./lib/scope.mjs"
20
20
 
21
21
  /**
@@ -82,13 +82,12 @@ export const requireExportJsdoc = {
82
82
  schema: [],
83
83
  messages: {
84
84
  jsdoc:
85
- "`{{name}}` is exported with no documentation block. An export is read far more often than it is written, and by people who never open the body - name the ROLE it plays, not the signature, which the signature already states.",
85
+ "`{{name}}` is exported without an adjacent JSDoc description. An export is read far more often than it is written, and by people who never open the body - name the ROLE it plays, not the signature, which the signature already states.",
86
86
  },
87
87
  },
88
88
  create(context) {
89
89
  const source = context.sourceCode || context.getSourceCode()
90
- const hasBlock = (node) =>
91
- source.getCommentsBefore(node).some((comment) => comment.type === "Block" && comment.value.startsWith("*"))
90
+ const hasBlock = (node) => jsdocBefore(source, node) !== null
92
91
  const check = (node) => {
93
92
  const declaration = node.declaration
94
93
  if (!declaration) return
@@ -2,13 +2,13 @@
2
2
 
3
3
  Law module: `lint-escape-hatch.mjs`. Catalogue: R18 HFS_INLINE_SUPPRESSION.
4
4
 
5
- No `eslint-disable` (line, next-line, block), `eslint-enable`, `eslint-env`, inline `eslint rule: ...` config, `@ts-ignore`, `@ts-expect-error`, `@ts-nocheck`, no retired `vn-ok:` pragma, and no `NOSONAR`, `@sonar-ignore`, `istanbul ignore`, `c8 ignore`, `v8 ignore`, `prettier-ignore` or `stylelint-disable` comment. `noInlineConfig` makes a directive ineffective, this rule makes it a finding, and `reportUnusedDisableDirectives` reports a leftover that suppresses nothing. The rule covers `src/**`, specs and the e2e tree.
5
+ No `eslint-disable` (line, next-line, block), `eslint-enable`, `eslint-env`, inline `eslint rule: ...` config, `@ts-ignore`, `@ts-expect-error`, `@ts-nocheck`, and no `NOSONAR`, `@sonar-ignore`, `istanbul ignore`, `c8 ignore`, `v8 ignore`, `prettier-ignore` or `stylelint-disable` comment. `noInlineConfig` makes a directive ineffective, this rule makes it a finding, and `reportUnusedDisableDirectives` reports a leftover that suppresses nothing. The rule covers `src/**`, specs and the e2e tree.
6
6
 
7
7
  Every rule below is an error in `starciFeConfig`; none can be switched off or suppressed inline.
8
8
 
9
9
  ## `starci-fe/no-inline-lint-config`
10
10
 
11
- Source cannot change its own lint or type-check policy, and the vn-ok pragma is retired.
11
+ Source cannot change its own lint or type-check policy.
12
12
 
13
13
  **Invalid** (`src/components/blocks/Feed/index.tsx`)
14
14
 
@@ -17,7 +17,7 @@ Source cannot change its own lint or type-check policy, and the vn-ok pragma is
17
17
  // @ts-ignore
18
18
  // NOSONAR
19
19
  /* istanbul ignore next */
20
- const s = "x" // vn-ok: reason
20
+ const s = "x" // eslint-disable-line no-console
21
21
  ```
22
22
 
23
23
  **Valid** (`src/components/blocks/Feed/index.tsx`)
@@ -31,4 +31,4 @@ const s = 1
31
31
 
32
32
  **Why:** There is a rule-disabling comment at `<file>:<line>`. HFS does not allow disabling in place - fix the code, or propose changing the rule.
33
33
 
34
- **Fix:** Remove `eslint-disable`, `@ts-ignore`, `@ts-expect-error` or `vn-ok` and fix the cause.
34
+ **Fix:** Remove `eslint-disable`, `@ts-ignore`, or `@ts-expect-error` and fix the cause.
@@ -2,7 +2,7 @@
2
2
 
3
3
  Law module: `translation.mjs`. Catalogue: R58 FE_I18N_LITERAL.
4
4
 
5
- No text a reader can see or hear is written in source, at any tier and in any language: it is read from a `next-intl` catalogue through `t()`. There is no `vn-ok` pragma, no endonym exemption and no `resources/` copy folder; the catalogues (`messages/<locale>.json`), fixtures and specs are the only content paths. The notion of a literal is the same as the repository's i18n catalog check: a word is two or more letters, and the copy attributes are `label`, `title`, `alt`, `placeholder` and the `aria-*` set.
5
+ No text a reader can see or hear is written in source, at any tier and in any language: it is read from a `next-intl` catalogue through `t()`. There is no pragma, no endonym exemption and no `resources/` copy folder; the catalogues (`messages/<locale>.json`), fixtures and specs are the only content paths. The notion of a literal is the same as the repository's i18n catalog check: a word is two or more letters, and the copy attributes are `label`, `title`, `alt`, `placeholder` and the `aria-*` set.
6
6
 
7
7
  Every rule below is an error in `starciFeConfig`; none can be switched off or suppressed inline.
8
8
 
@@ -51,4 +51,4 @@ A template literal with substitutions is copy when its static parts contain a wo
51
51
 
52
52
  **Why:** The text `"<text>"` at `<file>:<line>` does not go through `t()`.
53
53
 
54
- **Fix:** Move the sentence into `modules/i18n/messages/<locale>.json` and read it with `t("key")`; there is no exception and no `vn-ok`.
54
+ **Fix:** Move the sentence into `modules/i18n/messages/<locale>.json` and read it with `t("key")`; there is no exception.
package/file-layout.mjs CHANGED
@@ -346,7 +346,7 @@ export const sourceTierMarkerMatchesFolder = {
346
346
  },
347
347
  }
348
348
 
349
- /** `shells/` no longer exists; vendor mechanics are closed named branches. */
349
+ /** `shells/` is not a tier; vendor mechanics are closed named branches. */
350
350
  export const noShellTier = {
351
351
  meta: {
352
352
  type: "problem",
package/icon.mjs CHANGED
@@ -19,7 +19,7 @@ import { fileOf, isComponentFile, isProductSource, kindOfFile, roleOfFile } from
19
19
  /** The one module allowed to name a glyph from a library: the entry of the leaf owner named `Icon`. */
20
20
  const ICON_LEAF = { kind: "leaves", owner: "Icon" }
21
21
 
22
- /** Repeated goal / metric cells are text-led in the legacy reference: the entry of the composite owner named `LabelledProgressRow`. */
22
+ /** Repeated goal / metric cells are text-led in the reference: the entry of the composite owner named `LabelledProgressRow`. */
23
23
  const LABELLED_PROGRESS_ROW = { kind: "composites", owner: "LabelledProgressRow" }
24
24
 
25
25
  /**
@@ -169,7 +169,7 @@ export const noDecorativeIconInMetricCell = {
169
169
  schema: [],
170
170
  messages: {
171
171
  decorative:
172
- "`LabelledProgressRow` is a repeated metric cell, not a feature entry. Its legacy reference is text-led, so an `Icon` here invents emphasis and repeats glyph meanings across the grid. Keep the label and figure; reserve tiny glyphs for generic state or action semantics actually present in the reference.",
172
+ "`LabelledProgressRow` is a repeated metric cell, not a feature entry. Its reference is text-led, so an `Icon` here invents emphasis and repeats glyph meanings across the grid. Keep the label and figure; reserve tiny glyphs for generic state or action semantics actually present in the reference.",
173
173
  },
174
174
  },
175
175
  create(context) {
package/index.mjs CHANGED
@@ -47,6 +47,7 @@ import { recommended as typeSafetyRecommended, rules as typeSafetyRules } from "
47
47
  import { recommended as typographyRecommended, rules as typographyRules } from "./typography.mjs"
48
48
  import { recommended as vendorRecommended, rules as vendorRules } from "./vendor-boundary.mjs"
49
49
  import { recommended as projectGraphRecommended, rules as projectGraphRules } from "./project-graph.mjs"
50
+ import { recommended as sonarParityRecommended, rules as sonarParityRules } from "./sonar-parity.mjs"
50
51
  import { buildFeConfig } from "./lib/config.mjs"
51
52
  import { why } from "./lib/why.mjs"
52
53
 
@@ -88,6 +89,7 @@ const CONTRIBUTIONS = [
88
89
  { law: "type-safety", rules: typeSafetyRules, recommended: typeSafetyRecommended },
89
90
  { law: "typography", rules: typographyRules, recommended: typographyRecommended },
90
91
  { law: "vendor-boundary", rules: vendorRules, recommended: vendorRecommended },
92
+ { law: "sonar-parity", rules: sonarParityRules, recommended: sonarParityRecommended },
91
93
  ]
92
94
 
93
95
  /** Every gathered law. */
@@ -176,6 +178,9 @@ const loadReactHooks = () => {
176
178
  throw new Error("starciFeConfig needs eslint-plugin-react-hooks 7 or newer: npm i -D eslint-plugin-react-hooks")
177
179
  }
178
180
 
181
+ /** The typescript-eslint plugin whose type-aware rules the factory borrows (Sonar S1874, S6551 and S7503); a dependency of this package. */
182
+ const loadTsPlugin = () => createRequire(import.meta.url)("@typescript-eslint/eslint-plugin")
183
+
179
184
  /**
180
185
  * The React Hooks rules of a plugin, all at error.
181
186
  *
@@ -226,6 +231,7 @@ export const starciFeConfig = ({ hfs } = {}) => {
226
231
  hfs,
227
232
  plugin,
228
233
  reactHooks,
234
+ tsPlugin: loadTsPlugin(),
229
235
  source: { ...recommended, ...reactHooksRules(reactHooks) },
230
236
  })
231
237
  }
package/lib/config.mjs CHANGED
@@ -23,6 +23,13 @@ export const SOURCE_FILES = Object.freeze(["apps/*/src/**/*.{ts,tsx}", "packages
23
23
  /** Build output, installed packages, generated wire types and the harness's report directory are never linted. */
24
24
  export const IGNORED = Object.freeze(["**/node_modules/**", "**/.next/**", "**/dist/**", "**/coverage/**", "**/__generated__/**", ".starci/**"])
25
25
 
26
+ /** 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`. */
27
+ export const BORROWED = Object.freeze({
28
+ "@typescript-eslint/no-deprecated": "error",
29
+ "@typescript-eslint/no-base-to-string": "error",
30
+ "@typescript-eslint/require-await": "error",
31
+ })
32
+
26
33
  const levelOf = (setting) => (Array.isArray(setting) ? setting[0] : setting)
27
34
 
28
35
  /**
@@ -33,9 +40,10 @@ const levelOf = (setting) => (Array.isArray(setting) ? setting[0] : setting)
33
40
  * @param {object} input.plugin - The canon plugin.
34
41
  * @param {Record<string, unknown>} input.source - The source-tree levels (canon rules plus the React Hooks rules).
35
42
  * @param {object} input.reactHooks - The React Hooks plugin.
43
+ * @param {object} input.tsPlugin - The typescript-eslint plugin.
36
44
  * @returns {Array<object>} The flat config: one ignore block and one typed source block.
37
45
  */
38
- export const buildFeConfig = ({ hfs, plugin, source, reactHooks }) => {
46
+ export const buildFeConfig = ({ hfs, plugin, source, reactHooks, tsPlugin }) => {
39
47
  if (!hfs || typeof hfs.slotOf !== "function" || typeof hfs.repoRoot !== "string") {
40
48
  throw new Error("starciFeConfig needs { hfs: loadHfs(import.meta.url) } - the rules read the repository's slots through it")
41
49
  }
@@ -55,9 +63,9 @@ export const buildFeConfig = ({ hfs, plugin, source, reactHooks }) => {
55
63
  parserOptions: { projectService: true, tsconfigRootDir: hfs.repoRoot, ecmaFeatures: { jsx: true } },
56
64
  },
57
65
  linterOptions: { ...linterOptions },
58
- plugins: { "starci-fe": plugin, "react-hooks": reactHooks },
66
+ plugins: { "starci-fe": plugin, "react-hooks": reactHooks, "@typescript-eslint": tsPlugin },
59
67
  settings,
60
- rules: { ...source },
68
+ rules: { ...source, ...BORROWED },
61
69
  },
62
70
  ]
63
71
  }
package/lib/types.mjs CHANGED
@@ -50,7 +50,7 @@ export const declarationsOf = (context, identifier) => {
50
50
  if (!tsNode) return []
51
51
  let symbol = checker.getSymbolAtLocation(tsNode)
52
52
  if (!symbol) return []
53
- if (symbol.flags & ts.SymbolFlags.Alias) symbol = checker.getAliasedSymbol(symbol)
53
+ if ((symbol.flags & ts.SymbolFlags.Alias) !== 0) symbol = checker.getAliasedSymbol(symbol)
54
54
  return symbol.getDeclarations?.() ?? []
55
55
  }
56
56
 
package/lib/why.mjs CHANGED
@@ -92,7 +92,7 @@ export const why = {
92
92
  "no-hardcoded-copy": {
93
93
  code: "FE_I18N_LITERAL",
94
94
  en: "The text `\"<text>\"` in `<file>:<line>` does not go through `t()`.",
95
- fix: "Move the sentence into `modules/i18n/messages/<locale>.json` and read it with `t(\"key\")`; no exceptions and no `vn-ok`.",
95
+ fix: "Move the sentence into `modules/i18n/messages/<locale>.json` and read it with `t(\"key\")`; no exceptions.",
96
96
  },
97
97
  "no-raw-brand-value": {
98
98
  code: "FE_STYLE_TOKEN_ONLY",
@@ -127,7 +127,7 @@ export const why = {
127
127
  "no-inline-lint-config": {
128
128
  code: "HFS_INLINE_SUPPRESSION",
129
129
  en: "A comment disabling a rule sits at `<file>:<line>`. HFS does not allow in-place suppression - fix the code, or propose a rule change.",
130
- fix: "Remove `eslint-disable`, `@ts-ignore`, `@ts-expect-error` or `vn-ok` and fix the cause.",
130
+ fix: "Remove `eslint-disable`, `@ts-ignore` or `@ts-expect-error` and fix the cause.",
131
131
  },
132
132
  "no-double-cast": {
133
133
  code: "FE_TYPE_ESCAPE",
@@ -7,7 +7,6 @@
7
7
  *
8
8
  * eslint-disable / -next-line / -line / -enable / -env, and `/* eslint rule: "off" *\/`
9
9
  * @ts-ignore, @ts-expect-error, @ts-nocheck
10
- * vn-ok: <reason> - the retired second-language pragma; it excuses nothing any more
11
10
  * NOSONAR, @sonar-ignore - Sonar's own switches
12
11
  * istanbul ignore, c8 ignore, v8 ignore - a line taken out of the coverage the gate reads
13
12
  * prettier-ignore - a region taken out of the formatter
@@ -46,14 +45,11 @@ const TS_DIRECTIVE = /^\s*(?:\/\s*)?@ts-(?:ignore|expect-error|nocheck)\b/
46
45
  const TOOL_DIRECTIVE =
47
46
  /\bNOSONAR\b|^\s*@sonar-ignore\b|^\s*(?:istanbul|c8|v8)\s+ignore\b|^\s*prettier-ignore(?:-start|-end)?\b|^\s*stylelint-(?:disable(?:-next-line|-line)?|enable)\b/i
48
47
 
49
- /** The retired second-language pragma. Present anywhere in a comment, it is a finding. */
50
- const RETIRED_PRAGMA = /\bvn-ok:/
51
-
52
48
  /** Inline lint configuration is repository policy, never a file-local choice. */
53
49
  export const noInlineLintConfig = {
54
50
  meta: {
55
51
  type: "problem",
56
- docs: { description: "Source cannot change its own lint or type-check policy, and the vn-ok pragma is retired." },
52
+ docs: { description: "Source cannot change its own lint or type-check policy." },
57
53
  schema: [],
58
54
  messages: {
59
55
  directive:
@@ -62,8 +58,6 @@ export const noInlineLintConfig = {
62
58
  "A TypeScript suppression directive. It turns the compiler off for code that is failing the check for a reason. Fix the type, or change the shared contract that produced it; there is no local exception path.",
63
59
  tool:
64
60
  "A Sonar, coverage, formatter or stylelint suppression (`NOSONAR`, `@sonar-ignore`, `istanbul ignore`, `c8 ignore`, `prettier-ignore`, `stylelint-disable`). It removes this code from a gate that is failing it for a reason, and the next reader cannot tell a measured exception from an avoided fix. Fix the code, or change the shared rule or the coverage threshold; there is no local exception path.",
65
- pragma:
66
- "`vn-ok:` was the second-language escape hatch and it is retired: user-facing text comes from a `next-intl` catalogue through `t()`, and there is no comment that excuses a literal. Remove the pragma and move the string.",
67
61
  },
68
62
  },
69
63
  create(context) {
@@ -75,7 +69,6 @@ export const noInlineLintConfig = {
75
69
  if (ESLINT_DIRECTIVE.test(comment.value)) context.report({ node: comment, messageId: "directive" })
76
70
  else if (TS_DIRECTIVE.test(comment.value)) context.report({ node: comment, messageId: "typescript" })
77
71
  else if (TOOL_DIRECTIVE.test(comment.value)) context.report({ node: comment, messageId: "tool" })
78
- else if (RETIRED_PRAGMA.test(comment.value)) context.report({ node: comment, messageId: "pragma" })
79
72
  }
80
73
  },
81
74
  }
@@ -28,15 +28,15 @@ export const noMiddlewareFile = {
28
28
  schema: [],
29
29
  messages: {
30
30
  file:
31
- "`{{name}}` is the retired name of the request interceptor. Next 16 calls it `proxy.ts`: rename the file and export the handler as `proxy`.",
31
+ "`{{name}}` is not the request interceptor file: Next 16 reads `proxy.ts`. Rename the file and export the handler as `proxy`.",
32
32
  export:
33
- "The handler in `proxy.ts` is exported as `middleware`, which is the retired name. Export it as `proxy`.",
33
+ "The handler in `proxy.ts` is exported as `middleware`, but Next 16 reads `proxy`. Export it as `proxy`.",
34
34
  },
35
35
  },
36
36
  create(context) {
37
37
  const file = fileOf(context)
38
- // The retired name has a slot of its own (`fe.source-root-retired`, forbidden); the interceptor is the `proxy` role of `fe.source-root-pinned`.
39
- if (inSlot(context, "fe.source-root-retired")) {
38
+ // `middleware.ts` has a slot of its own (`fe.source-root-middleware`, forbidden); the interceptor is the `proxy` role of `fe.source-root-pinned`.
39
+ if (inSlot(context, "fe.source-root-middleware")) {
40
40
  return { Program: (node) => context.report({ node, messageId: "file", data: { name: baseName(file) } }) }
41
41
  }
42
42
  if (!inSlot(context, "fe.source-root-pinned") || roleOfFile(context) !== "proxy") return {}
package/package.json CHANGED
@@ -1,15 +1,16 @@
1
1
  {
2
2
  "name": "@starci/eslint-canon-fe",
3
- "version": "8.0.9",
3
+ "version": "8.1.0",
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
- "license": "UNLICENSED",
6
+ "license": "MIT",
7
7
  "private": false,
8
8
  "exports": {
9
9
  ".": "./index.mjs"
10
10
  },
11
11
  "main": "./index.mjs",
12
12
  "files": [
13
+ "LICENSE",
13
14
  "*.mjs",
14
15
  "lib/*.mjs",
15
16
  "runtime/**",
@@ -42,6 +43,7 @@
42
43
  "canon"
43
44
  ],
44
45
  "dependencies": {
46
+ "@typescript-eslint/eslint-plugin": "^8.70.0",
45
47
  "@typescript-eslint/parser": "^8.70.0"
46
48
  }
47
49
  }
@@ -0,0 +1,187 @@
1
+ # config.example.yaml — seed for the per-project owner config.
2
+ # The installer copies this file to the gitignored `config.yaml` next to it;
3
+ # owners edit that copy. Everything below is read at runtime — no key here is
4
+ # decorative. Kernel route precedence: explicit `--agent` flag >
5
+ # `kernel` below > the tier chain (modules/models/tiers.yaml).
6
+ language: vi
7
+ model: null
8
+ effort: medium
9
+ # Explicit current owner adoption is required; shipped defaults grant no trust or purge authority.
10
+ # launchTrust: {profile: automatic, approvedBy: owner, approvalRef: '<current approval>', roots: ['<exact absolute repository root>']}
11
+ # profile: declined preserves an explicit decision; provider-side declines also remain authoritative.
12
+ launchTrust: null
13
+ # retention: {workflowPurge: {approvedBy: owner, approvalRef: '<current approval>', repos: ['<exact absolute ledger-owner repo>']}}
14
+ retention: null
15
+ # kernel — the long-lived [Kernel] seat. It takes the `high` tier of modules/models/tiers.yaml (an ordered chain of
16
+ # members; the picker takes the first member with tokens after your bias and the balance step; docs/config-format.md).
17
+ # {agent?, model?, effort?}: agent and/or model PIN the seat — the bias `only`, keeping the tier's model of that agent
18
+ # (an explicit `--agent` flag is the same bias and wins). effort: the seat's effort; null takes the tier member's effort.
19
+ kernel: {effort: high}
20
+ # parallel — the one knob for how wide the workers run. `gear` indexes the agent
21
+ # table in modules/models/runtimes.yaml allocation.slicing.size: a long (`l`)
22
+ # operation asks for the `l.agents[gear]` agents, an extra-long (`xl`) one for
23
+ # `xl.agents[gear]`; `s` and `m` operations are always one agent. Declared gears
24
+ # are allocation.slicing.gears — an undeclared value fails closed. Absent means
25
+ # the first declared gear. A gear raises only what `starci kernel estimate` requests; it
26
+ # never raises a pool's maxParallel, maxParallelOps or budgets.maxOps below.
27
+ parallel: {gear: 1}
28
+ # supervisor — the optional chat digest cadence. pollIntervalMs controls
29
+ # scripts/supervisor/poll.mjs when run continuously (its floor is the config
30
+ # validation of engine/config.mjs); null uses scripts/machine/home.mjs
31
+ # DEFAULTS, and --interval-ms overrides this key.
32
+ # The reconciler engine owns host runtime cadence and restores seats after reboot.
33
+ # repos: managed product ledger-owner repositories for the reconciler and Supervisor tools,
34
+ # relative to the source root (the directory holding this skill) or absolute.
35
+ # --repo on the poll command line can add another. stallMinutes (optional,
36
+ # integer >= 5) marks a running workflow STALLED in scripts/supervisor/stall.mjs;
37
+ # the Workflow controller turns actionable findings into Decision Items. The [Supervisor]
38
+ # kernel (docs/supervisor.md) also takes, optional, defaults in home.mjs DEFAULTS:
39
+ # kernel: {agent, model, effort} its agent pin (default: the kernel pin below)
40
+ # workers: {base, max} the adaptive [Worker] cap, 0 to 10 (0 = the Supervisor runs no fix worker)
41
+ # landGate: {mode: shared} shared lets running lanes commit directly; exclusive = gate only. A land
42
+ # fast-forwards LOCAL main only: the remote main moves with a release (starci release cut)
43
+ supervisor:
44
+ # mode: chat (default) - the owner's desktop chat session is the Supervisor: it owns channel 'main'
45
+ # and reads Decision Items; nothing starts a [Supervisor] kernel. kernel - the optional [Supervisor]
46
+ # Orca kernel (scripts/supervisor/start-supervisor.mjs), kept alive by the reconciler Host controller.
47
+ # See docs/supervisor.md.
48
+ # mode: chat
49
+ pollIntervalMs: null
50
+ repos: []
51
+ # frozenMinutes: <n> # Host seat check: a busy frame unchanged this long is frozen, not busy
52
+ # landGate: {mode: shared|exclusive}
53
+ # delegation — while the owner is away, a named delegate (e.g. the supervisor chat) may
54
+ # answer owner asks until the given time; excluded classes stay owner-only. null = none.
55
+ # delegation: {asks: supervisor, until: 2026-09-23T09:00:00+07:00,
56
+ # excludes: [credentials, oauth-consent, payments, legal, payment-provider]}
57
+ delegation: null
58
+ # budgets — the owner's ceiling, surfaced by every plan before `ok`. Null = unbounded.
59
+ budgets:
60
+ maxOps: null # max concurrent dispatched ops one workflow may hold open;
61
+ # route/dispatch refuse `max-ops` at min(this, maxParallelOps)
62
+ # allocation — the owner's default grant for a pool whose capacity is an explicit grant (Devin).
63
+ # grants: '<pool>=<slots>@<role>+<role>' — the default grant every workflow gets. Once a grants list is
64
+ # declared it is the whole set: a pool whose registry.yaml capacityAuthority is explicit-workflow-quota
65
+ # (Devin) routes only for the roles and up to the running slots a grant names ([] closes it).
66
+ allocation:
67
+ grants: [devin-agent=10@implement+verify+write]
68
+ # models — optional overrides of the shipped model tiers (modules/models/tiers.yaml owns the defaults). Every key is optional:
69
+ # tiers: {<tier>: [{agent, model, effort?}, ...]} replaces or adds one tier's ordered member chain
70
+ # seats: {<seat>: <tier>} remaps a seat (supervisor, kernel, worker, planner, validator, kernelManager)
71
+ # balance: {maxStreak, maxSharePercent} a head member picked maxStreak times in a row, or holding more than
72
+ # maxSharePercent of the tier's running seats, yields to the next member
73
+ # usage: {reservePercent, biasPercent} a member at reservePercent of its tokens is skipped; an owner bias
74
+ # naming it keeps it usable up to biasPercent; never above it
75
+ models: {}
76
+ # connectors — the owner's public ask channel (docs/connectors.md). Everything is off by default.
77
+ # One local gateway (scripts/connectors/ask-gateway.mjs) fronts every serve-ask form; a Cloudflare
78
+ # tunnel (scripts/connectors/tunnel.mjs) publishes the gateway; when serve-ask binds a form, the
79
+ # kernel's ask path sends the owner one Telegram message (scripts/connectors/telegram.mjs) with the
80
+ # question, its options and the public link. Telegram carries owner asks only.
81
+ # SECRETS NEVER GO IN THIS FILE: tokenEnv/botTokenEnv NAME an environment variable holding the token
82
+ # (or <NAME>_FILE pointing at a custody file); a value that is not an UPPER_SNAKE env var name is refused.
83
+ # Host credentials use the canonical runtime root secret.env; docs/host-secrets.md owns setup.
84
+ # Supplied environment values win, including blank values. Missing local file adds no values.
85
+ # repos: ledger-owner repository roots whose asks go public, relative to the source root (the
86
+ # directory holding this skill) or absolute. [] = the source root plus every .workspaces project owner.
87
+ # gateway.port: the one fixed local port the tunnel points at (outside the 6969..7069 form band).
88
+ # cloudflare.mode: off | quick | named.
89
+ # quick — `cloudflared tunnel --url`, a random https://*.trycloudflare.com host, no account, no auth,
90
+ # a new host on every restart. Good for decisions only.
91
+ # named — a fixed `hostname`, authenticated one of two ways: `tunnel` (UUID) + `credentialsFile`
92
+ # (the JSON `cloudflared tunnel create` wrote; the manager writes its own ingress config mapping
93
+ # hostname -> the gateway and never reads ~/.cloudflared/config.yml), or a remotely managed tunnel
94
+ # token in `tokenEnv` (its public hostname is routed to http://127.0.0.1:<gateway.port> in the
95
+ # Cloudflare dashboard).
96
+ # cloudflare.access: whether a Cloudflare Access application guards `hostname` (informational; false
97
+ # prints a warning). Recommended for anything beyond yes/no decisions.
98
+ # telegram.enabled / chatId: the owner's chat with the bot (BotFather token in botTokenEnv). Send the
99
+ # bot /start, then `starci connect telegram discover-chat` prints the chat id.
100
+ # telegram.exposeCredentialAsks: false keeps a credential/secret ask off the public link — the message
101
+ # says to answer it on this machine and carries only the localhost link, and the gateway refuses it.
102
+ connectors:
103
+ repos: []
104
+ gateway: {port: 7070}
105
+ cloudflare: {mode: off, tunnel: null, credentialsFile: null, tokenEnv: CLOUDFLARE_TUNNEL_TOKEN, hostname: null, access: false}
106
+ telegram:
107
+ enabled: false
108
+ botTokenEnv: TELEGRAM_BOT_TOKEN
109
+ chatId: null
110
+ exposeCredentialAsks: false
111
+ # asks — owner asks that carry a recommended option (question.recommended, or exactly one option
112
+ # marked "(recommended)" or its Vietnamese equivalents). autoAcceptRecommended: true answers such an
113
+ # ask with its recommendation instead of serving the form: the ledger records ask-answered with
114
+ # answeredBy auto-recommended, the kernel is woken, and Telegram gets one plain message naming the
115
+ # pick. A later owner answer supersedes it. excludes lists the ask classes that always reach the owner:
116
+ # credential — any ask with secret fields, or of kind credential, account, access or consent;
117
+ # handover — every handover.review ask (always excluded, even when removed from this list);
118
+ # draw-review — the opt-out for drawings (interface.draw, scripts/work/draw-review.mjs): unlisted, a
119
+ # drawing the owner did not ask to review is accepted without the owner (its accept option is the
120
+ # recommendation); a drawing the owner asked for (a prior owner redraw of the record, the owner
121
+ # opened its form, or question.ownerRequested) always reaches the owner;
122
+ # or any ask kind: information, authority, business-decision, irreversible-confirmation, ...
123
+ asks:
124
+ autoAcceptRecommended: false
125
+ excludes: [credential, irreversible-confirmation, handover]
126
+ # uat — the machine-wide UAT ceiling. At most maxConcurrent UAT runs (a browser session, a player, the
127
+ # app it drives) execute at once on this host, across every workflow; the rest wait queued for a slot
128
+ # (scripts/uat/uat-slots.mjs, lock files under <runtime>/uat-slots). A slot whose holder died is
129
+ # reclaimed. STARCI_UAT_MAX_CONCURRENT overrides it for one process tree.
130
+ uat:
131
+ maxConcurrent: 10
132
+ # orca — the owner's Orca app settings the runtime must know (engine/orca-config.mjs orcaSettings). maxWorkerDepth MUST equal
133
+ # the worker depth set in the Orca app: the deepest worker Orca starts under the owner's chat (chat 0, Kernel or
134
+ # Supervisor 1, op or [Worker] 2, draw critic 3). Orca exposes no read of it, so the runtime trusts this value and refuses
135
+ # a deeper launch before worker-start (worker-depth-exceeded). `start --check` with STARCI_ORCA_LIVE=1 measures Orca's
136
+ # real limit and turns red when the two differ.
137
+ orca:
138
+ maxWorkerDepth: 4
139
+ # debugLoop — the debug watcher. Workflow DEBUG is a /loop of the chat that started the workflow (the /starci skill sets it up in
140
+ # Claude Code and Codex; no seat, no background agent, nothing in the runtime schedules it). Each tick runs the read-only
141
+ # `starci debug digest`. interval is the loop cadence (<n>s | <n>m | <n>h, default `10m` = `debugLoop.interval`); worktreeLimit is the
142
+ # per-repository core-watch alert threshold (default `40` = `debugLoop.worktreeLimit`). Both keys are optional (engine/config.mjs DEBUG_LOOP_DEFAULTS).
143
+ debugLoop:
144
+ interval: 10m
145
+ worktreeLimit: 40
146
+ # reconciler — the host's one reconciler engine (scripts/reconciler/engine.mjs; contract
147
+ # modules/reconciler/reconciler.yaml). Each controller runs off | shadow (reads, records what it would do as
148
+ # reconciler.would rows, runs nothing) | active (acts; the old loop yields that duty via scripts/reconciler/owns.mjs).
149
+ # profile — the operational default of every controller, so a host is never left all-shadow by accident:
150
+ # operational job, host, workflow, resource active (they settle jobs, start services and seats, wake stalled
151
+ # workflows, throttle); gc, workers, learning shadow. The `start` skill applies and checks it.
152
+ # observe every controller shadow (read-only; a debugging or audit host).
153
+ # (absent) only the explicit controllers.<name>.mode entries count; an unnamed controller is off.
154
+ # controllers.<name>.mode overrides the profile for that one controller (e.g. gc: {mode: active}).
155
+ # Rollback of a controller: set its mode to off (the old loops take the duty back within
156
+ # allocation.reconciler.heartbeatStaleMs). Numbers: modules/models/runtimes.yaml allocation.reconciler.
157
+ reconciler:
158
+ enabled: true
159
+ profile: operational
160
+ controllers: {}
161
+ # specs.unit / specs.e2e in workflows (the `specs` map near the top; owner 2026-09-28 "speed up development;
162
+ # test later when asked"; scripts/route/spec-deferral.mjs). Each op brief's policy.specsToggle says what it does:
163
+ # unit: false - backend.implement, interface.implement and code.refactor run no jest/vitest/test:ci/coverage
164
+ # and demand no changed-line coverage (Sonar still runs: bugs, smells, security gate);
165
+ # test.author legs are never dispatched; review/handover gates do not count unit tests or coverage.
166
+ # e2e: false - e2e.verify legs and e2e test authoring are never dispatched; uat.verify is not e2e.
167
+ # A leg that is not dispatched settles `deferred: specs.<class>=false` with no attempt spent and the workflow
168
+ # proceeds past it; starci kernel status lists testsDeferred and `starci kernel run-deferred-tests --workflow <id> [--kind unit|e2e]`
169
+ # runs them later. Read on every wake: flipping a switch needs no restart.
170
+ # roots — where this host keeps the archive, the lane worktrees and its temporary files, when the owner moves them off the defaults.
171
+ # All keys are optional absolute directories; env STARCI_ARCHIVE_ROOT / STARCI_LANES_ROOT / STARCI_TEMP_ROOT win over them, and an
172
+ # absent key means <starciLocalRoot>/archive (<runtime root>/.runtime/archive), the per-user profile lanes directory, kept out of the checkout (scripts/machine/home.mjs lanesRoot),
173
+ # and the OS temp directory (engine/temp-root.mjs tempRoot; the processes the runtime starts get it as TEMP/TMP/TMPDIR). No host location lives in
174
+ # a tracked file: set the real directories only in the gitignored config.yaml copy, e.g.
175
+ # roots: {archive: <absolute directory>, lanes: <absolute directory>, temp: <absolute directory>}
176
+ # sonar — the SonarCloud organization the release's Sonar proof of the example apps analyses in (scripts/supervisor/release-sonarcloud.mjs). It is configuration, not a secret,
177
+ # so it lives here and not in secret.env; the project keys are <organization>_starci-example-<app>. The environment variable SONAR_ORGANIZATION wins over it (CI reads the
178
+ # repository variable of the same name). Empty until the owner sets it in the gitignored config.yaml copy, e.g.
179
+ # sonar: {organization: <the organization key shown on sonarcloud.io>}
180
+ sonar: {organization: null}
181
+ # resources — the host capacity floors the dispatch gate enforces, over the shipped numbers of modules/models/runtimes.yaml allocation.resources
182
+ # (scripts/machine/host-resources.mjs; docs/config-format.md "Host resource floors"). Every key is optional (a number above 0, or null):
183
+ # minFreeDiskGb free GB a drive holding the temp root or the repository must keep
184
+ # minFreeDiskPct the same floor as a percentage of the drive's size; with minFreeDiskGb the larger requirement applies
185
+ # minFreeRamPct free-RAM percentage below which no new heavy op starts
186
+ # set them only in the gitignored config.yaml copy, e.g.
187
+ # resources: {minFreeDiskGb: 5, minFreeDiskPct: 1, minFreeRamPct: 10}
@@ -0,0 +1,12 @@
1
+ // by-code-unit.mjs - the comparator of a bare `Array.prototype.sort()`, owned beside the engine files that digest and canonicalize (scripts/lib/list.mjs re-exports it).
2
+
3
+ /**
4
+ * The comparator of a bare `Array.prototype.sort()`: values compared as strings by UTF-16 code unit. Pass it to `sort` and
5
+ * `toSorted` so the order is stated, and stays byte-identical to the default order that digests and canonical output depend on.
6
+ */
7
+ export const byCodeUnit = (a, b) => {
8
+ const left = String(a);
9
+ const right = String(b);
10
+ if (left < right) return -1;
11
+ return left > right ? 1 : 0;
12
+ };