@starci/eslint-canon-be 3.0.9 → 3.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 (151) hide show
  1. package/CHANGELOG.md +3 -1
  2. package/LICENSE +21 -0
  3. package/README.md +2 -1
  4. package/builders.mjs +1 -1
  5. package/comments.mjs +12 -21
  6. package/config-owner.mjs +1 -1
  7. package/idempotency.mjs +54 -5
  8. package/index.mjs +2 -0
  9. package/lib/config.mjs +4 -1
  10. package/lib/doors.mjs +3 -1
  11. package/module-shape.mjs +1 -2
  12. package/package.json +3 -2
  13. package/resilience.mjs +1 -1
  14. package/runtime/config.example.yaml +187 -0
  15. package/runtime/engine/by-code-unit.mjs +12 -0
  16. package/runtime/engine/config.mjs +247 -273
  17. package/runtime/engine/invalid-config.mjs +5 -5
  18. package/runtime/engine/model-config.mjs +90 -0
  19. package/runtime/engine/orca-config.mjs +3 -1
  20. package/runtime/engine/removed-vocabulary.mjs +28 -0
  21. package/runtime/engine/resources-config.mjs +52 -0
  22. package/runtime/engine/runtime-root.mjs +17 -0
  23. package/runtime/engine/secrets.mjs +134 -0
  24. package/runtime/engine/sonar-config.mjs +21 -0
  25. package/runtime/engine/temp-root.mjs +32 -0
  26. package/runtime/knowledge/hfs/canon-pins.yaml +18 -18
  27. package/runtime/knowledge/hfs/rules.yaml +102 -69
  28. package/runtime/knowledge/hfs/slots.yaml +11 -14
  29. package/runtime/modules/kernel/removed-vocabulary.yaml +168 -0
  30. package/runtime/modules/models/registry.yaml +25 -397
  31. package/runtime/modules/models/runtimes.yaml +92 -262
  32. package/runtime/modules/models/tiers.yaml +65 -0
  33. package/runtime/scripts/api/git/lib.mjs +36 -1
  34. package/runtime/scripts/hfs/allows.mjs +3 -3
  35. package/runtime/scripts/hfs/architecture/ast-walks.mjs +63 -50
  36. package/runtime/scripts/hfs/architecture/automatic-gates.mjs +96 -0
  37. package/runtime/scripts/hfs/architecture/backend.mjs +261 -175
  38. package/runtime/scripts/hfs/architecture/background-unowned.mjs +69 -34
  39. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +55 -54
  40. package/runtime/scripts/hfs/architecture/clones.mjs +179 -106
  41. package/runtime/scripts/hfs/architecture/config-unread.mjs +5 -22
  42. package/runtime/scripts/hfs/architecture/config.mjs +95 -80
  43. package/runtime/scripts/hfs/architecture/connection-map.mjs +233 -141
  44. package/runtime/scripts/hfs/architecture/constructor-deps.mjs +15 -9
  45. package/runtime/scripts/hfs/architecture/context-coupling.mjs +76 -52
  46. package/runtime/scripts/hfs/architecture/context-map.mjs +215 -141
  47. package/runtime/scripts/hfs/architecture/context-owner.mjs +39 -30
  48. package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +31 -17
  49. package/runtime/scripts/hfs/architecture/context-transaction.mjs +38 -25
  50. package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +86 -40
  51. package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +342 -0
  52. package/runtime/scripts/hfs/architecture/contracts.mjs +263 -455
  53. package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +43 -34
  54. package/runtime/scripts/hfs/architecture/dead-exports.mjs +201 -154
  55. package/runtime/scripts/hfs/architecture/default-deny.mjs +118 -93
  56. package/runtime/scripts/hfs/architecture/doc-language.mjs +2 -1
  57. package/runtime/scripts/hfs/architecture/entrypoint.mjs +45 -30
  58. package/runtime/scripts/hfs/architecture/error-codes.mjs +19 -12
  59. package/runtime/scripts/hfs/architecture/error-masked.mjs +39 -26
  60. package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +3 -2
  61. package/runtime/scripts/hfs/architecture/feature-shape.mjs +26 -20
  62. package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
  63. package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +190 -0
  64. package/runtime/scripts/hfs/architecture/frontend-routing.mjs +186 -0
  65. package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +311 -0
  66. package/runtime/scripts/hfs/architecture/frontend-world.mjs +328 -0
  67. package/runtime/scripts/hfs/architecture/frontend.mjs +109 -829
  68. package/runtime/scripts/hfs/architecture/hfs-graph.mjs +14 -2
  69. package/runtime/scripts/hfs/architecture/hfs.mjs +120 -288
  70. package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +46 -26
  71. package/runtime/scripts/hfs/architecture/i18n-keys.mjs +114 -76
  72. package/runtime/scripts/hfs/architecture/index.mjs +158 -102
  73. package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +33 -28
  74. package/runtime/scripts/hfs/architecture/machine-ast.mjs +180 -144
  75. package/runtime/scripts/hfs/architecture/managed-scripts.mjs +8 -2
  76. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +150 -101
  77. package/runtime/scripts/hfs/architecture/next-data-contract.mjs +51 -43
  78. package/runtime/scripts/hfs/architecture/next-data-key.mjs +338 -0
  79. package/runtime/scripts/hfs/architecture/next-data.mjs +179 -427
  80. package/runtime/scripts/hfs/architecture/owners.mjs +27 -13
  81. package/runtime/scripts/hfs/architecture/package-shape.mjs +51 -23
  82. package/runtime/scripts/hfs/architecture/presentation.mjs +165 -0
  83. package/runtime/scripts/hfs/architecture/reachability.mjs +127 -61
  84. package/runtime/scripts/hfs/architecture/register-once.mjs +112 -63
  85. package/runtime/scripts/hfs/architecture/registration.mjs +187 -110
  86. package/runtime/scripts/hfs/architecture/required-files.mjs +100 -68
  87. package/runtime/scripts/hfs/architecture/route-files-thin.mjs +83 -58
  88. package/runtime/scripts/hfs/architecture/schema-owner.mjs +182 -108
  89. package/runtime/scripts/hfs/architecture/source-names-shape.mjs +318 -0
  90. package/runtime/scripts/hfs/architecture/source-names.mjs +83 -248
  91. package/runtime/scripts/hfs/architecture/sql-owner.mjs +175 -118
  92. package/runtime/scripts/hfs/architecture/sql-returning.mjs +39 -28
  93. package/runtime/scripts/hfs/architecture/sql-tokens.mjs +302 -172
  94. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +1 -1
  95. package/runtime/scripts/hfs/architecture/supabase-be.mjs +55 -45
  96. package/runtime/scripts/hfs/architecture/supabase-results.mjs +182 -0
  97. package/runtime/scripts/hfs/architecture/supabase-tables.mjs +20 -12
  98. package/runtime/scripts/hfs/architecture/supabase.mjs +191 -263
  99. package/runtime/scripts/hfs/architecture/symbols.mjs +175 -99
  100. package/runtime/scripts/hfs/architecture/test-world-files.mjs +82 -41
  101. package/runtime/scripts/hfs/architecture/tiers.mjs +120 -69
  102. package/runtime/scripts/hfs/architecture/transport-owner.mjs +84 -57
  103. package/runtime/scripts/hfs/architecture/type-context.mjs +282 -0
  104. package/runtime/scripts/hfs/architecture/typescript.mjs +116 -276
  105. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +84 -62
  106. package/runtime/scripts/hfs/declaration-shape.mjs +77 -36
  107. package/runtime/scripts/hfs/declaration-slots.mjs +1 -1
  108. package/runtime/scripts/hfs/edition-slots.mjs +5 -3
  109. package/runtime/scripts/hfs/linear-text.mjs +30 -0
  110. package/runtime/scripts/hfs/manifest-shape.mjs +164 -63
  111. package/runtime/scripts/hfs/path-findings.mjs +58 -38
  112. package/runtime/scripts/hfs/project-graph.mjs +4 -1
  113. package/runtime/scripts/hfs/repo-identity.mjs +5 -2
  114. package/runtime/scripts/hfs/rule-catalog.mjs +194 -0
  115. package/runtime/scripts/hfs/rule-params-shape.mjs +33 -27
  116. package/runtime/scripts/hfs/rules/fe-no-tests.mjs +36 -16
  117. package/runtime/scripts/hfs/rules/secrets.mjs +4 -3
  118. package/runtime/scripts/hfs/slot-app-view.mjs +102 -0
  119. package/runtime/scripts/hfs/slot-classify.mjs +65 -0
  120. package/runtime/scripts/hfs/slot-errors.mjs +11 -0
  121. package/runtime/scripts/hfs/slot-imports.mjs +76 -0
  122. package/runtime/scripts/hfs/slot-manifest-shape.mjs +73 -0
  123. package/runtime/scripts/hfs/slot-match.mjs +153 -0
  124. package/runtime/scripts/hfs/slot-path.mjs +8 -0
  125. package/runtime/scripts/hfs/slot-required.mjs +76 -0
  126. package/runtime/scripts/hfs/slot-semantic-problems.mjs +148 -0
  127. package/runtime/scripts/hfs/slot-side-problems.mjs +28 -0
  128. package/runtime/scripts/hfs/slots.mjs +72 -693
  129. package/runtime/scripts/hfs/trailing-slashes.mjs +8 -0
  130. package/runtime/scripts/hfs/typescript-programs.mjs +6 -4
  131. package/runtime/scripts/hfs/view.mjs +1 -1
  132. package/runtime/scripts/lib/dockerfile.mjs +56 -33
  133. package/runtime/scripts/lib/git.mjs +2 -2
  134. package/runtime/scripts/lib/i18n.mjs +3 -2
  135. package/runtime/scripts/lib/jsdoc.mjs +35 -0
  136. package/runtime/scripts/lib/language.mjs +3 -3
  137. package/runtime/scripts/lib/list.mjs +8 -1
  138. package/runtime/scripts/lib/mutation-fence.mjs +15 -0
  139. package/runtime/scripts/lib/path-key.mjs +42 -9
  140. package/runtime/scripts/lib/regex.mjs +1 -1
  141. package/runtime/scripts/lib/secret-patterns.mjs +6 -2
  142. package/runtime/scripts/lib/sonar-syntax-rules.mjs +225 -0
  143. package/runtime/scripts/lib/sops-envelope.mjs +103 -2
  144. package/runtime/scripts/lib/stack-services.mjs +1 -2
  145. package/runtime/scripts/lib/ts-ast.mjs +9 -0
  146. package/runtime/scripts/lib/walk.mjs +6 -1
  147. package/schema-authority.mjs +1 -1
  148. package/service-deps.mjs +1 -1
  149. package/sonar-parity.mjs +19 -0
  150. package/suppression.mjs +2 -6
  151. package/test-world.mjs +2 -10
package/CHANGELOG.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 3.1.0 - 2026-10-08
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) and `prefer-export-from` (S7763), each the syntax-tree form of the Sonar rule SonarCloud flagged in the example apps, shared with the front-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).
4
6
 
5
7
  - New: `feature-thin` (R203 `BE_FEATURE_THIN`, law `feature-thin`): a feature-tier file of a role of `ruleParams.be.thinRoles` (handler, resolver, controller, consumer, processor, step, saga, saga-step, compensation, webhook, gateway, subscription, cli, mapper, the saga orchestrator service) maps its parameters and makes ONE delegating call into modules/, with no branch, loop, ternary, try, throw or computation. The slot manifest copy gains `coverage` on every tracked be slot and `ruleParams.be.logicRoles` and `thinRoles`.
6
8
  - Changed: `unit-test-colocated` also allows `<name>.<role>.spec.ts` beside a logic role (policy, client, guard, mapper, ...) inside a slot whose coverage is required; the spec stays required only for the roles of `ruleParams.be.unitRoles`, and the texts say so.
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
@@ -41,10 +41,11 @@ A sample, not the list:
41
41
  | **Schema authority** | Migrations only: every DataSource or TypeORM options object states the literal `synchronize: false` (found by the contextual type), no `migrationsRun`, no DDL string outside a migration, no entity glob; no `@Entity` type reachable from a transport signature, a handler `execute`, a contract or a message, and no GraphQL or validator import in an entity file (`no-runtime-schema`, `no-entity-in-contract`) |
42
42
  | **Config and secrets** | `process.env` (any form), `@nestjs/config`, `dotenv` only in the file of `platform/config` that declares `EnvSource` (and the test world, slot `be.tests.world`, for `process.env`); a function exported by a `<capability>.config.ts` is called only from `apps/<app>/src/main.ts` and `<app>.options.ts`, its own config file or spec, the test world and a provider contract spec (`config-parsed-in-main`); no default of any kind for a value typed `Secret` or `Url`; secrets compare with `timingSafeEqual` (`no-direct-env-read`, `config-parsed-in-main`, `no-secret-default`, `secret-compare-timing-safe`) |
43
43
  | **Ambient ids** (R142) | `randomUUID` of Node's crypto (named, namespace or global), the `v1`, `v4`, `v6` and `v7` generators of `uuid`, `nanoid` and `ulid` are used only inside `platform/ids`, specs included: code asks the injected `Ids` port and a spec provides `fakeIds()`; the generators are found by the module they are imported from, never by a local name (`no-ambient-id`) |
44
+ | **Sonar parity** (R235) | The smells SonarCloud measures that a lint rule can decide: no `await` inside a loop (`no-await-in-loop`, S9382; `for (;;)` and `while (true)` are spared), no `charCodeAt`/`fromCharCode` (`prefer-code-point`), no string that only escapes backslashes (`prefer-string-raw`), no nested conditional (`no-nested-conditional`), `void` only on a call (`no-void-operator`), no unused import (`no-unused-import`), `export ... from` for an import that is only handed on (`prefer-export-from`); the factory also turns on typescript-eslint `no-deprecated`, `no-base-to-string` and `require-await`. The files `*.spec.ts` and `*.e2e-spec.ts` are outside the scan, so outside these rules |
44
45
  | **Cookies** (R141) | A cookie a door writes through the response of Express or Fastify (`cookie`, `setCookie`) states `httpOnly: true`, `secure: true` and `sameSite` `lax` or `strict` in options typed with those literals, and `Set-Cookie` is never written as a header; the call is recognised by where its signature is declared (`response-cookie-attributes`). A secret-derived value is compared with `timingSafeEqual`: a `Secret` of `platform/config` and the `.digest()` of a `Hash` or `Hmac` of Node's crypto are recognised by type (`secret-compare-timing-safe`) |
45
46
  | **Default deny** | Typed bodies and arguments, no `GraphQLJSON` tunnel, no `switch (operation)`, `@Public` carries a `PublicReason` member of `domain/identity`, and no `@UseGuards` (the global `APP_GUARD` chain decides) (`no-untyped-body`, `public-needs-reason`, `no-auth-use-guards`) |
46
47
  | **Module shape** | No `@Global()`; `isGlobal: true` only in an app's `app.module.ts`; no import of another owner's module; typed `ConfigurableModuleBuilder`; `static register` is the only factory; a capability's representative module extends the `ConfigurableModuleClass` of its own module definition and every sub-module or feature module is plain (`capability-module-shape`); one module per file; no `new` of a provider; no module-level `let` (`no-global-decorator`, `is-global-only-in-app`, `no-cross-owner-module-import`, `static-module-register`) |
47
- | **Suppression** | No `eslint-disable`, `eslint-env`, `/* global */`, `@ts-ignore`, `@ts-expect-error`, `@ts-nocheck`, `istanbul`, `c8` or `v8 ignore`, `NOSONAR`, `sonar-disable`, `prettier-ignore` or `vn-ok` comment, anywhere (`no-inline-suppression`) |
48
+ | **Suppression** | No `eslint-disable`, `eslint-env`, `/* global */`, `@ts-ignore`, `@ts-expect-error`, `@ts-nocheck`, `istanbul`, `c8` or `v8 ignore`, `NOSONAR`, `sonar-disable`, or `prettier-ignore` comment, anywhere (`no-inline-suppression`) |
48
49
  | **Size** | A source file, a spec included, over the manifest's line budget is not new and does not grow past its size at the parent commit; the rule takes no option (`file-size-growth`) |
49
50
  | **Query safety** | `.query(...)` takes only a `SqlText` built with the `sql` tag in a persistence `<name>.sql.ts`, and no query builder exists (`sql-text-only`, `no-query-builder`); the tag's only substitution is a `SqlIdent` and a `.query` string is never built by template or `+` (`no-interpolated-sql`); `find`, `findBy` and `findAndCount` on an `EntityManager` state `take` and no `EntityManager` read runs per loop element (`query-needs-limit`, `no-query-in-loop`) |
50
51
  | **Resilience** | An outbound HTTP call states a timeout or signal; `JSON.parse` of outside text sits in a `try`; a loop that catches and waits goes through `platform/retry` - the wait is `setTimeout`, `node:timers/promises` or a function outside `platform/retry` whose body waits, whatever it is called (`http-needs-timeout`, `json-parse-needs-guard`, `no-hand-rolled-retry`) |
package/builders.mjs CHANGED
@@ -72,7 +72,7 @@ const isEntityClass = (checker, declaration) => {
72
72
  const callee = ts.isCallExpression(decorator.expression) ? decorator.expression.expression : decorator.expression
73
73
  const symbol = checker.getSymbolAtLocation(callee)
74
74
  if (!symbol) return false
75
- const target = symbol.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : symbol
75
+ const target = (symbol.flags & ts.SymbolFlags.Alias) !== 0 ? checker.getAliasedSymbol(symbol) : symbol
76
76
  return (target.getDeclarations?.() ?? []).some((found) => moduleOf(found, String(found.getSourceFile().fileName).replace(/\\/g, "/")) === "typeorm")
77
77
  })
78
78
  }
package/comments.mjs CHANGED
@@ -10,9 +10,9 @@
10
10
  * constructor and an index signature are not surface, overload signatures of one name share one doc, and a property
11
11
  * set to a literal or a named constant is a data constant (COMMENT-1's reason). Product source only: the test tiers
12
12
  * (`e2e`, `fixtures` of the slot manifest) document their spec-read shapes at the type.
13
- * - `require-enum-member-jsdoc` can check that a doc EXISTS and never that it states a
13
+ * - `require-enum-member-jsdoc` checks an adjacent non-empty description, never whether it states a
14
14
  * consequence. That half is read by a person, and the rule says so rather than pretending.
15
- * - `no-non-ascii-source` takes no exemption marker: HFS removed `vn-ok`, so text a program depends on
15
+ * - `no-non-ascii-source` takes no exemption marker, so text a program depends on
16
16
  * lives in a message catalog (slot be.domain.messages, of any module tier, or be.feature.messages), the only place Vietnamese may appear. Specs
17
17
  * and fixtures get no exemption.
18
18
  * - `no-restated-name-jsdoc` (law 7) holds the decidable slice of law 3 - a doc block whose only
@@ -26,6 +26,7 @@
26
26
  import { hfsOf } from "./lib/hfs.mjs"
27
27
  import { normalizePath } from "./lib/path.mjs"
28
28
  import { hasSecondLanguage } from "./runtime/scripts/lib/language.mjs"
29
+ import { jsdocBefore, jsdocDescription } from "./runtime/scripts/lib/jsdoc.mjs"
29
30
 
30
31
  /** The slots of the per-owner message catalogs: the only source files that may hold Vietnamese. */
31
32
  const CATALOG_SLOTS = new Set(["be.domain.messages", "be.feature.messages"])
@@ -91,20 +92,15 @@ const offenceIn = (line) => {
91
92
  */
92
93
  const hasDocumentedSurface = (declaration) => {
93
94
  if (declaration.type === "VariableDeclaration") {
94
- // only a const bound to a function has a surface; a data constant is already described
95
- const first = declaration.declarations[0]
96
- const init = first && first.init
97
- return Boolean(init)
98
- && (init.type === "ArrowFunctionExpression" || init.type === "FunctionExpression")
95
+ // a later declarator can expose a function even when the first binding is plain data
96
+ return declaration.declarations.some(({ init }) => Boolean(init)
97
+ && (init.type === "ArrowFunctionExpression" || init.type === "FunctionExpression"))
99
98
  }
100
99
  return DOCUMENTED_KINDS.has(declaration.type)
101
100
  }
102
101
 
103
- /** Whether a JSDoc block sits immediately before a node. */
104
- const hasJsdocBefore = (sourceCode, node) =>
105
- sourceCode
106
- .getCommentsBefore(node)
107
- .some((comment) => comment.type === "Block" && comment.value.startsWith("*"))
102
+ /** Whether the adjacent JSDoc has a declaration description rather than only tags or a file header. */
103
+ const hasJsdocBefore = (sourceCode, node) => jsdocBefore(sourceCode, node) !== null
108
104
 
109
105
  /** The declared name, for the message. */
110
106
  const nameOf = (declaration) => {
@@ -123,7 +119,7 @@ export const requireExportJsdoc = {
123
119
  schema: [],
124
120
  messages: {
125
121
  jsdoc:
126
- "`{{name}}` is exported with no doc block. This is surface other files depend on, and a name plus a signature says what it TAKES - never what it is for, or when to reach for it rather than the thing beside it.",
122
+ "`{{name}}` is exported without an adjacent JSDoc description. This is surface other files depend on, and a name plus a signature says what it TAKES - never what it is for, or when to reach for it rather than the thing beside it.",
127
123
  },
128
124
  },
129
125
  create(context) {
@@ -214,7 +210,7 @@ export const requirePublicMemberJsdoc = {
214
210
  schema: [],
215
211
  messages: {
216
212
  jsdoc:
217
- "`{{owner}}.{{name}}` is public surface with no doc block. A caller reaches the member, not the type's doc: say what it is for, what calling or reading it causes, or what it holds.",
213
+ "`{{owner}}.{{name}}` is public surface without an adjacent JSDoc description. A caller reaches the member, not the type's doc: say what it is for, what calling or reading it causes, or what it holds.",
218
214
  },
219
215
  },
220
216
  create(context) {
@@ -253,7 +249,7 @@ export const requireEnumMemberJsdoc = {
253
249
  schema: [],
254
250
  messages: {
255
251
  jsdoc:
256
- "Enum member `{{name}}` has no doc. State what CHOOSING it causes, not what it is called - a member is picked at a call site far from the switch that gives it meaning. (A rule can only see that a doc exists; whether it states a consequence is read by a person.)",
252
+ "Enum member `{{name}}` has no adjacent JSDoc description. State what CHOOSING it causes, not what it is called - a member is picked at a call site far from the switch that gives it meaning. (A rule can only see that a doc exists; whether it states a consequence is read by a person.)",
257
253
  },
258
254
  },
259
255
  create(context) {
@@ -334,7 +330,7 @@ const RESTATEMENT_FILLER = new Set(["a", "an", "the", "is", "of", "for", "to", "
334
330
 
335
331
  /** The content words a doc comment contributes once filler is stripped out. */
336
332
  const docContentWords = (comment) =>
337
- (comment.value.match(/[A-Za-z]+/g) || [])
333
+ (jsdocDescription(comment).match(/[A-Za-z]+/g) || [])
338
334
  .map((word) => word.toLowerCase())
339
335
  .filter((word) => !RESTATEMENT_FILLER.has(word))
340
336
 
@@ -350,11 +346,6 @@ const isPureRestatement = (comment, declaredName) => {
350
346
  return true
351
347
  }
352
348
 
353
- /** The doc block immediately before a node, or null. */
354
- const jsdocBefore = (sourceCode, node) =>
355
- sourceCode.getCommentsBefore(node).find((comment) => comment.type === "Block" && comment.value.startsWith("*"))
356
- || null
357
-
358
349
  /** A doc block that only re-spells the declared name is COMMENT-3's violation wearing COMMENT-1's shape. */
359
350
  export const noRestatedNameJsdoc = {
360
351
  meta: {
package/config-owner.mjs CHANGED
@@ -213,7 +213,7 @@ export const configParsedInMain = {
213
213
  /** The brand classes of `platform/config` a default must never be given to. */
214
214
  const BRANDS = new Set(["Secret", "Url"])
215
215
  const SECRET_READERS = new Set(["secret", "url", "host"])
216
- const DEFAULT_LITERAL = /^(?:|localhost|127\.0\.0\.1|0\.0\.0\.0|https?:\/\/.*)$/i
216
+ const DEFAULT_LITERAL = /^(?:localhost|127\.0\.0\.1|0\.0\.0\.0|https?:\/\/.*)?$/i
217
217
 
218
218
  /** No secret or URL value has a default of any kind: a missing value stops the boot. */
219
219
  export const noSecretDefault = {
package/idempotency.mjs CHANGED
@@ -9,8 +9,8 @@
9
9
  * - `inbox-dedupe-required` judges every DELIVERY method of a service. Doors (consumers, `SignedWebhook` controllers) are
10
10
  * thin (R88): they map the delivery and dispatch one message, so the claim lives in the `*.service.ts` the handler calls,
11
11
  * inside its transaction. A delivery method is a public method of a `<name>.service.ts` whose FIRST parameter has an
12
- * `eventId` property, read by the parameter's TYPE. The FIRST awaited expression of such a method must be
13
- * `claim(source, eventId)` on a receiver whose type is the `Inbox` port of `platform/inbox`, and the method must return
12
+ * `eventId` property, read by the parameter's TYPE. Its first awaited work is the Inbox claim, directly or inside
13
+ * the callback of its own EntityManager transaction, with that callback's manager, and the method must return
14
14
  * early when the claim answers `false`. A lookalike `Inbox` declared by another owner, a renamed receiver and a
15
15
  * property injection are all judged by the receiver's type, so only the real port satisfies the rule; a keyword or a
16
16
  * class name never does.
@@ -20,12 +20,15 @@
20
20
  * if ((await this.inbox.claim(source, id)) === false) { return }
21
21
  * const fresh = await this.inbox.claim(source, id)
22
22
  * if (!fresh) return
23
- * Whether the handler has side effects BEFORE its first await stays a review question (the call graph is not read).
23
+ * A transaction delivery returns or awaits one transaction as its sole statement. The actual selected inline callback
24
+ * claims with its own manager in its first statement, then returns on false; no outer work runs after a duplicate.
25
+ * Direct-claim synchronous effects, getters and called helpers stay review questions (the call graph is not read).
24
26
  */
25
27
  import { hfsOf } from "./lib/hfs.mjs"
26
28
  import { walk } from "./lib/ast.mjs"
27
29
  import { baseName, isOwnedType, ownerNameOf } from "./lib/ports.mjs"
28
- import { typed } from "./lib/types.mjs"
30
+ import { resolveVariable } from "./lib/transactions.mjs"
31
+ import { isPackageType, typed } from "./lib/types.mjs"
29
32
 
30
33
  /** The `Inbox` port of `platform/inbox`. */
31
34
  const isInboxType = (context, node) => isOwnedType(context, node, { name: "Inbox", capability: "inbox", tier: "platform" })
@@ -73,6 +76,52 @@ const firstAwait = (body) => {
73
76
  return first
74
77
  }
75
78
 
79
+ /** True when an expression evaluates a call, construction or write before the designated call completes. */
80
+ const hasPriorEffect = (expression, call) => {
81
+ let found = false
82
+ walk(expression, (node) => {
83
+ if (["CallExpression", "NewExpression", "AssignmentExpression", "UpdateExpression"].includes(node.type)
84
+ && node !== call && node.range[1] < call.range[1]) found = true
85
+ }, { intoFunctions: false })
86
+ return found
87
+ }
88
+
89
+ /** The first awaited claim in the actual callback of a sole, returned or awaited EntityManager transaction. */
90
+ const transactionClaim = (context, body) => {
91
+ if (body.body.length !== 1) return null
92
+ const outer = body.body[0]
93
+ const expression = outer.type === "ReturnStatement" ? outer.argument
94
+ : outer.type === "ExpressionStatement" && outer.expression.type === "AwaitExpression" ? outer.expression : null
95
+ const call = expression?.type === "AwaitExpression" ? expression.argument : expression
96
+ const callee = call?.callee
97
+ if (call?.type !== "CallExpression" || callee.type !== "MemberExpression" || callee.computed
98
+ || callee.property.type !== "Identifier" || callee.property.name !== "transaction"
99
+ || !isPackageType(context, callee.object, "EntityManager", "typeorm") || hasPriorEffect(call, call)) return null
100
+ // TypeORM runs arg0 when it is a function, otherwise arg1. Never accept a later, ignored callback.
101
+ const isolation = call.arguments[0]
102
+ const callback = call.arguments.length === 1 ? isolation
103
+ : call.arguments.length === 2 && isolation.type === "Literal"
104
+ && ["READ UNCOMMITTED", "READ COMMITTED", "REPEATABLE READ", "SERIALIZABLE"].includes(isolation.value)
105
+ ? call.arguments[1] : null
106
+ if (!callback || !["ArrowFunctionExpression", "FunctionExpression"].includes(callback.type)
107
+ || !callback.async || callback.body.type !== "BlockStatement" || callback.params.length !== 1) return null
108
+ const manager = callback.params[0]
109
+ if (manager.type !== "Identifier" || !isPackageType(context, manager, "EntityManager", "typeorm")) return null
110
+ const awaited = firstAwait(callback.body)
111
+ const parent = awaited?.parent
112
+ const statement = parent?.type === "VariableDeclarator" && parent.init === awaited
113
+ && parent.parent.declarations.length === 1 ? parent.parent
114
+ : ["UnaryExpression", "BinaryExpression"].includes(parent?.type) ? parent.parent
115
+ : parent?.type === "ExpressionStatement" ? parent : null
116
+ if (statement !== callback.body.body[0] || statement?.parent !== callback.body) return null
117
+ const claim = awaited.argument
118
+ const argument = claim?.type === "CallExpression" ? claim.arguments[2] : null
119
+ const managerBinding = resolveVariable(context.sourceCode, manager)
120
+ const argumentBinding = argument?.type === "Identifier" ? resolveVariable(context.sourceCode, argument) : null
121
+ if (!managerBinding || managerBinding.defs[0]?.name !== manager || !argumentBinding
122
+ || argumentBinding !== managerBinding || hasPriorEffect(statement, claim)) return null
123
+ return awaited
124
+ }
76
125
  /** Whether a parameter's type has an `eventId` property: the shape of a delivery. */
77
126
  const isDeliveryParam = (context, param) => {
78
127
  const target = param.type === "TSParameterProperty" ? param.parameter : param.type === "AssignmentPattern" ? param.left : param
@@ -107,7 +156,7 @@ export const inboxDedupeRequired = {
107
156
  if (node.accessibility === "private" || node.accessibility === "protected") return
108
157
  const first = node.value.params[0]
109
158
  if (!first || !isDeliveryParam(context, first)) return
110
- const awaited = firstAwait(node.value.body)
159
+ const awaited = transactionClaim(context, node.value.body) ?? firstAwait(node.value.body)
111
160
  const call = awaited?.argument
112
161
  const claims = call?.type === "CallExpression"
113
162
  && call.callee.type === "MemberExpression"
package/index.mjs CHANGED
@@ -65,6 +65,7 @@ import { recommended as webhooksRecommended, rules as webhooksRules } from "./we
65
65
  import { recommended as realtimeRecommended, rules as realtimeRules } from "./realtime.mjs"
66
66
  import { recommended as featureThinRecommended, rules as featureThinRules } from "./feature-thin.mjs"
67
67
  import { recommended as projectGraphRecommended, rules as projectGraphRules } from "./project-graph.mjs"
68
+ import { recommended as sonarParityRecommended, rules as sonarParityRules } from "./sonar-parity.mjs"
68
69
 
69
70
  /** Each law's contribution, kept separate so a duplicate name is detectable rather than silent. */
70
71
  const CONTRIBUTIONS = [
@@ -115,6 +116,7 @@ const CONTRIBUTIONS = [
115
116
  { law: "webhooks", rules: webhooksRules, recommended: webhooksRecommended },
116
117
  { law: "realtime", rules: realtimeRules, recommended: realtimeRecommended },
117
118
  { law: "feature-thin", rules: featureThinRules, recommended: featureThinRecommended },
119
+ { law: "sonar-parity", rules: sonarParityRules, recommended: sonarParityRecommended },
118
120
  ]
119
121
 
120
122
  /** Every gathered law. */
package/lib/config.mjs CHANGED
@@ -32,7 +32,7 @@ export const IGNORED = Object.freeze(["dist/**", "coverage/**", "**/node_modules
32
32
  */
33
33
  const ROOT_SCRIPTS = Object.freeze(["*.js", "*.mjs", "*.cjs"])
34
34
 
35
- /** The typescript-eslint rules the canon borrows (BE-CONVENTION 1.17, R72/R73), all `error`. */
35
+ /** The typescript-eslint rules the canon borrows (BE-CONVENTION 1.17, R72/R73; Sonar S1874, S6551 and S7503 through R235), all `error`. */
36
36
  export const BORROWED = Object.freeze({
37
37
  "@typescript-eslint/consistent-type-assertions": ["error", { assertionStyle: "never" }],
38
38
  "@typescript-eslint/no-explicit-any": "error",
@@ -41,6 +41,9 @@ export const BORROWED = Object.freeze({
41
41
  "@typescript-eslint/no-misused-promises": "error",
42
42
  "@typescript-eslint/switch-exhaustiveness-check": "error",
43
43
  "@typescript-eslint/array-type": ["error", { default: "generic", readonly: "generic" }],
44
+ "@typescript-eslint/no-deprecated": "error",
45
+ "@typescript-eslint/no-base-to-string": "error",
46
+ "@typescript-eslint/require-await": "error",
44
47
  "no-console": "error",
45
48
  })
46
49
 
package/lib/doors.mjs CHANGED
@@ -55,7 +55,9 @@ export const aliasTarget = (context, identifier) => {
55
55
  const { checker, toTs } = typed(context)
56
56
  const tsNode = toTs(identifier)
57
57
  const symbol = tsNode && checker.getSymbolAtLocation(tsNode)
58
- return symbol && (symbol.flags & ts.SymbolFlags.Alias) !== 0 ? checker.getAliasedSymbol(symbol) : symbol
58
+ if (!symbol) return symbol
59
+ const isAlias = (symbol.flags & ts.SymbolFlags.Alias) !== 0
60
+ return isAlias ? checker.getAliasedSymbol(symbol) : symbol
59
61
  }
60
62
 
61
63
  /** The methods of a class (instance, non-constructor) that carry one of the decorators `names` of `source`. */
package/module-shape.mjs CHANGED
@@ -4,8 +4,7 @@
4
4
  * Globality is a composition decision, not a property of a module: a capability is registered once per app, in the app's
5
5
  * `app.module.ts`, as `X.register({ isGlobal: true, ...options })`, and never listed in another module's `imports`.
6
6
  *
7
- * - `no-global-decorator` refuses `@Global()` anywhere, specs included. (It replaces `global-module-allowlist`: the
8
- * manifest no longer carries a list of modules that may be global.)
7
+ * - `no-global-decorator` refuses `@Global()` anywhere, specs included.
9
8
  * - `is-global-only-in-app` refuses `isGlobal: true` (and a dynamic module literal `global: true`) outside
10
9
  * `apps/<app>/src/app.module.ts`.
11
10
  * - `no-cross-owner-module-import` refuses a `@Module({ imports })` entry whose module class is declared by another
package/package.json CHANGED
@@ -1,15 +1,16 @@
1
1
  {
2
2
  "name": "@starci/eslint-canon-be",
3
- "version": "3.0.9",
3
+ "version": "3.1.0",
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
- "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/**",
package/resilience.mjs CHANGED
@@ -185,7 +185,7 @@ const waitsThrough = (context, checker, call, depth, seen) => {
185
185
  const hfs = hfsOf(context)
186
186
  const target = ts.isPropertyAccessExpression(call.expression) ? call.expression.name : call.expression
187
187
  let symbol = checker.getSymbolAtLocation(target)
188
- if (symbol && symbol.flags & ts.SymbolFlags.Alias) symbol = checker.getAliasedSymbol(symbol)
188
+ if (symbol && (symbol.flags & ts.SymbolFlags.Alias) !== 0) symbol = checker.getAliasedSymbol(symbol)
189
189
  for (const declaration of symbol?.declarations ?? []) {
190
190
  if (seen.has(declaration)) continue
191
191
  seen.add(declaration)
@@ -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
+ };