@starci/eslint-canon-fe 8.0.8 → 8.0.9

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 (117) hide show
  1. package/CHANGELOG.md +1 -0
  2. package/README.md +5 -4
  3. package/client-boundary.mjs +4 -2
  4. package/comments.mjs +1 -1
  5. package/docs/next-conventions.md +1 -1
  6. package/docs/translation.md +1 -1
  7. package/docs/transport.md +2 -2
  8. package/{shape-slot.mjs → drawing.mjs} +5 -5
  9. package/file-layout.mjs +8 -6
  10. package/icon.mjs +1 -23
  11. package/index.mjs +2 -2
  12. package/lib/ast.mjs +14 -11
  13. package/lib/config.mjs +1 -1
  14. package/lib/scope.mjs +6 -2
  15. package/lib/why.mjs +113 -110
  16. package/naming.mjs +4 -4
  17. package/package.json +3 -1
  18. package/project-graph.mjs +1 -1
  19. package/runtime/engine/config.mjs +494 -0
  20. package/runtime/engine/invalid-config.mjs +15 -0
  21. package/runtime/engine/orca-config.mjs +27 -0
  22. package/runtime/knowledge/hfs/canon-pins.yaml +160 -29
  23. package/runtime/knowledge/hfs/slots.yaml +912 -120
  24. package/runtime/knowledge/patterns/fe/folder.yaml +131 -25
  25. package/runtime/knowledge/sonar-gate.yaml +52 -18
  26. package/runtime/modules/models/registry.yaml +591 -0
  27. package/runtime/modules/models/runtimes.yaml +701 -0
  28. package/runtime/packages/hfs/templates/app/package-scripts/package.json +3 -10
  29. package/runtime/scripts/api/git/config-get.mjs +10 -0
  30. package/runtime/scripts/api/git/lib.mjs +16 -26
  31. package/runtime/scripts/api/git/ls-files.mjs +6 -0
  32. package/runtime/scripts/api/git/remote.mjs +5 -0
  33. package/runtime/scripts/api/git/rev-parse-query.mjs +5 -0
  34. package/runtime/scripts/hfs/architecture/ast-walks.mjs +220 -0
  35. package/runtime/scripts/hfs/architecture/backend.mjs +16 -23
  36. package/runtime/scripts/hfs/architecture/background-unowned.mjs +16 -34
  37. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +3 -1
  38. package/runtime/scripts/hfs/architecture/clones.mjs +4 -4
  39. package/runtime/scripts/hfs/architecture/config.mjs +9 -6
  40. package/runtime/scripts/hfs/architecture/connection-map.mjs +20 -8
  41. package/runtime/scripts/hfs/architecture/context-coupling.mjs +88 -0
  42. package/runtime/scripts/hfs/architecture/context-map.mjs +237 -0
  43. package/runtime/scripts/hfs/architecture/context-owner.mjs +82 -0
  44. package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +47 -0
  45. package/runtime/scripts/hfs/architecture/context-transaction.mjs +60 -0
  46. package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +4 -10
  47. package/runtime/scripts/hfs/architecture/contracts.mjs +33 -162
  48. package/runtime/scripts/hfs/architecture/dead-exports.mjs +58 -29
  49. package/runtime/scripts/hfs/architecture/doc-language.mjs +1 -1
  50. package/runtime/scripts/hfs/architecture/entrypoint.mjs +2 -1
  51. package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +1 -1
  52. package/runtime/scripts/hfs/architecture/feature-shape.mjs +5 -2
  53. package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
  54. package/runtime/scripts/hfs/architecture/frontend.mjs +17 -43
  55. package/runtime/scripts/hfs/architecture/hfs.mjs +24 -39
  56. package/runtime/scripts/hfs/architecture/i18n-keys.mjs +1 -1
  57. package/runtime/scripts/hfs/architecture/index.mjs +17 -4
  58. package/runtime/scripts/hfs/architecture/machine-ast.mjs +44 -11
  59. package/runtime/scripts/hfs/architecture/managed-scripts.mjs +27 -0
  60. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +30 -6
  61. package/runtime/scripts/hfs/architecture/next-data.mjs +22 -63
  62. package/runtime/scripts/hfs/architecture/owners.mjs +35 -15
  63. package/runtime/scripts/hfs/architecture/register-once.mjs +2 -1
  64. package/runtime/scripts/hfs/architecture/registration.mjs +23 -81
  65. package/runtime/scripts/hfs/architecture/required-files.mjs +20 -12
  66. package/runtime/scripts/hfs/architecture/schema-owner.mjs +17 -114
  67. package/runtime/scripts/hfs/architecture/server-action.mjs +24 -0
  68. package/runtime/scripts/hfs/architecture/side-boundary.mjs +16 -0
  69. package/runtime/scripts/hfs/architecture/source-names.mjs +27 -147
  70. package/runtime/scripts/hfs/architecture/sql-owner.mjs +59 -18
  71. package/runtime/scripts/hfs/architecture/sql-returning.mjs +64 -0
  72. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +111 -0
  73. package/runtime/scripts/hfs/architecture/supabase-be.mjs +131 -0
  74. package/runtime/scripts/hfs/architecture/supabase-tables.mjs +42 -0
  75. package/runtime/scripts/hfs/architecture/supabase.mjs +434 -0
  76. package/runtime/scripts/hfs/architecture/surface.mjs +9 -3
  77. package/runtime/scripts/hfs/architecture/test-world-files.mjs +9 -19
  78. package/runtime/scripts/hfs/architecture/tiers.mjs +47 -5
  79. package/runtime/scripts/hfs/architecture/transport-owner.mjs +3 -3
  80. package/runtime/scripts/hfs/architecture/typescript.mjs +12 -26
  81. package/runtime/scripts/hfs/declaration-shape.mjs +62 -0
  82. package/runtime/scripts/hfs/declaration-slots.mjs +75 -0
  83. package/runtime/scripts/hfs/edition-slots.mjs +66 -0
  84. package/runtime/scripts/hfs/manifest-shape.mjs +109 -32
  85. package/runtime/scripts/hfs/param-names.mjs +18 -0
  86. package/runtime/scripts/hfs/path-findings.mjs +1 -1
  87. package/runtime/scripts/hfs/project-graph.mjs +3 -3
  88. package/runtime/scripts/hfs/repo-identity.mjs +9 -6
  89. package/runtime/scripts/hfs/rule-params-shape.mjs +50 -0
  90. package/runtime/scripts/hfs/rules/fe-no-tests.mjs +6 -5
  91. package/runtime/scripts/hfs/rules/read.mjs +1 -1
  92. package/runtime/scripts/hfs/rules/secrets.mjs +3 -3
  93. package/runtime/scripts/hfs/rules/spec-placement.mjs +1 -1
  94. package/runtime/scripts/hfs/slots.mjs +78 -108
  95. package/runtime/scripts/hfs/test-world-slot.mjs +6 -0
  96. package/runtime/scripts/hfs/typescript-programs.mjs +0 -5
  97. package/runtime/scripts/hfs/view.mjs +3 -2
  98. package/runtime/scripts/lib/dockerfile.mjs +124 -0
  99. package/runtime/scripts/lib/git.mjs +36 -0
  100. package/runtime/scripts/lib/i18n.mjs +86 -0
  101. package/runtime/scripts/lib/json.mjs +13 -3
  102. package/runtime/scripts/lib/language.mjs +24 -5
  103. package/runtime/scripts/lib/list.mjs +20 -0
  104. package/runtime/scripts/lib/path-key.mjs +57 -2
  105. package/runtime/scripts/lib/read-text.mjs +34 -0
  106. package/runtime/scripts/lib/regex.mjs +4 -0
  107. package/runtime/scripts/lib/secret-patterns.mjs +4 -3
  108. package/runtime/scripts/lib/sops-envelope.mjs +58 -0
  109. package/runtime/scripts/lib/stack-services.mjs +7 -31
  110. package/runtime/scripts/lib/ts-ast.mjs +35 -0
  111. package/runtime/scripts/lib/walk.mjs +4 -3
  112. package/size-and-state-budget.mjs +1 -1
  113. package/tokens.mjs +10 -18
  114. package/translation.mjs +1 -1
  115. package/transport.mjs +2 -2
  116. package/runtime/scripts/api/sops/decrypt.mjs +0 -16
  117. package/runtime/scripts/lib/test-secrets.mjs +0 -116
package/CHANGELOG.md CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - 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.
5
6
  - 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.
6
7
  ## Unreleased (C0 batch)
7
8
 
package/README.md CHANGED
@@ -8,7 +8,7 @@ and colocated class-name composition consistent without prescribing a custom ren
8
8
  npm i -D @starci/eslint-canon-fe eslint-plugin-react-hooks typescript
9
9
  ```
10
10
 
11
- A repository's `eslint.config.mjs` is the managed one-liner `hfs sync` renders (and `hfs check` compares, R05/R17):
11
+ A repository's `eslint.config.mjs` is the managed one-liner `starci app sync` renders (and `starci app check` compares, R05/R17):
12
12
 
13
13
  ```js
14
14
  import { loadHfs, starciFeConfig } from "@starci/eslint-canon-fe"
@@ -42,12 +42,13 @@ naming, props, served locales, component splits, design tokens, type safety, typ
42
42
  class-name ownership. Reusable class names belong in a colocated `classNames.ts` module and should
43
43
  be composed with HeroUI `cn` using one utility token per argument.
44
44
 
45
- The `shape-slot` law holds the split surfaces (blocks, pages, layouts, overlays under `components/`
45
+ The `drawing` law holds the split surfaces (blocks, pages, layouts, overlays under `components/`
46
46
  or `features/`): the pure `XBase` takes `{ state, props, on }` of atoms (`base-props-atom`), only its
47
47
  sibling `index.tsx` imports it and never re-exports it (`base-import-pair`), `XState` names drawn
48
48
  shapes and never a data status (`no-data-status-shape`), and a block renders each slot's
49
- loading/forbidden/error/empty through `SlotView` (`slot-status-through-slotview`). Reference tree:
50
- `.claude/examples/shape-slot`.
49
+ loading/forbidden/error/empty through `SlotView` (`slot-status-through-slotview`). The law's name is
50
+ the `drawing` slot role of `component.tsx`; a split-surface reference tree lives under
51
+ `.claude/examples/`.
51
52
 
52
53
  The one locale pattern everywhere is `next-intl`, a `[locale]` segment, default `vi`,
53
54
  `localePrefix: "as-needed"`, and `proxy.ts` instead of `middleware.ts`.
@@ -139,7 +139,8 @@ const isMarkerStatement = (statement) =>
139
139
 
140
140
  /** True when a TypeScript source file opens with `import "server-only"`. */
141
141
  const tsFileIsMarked = (file) => {
142
- const first = file.statements[0]
142
+ // a file-level directive ("use server") is the prologue: the marker is the first statement after it
143
+ const first = file.statements.find((statement) => !(ts.isExpressionStatement(statement) && ts.isStringLiteral(statement.expression)))
143
144
  return Boolean(first) && ts.isImportDeclaration(first) && !first.importClause && ts.isStringLiteral(first.moduleSpecifier) && first.moduleSpecifier.text === "server-only"
144
145
  }
145
146
 
@@ -183,7 +184,8 @@ export const serverModuleMarksServerOnly = {
183
184
  return {
184
185
  Program(program) {
185
186
  client = clientDirective(program) !== null
186
- marked = isMarkerStatement(program.body[0])
187
+ // a file-level directive ("use server") is the prologue, not a statement: the marker is the first statement after it
188
+ marked = isMarkerStatement(program.body.find((statement) => !(statement.type === "ExpressionStatement" && typeof statement.directive === "string")))
187
189
  },
188
190
  "ImportDeclaration, ExportNamedDeclaration, ExportAllDeclaration"(node) {
189
191
  if (!node.source || node.importKind === "type" || node.exportKind === "type") return
package/comments.mjs CHANGED
@@ -25,7 +25,7 @@ import { fileOf, inSlot } from "./lib/scope.mjs"
25
25
  * - a single class covering both trips the misleading-character-class rule, and a rule that has to
26
26
  * be silenced to exist is one nobody trusts.
27
27
  */
28
- export const hasEmoji = (text) =>
28
+ const hasEmoji = (text) =>
29
29
  typeof text === "string" &&
30
30
  (/\p{Extended_Pictographic}/u.test(text) || /[\u{1F1E6}-\u{1F1FF}]{2}/u.test(text))
31
31
 
@@ -221,7 +221,7 @@ export default getRequestConfig(async () => ({ locale: "vi", messages: {} }))
221
221
  **Valid** (`apps/web/src/modules/i18n/index.ts`)
222
222
 
223
223
  ```ts
224
- import { createAppI18n } from "@nivo/i18n"
224
+ import { createAppI18n } from "@acme/i18n"
225
225
  export const i18n = createAppI18n({ locales: ["vi"] })
226
226
  ```
227
227
 
@@ -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 nivo-fe's `scripts/check-i18n-catalog.mjs`: 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 `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.
6
6
 
7
7
  Every rule below is an error in `starciFeConfig`; none can be switched off or suppressed inline.
8
8
 
package/docs/transport.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Law module: `transport.mjs`. Catalogue: R50 FE_TRANSPORT_OWNER, R51 FE_HTTP_STATUS_COLLAPSE, R52 FE_WIRE_GENERATED.
4
4
 
5
- One repository, one client, one result vocabulary. The client is the file of HFS slot `fe.transport.client` (`apps/<app>/src/modules/api/client.ts`, a one-app repository) or `fe.package.api.client` (`packages/<family>-api/src/client.ts`, the shared client of a multi-app repository); the vocabulary is `Outcome<T>` (`ok`, `refused`, `invalid`, `not-found`, `unavailable`), declared in the `fe.transport.outcome` / `fe.package.api.outcome` file. The rules read the slot of the file being linted, never its path. 401 and 403 become `refused`; nothing collapses a status into `null`; wire types are generated from the contract copy in `modules/api/contract/`.
5
+ One repository, one client, one result vocabulary. The client is the file of HFS slot `fe.transport.client` (`apps/<app>/src/modules/api/client.ts`, a one-app repository) or `fe.package.api.client` (`packages/<family>-api/src/client.ts`, the shared client of a multi-app repository); the vocabulary is `Outcome<T>` (`ok`, `refused`, `invalid`, `not-found`, `unavailable`), declared in the file of the slot the manifest marks `outcomeHome`: `fe.transport.outcome`, `fe.package.api.outcome`, or `fe.modules.db.outcome` (`modules/db/outcome.ts`) in a lite app, whose db owner is the transport. The rules read the slot of the file being linted, never its path. 401 and 403 become `refused`; nothing collapses a status into `null`; wire types are generated from the contract copy in `modules/api/contract/`.
6
6
 
7
7
  Every rule below is an error in `starciFeConfig`; none can be switched off or suppressed inline.
8
8
 
@@ -171,7 +171,7 @@ switch (outcome.kind) {
171
171
 
172
172
  ## `starci-fe/one-outcome-union`
173
173
 
174
- A result union (`ok` / `kind` discriminant) is declared only in the outcome slot; elsewhere the code composes `Outcome<T>`.
174
+ A result union (`ok` / `kind` discriminant) is declared only in the outcome-home slot (`outcomeHome: true` in the slot manifest); elsewhere the code composes `Outcome<T>`.
175
175
 
176
176
  A union type alias is a result union when its resolved members are all object types that carry the same literal-typed discriminant in the result vocabulary: `ok` (each member pins it to one value, `true` and `false` both present) or `kind` (at least one literal of the Outcome vocabulary: `ok`, `refused`, `forbidden`, `invalid`, `not-found`, `unavailable`). The members are read through the type checker, so `Ok<T> | Failure` and intersections are seen as what they are; no name (`*Outcome`, `*Result`) decides anything. UI state unions (`status`, `state`, `type`, a `kind` of tree nodes or menu items) are not result vocabulary and pass; so does an alias that only composes the one union (`type Read = Outcome<Course>`, `Exclude<Outcome<T>, ...>`). `Outcome<T> | { ok: false; kind: "conflict" }` adds an arm to the one union and is refused.
177
177
 
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The rules that hold the shape-slot law (example: examples/shape-slot).
2
+ * The rules that hold the drawing law (the `drawing` slot role: a surface's pure `component.tsx`).
3
3
  *
4
4
  * TWO AXES, AND ONLY ONE IS DRAWN. A surface's `state` is its SHAPE: a layout the owner approves as
5
5
  * one drawing. A slot's data status (loading, forbidden, error, empty) is not a shape; it is a
@@ -77,7 +77,7 @@ const membersOf = (node, types, seen = new Set()) => {
77
77
  /** The key a property signature declares. */
78
78
  const keyOf = (member) => (member.key?.type === "Identifier" ? member.key.name : member.key?.value)
79
79
 
80
- // -- SHAPE-SLOT-1 ----------------------------------------------------------------------------------
80
+ // -- DRAWING-1 ----------------------------------------------------------------------------------
81
81
 
82
82
  /** The pure half takes `{ state, props, on }` of atoms; only a layout also takes `children`. */
83
83
  export const basePropsAtom = {
@@ -189,7 +189,7 @@ export const basePropsAtom = {
189
189
  },
190
190
  }
191
191
 
192
- // -- SHAPE-SLOT-2 ----------------------------------------------------------------------------------
192
+ // -- DRAWING-2 ----------------------------------------------------------------------------------
193
193
 
194
194
  /** Only the sibling `index.tsx` (and its specs) may reach `component.tsx`; only `X` leaves the folder. */
195
195
  export const baseImportPair = {
@@ -241,7 +241,7 @@ export const baseImportPair = {
241
241
  },
242
242
  }
243
243
 
244
- // -- SHAPE-SLOT-3 ----------------------------------------------------------------------------------
244
+ // -- DRAWING-3 ----------------------------------------------------------------------------------
245
245
 
246
246
  /** Words that name a data status or an open/closed lifecycle, never a drawn shape. */
247
247
  const DATA_STATUS = new Set([
@@ -279,7 +279,7 @@ export const noDataStatusShape = {
279
279
  },
280
280
  }
281
281
 
282
- // -- SHAPE-SLOT-4 ----------------------------------------------------------------------------------
282
+ // -- DRAWING-4 ----------------------------------------------------------------------------------
283
283
 
284
284
  /** Flags that are a slot's data status. */
285
285
  const STATUS_FLAG = /^(?:isLoading|isError|isForbidden|isPending|isValidating|isEmpty)$/
package/file-layout.mjs CHANGED
@@ -205,14 +205,15 @@ const featureTiers = (hfs) => {
205
205
  }
206
206
 
207
207
  /**
208
- * In a monorepo, the shared package stops below the block.
208
+ * In a front end with two or more apps, the shared package stops below the block.
209
209
  *
210
210
  * WHY THE SLOT IS ENOUGH HERE. The feature line is already drawn by the layer: a leaf knows
211
211
  * no domain and a block is a domain sentence, so which side of the workspace each belongs on
212
212
  * follows from the layer the slot reports. The layers a shared package may hold are the `kinds` of
213
- * `fe.package.ui`; an app holds the shared ones only when the repository declares that package.
213
+ * `fe.package.ui`; an app holds the shared ones only when the repository declares that package and another app can
214
+ * consume them. A one-app front end has nothing to share, so every tier may stay with that app.
214
215
  *
215
- * WHAT IT COST TO LEARN. A shared package carried one `blocks/FleetRow/` while its own header
216
+ * WHAT IT COST TO LEARN. A shared package carried one `blocks/WorkerRow/` while its own header
216
217
  * insisted that "a block carries feature meaning and therefore belongs to the app that owns the
217
218
  * feature". The document was right, the tree disagreed, and nothing turned red for either of them -
218
219
  * which is the whole argument for this rule rather than the paragraph.
@@ -222,18 +223,19 @@ export const monorepoTierBelongsToItsSide = {
222
223
  type: "problem",
223
224
  docs: {
224
225
  description:
225
- "In a monorepo the shared package holds leaves, composites and branches; blocks, overlays, layouts and pages belong to the app that owns the feature.",
226
+ "With two or more apps, the shared package holds leaves, composites and branches; blocks, overlays, layouts and pages belong to the app that owns the feature.",
226
227
  },
227
228
  schema: [],
228
229
  messages: {
229
230
  featureInPackage:
230
231
  "`{{tier}}/` knows a feature, so it belongs to the app that owns that feature, not to the shared package - every app that never wanted this domain now ships it. Move it to `apps/<app>/src/components/{{tier}}/`.",
231
232
  vocabularyInApp:
232
- "`{{tier}}/` knows no feature, so one copy belongs to every app - left here, the second app writes it again and the two drift with nothing to notice. Move it to `packages/ui/src/{{tier}}/`.",
233
+ "`{{tier}}/` knows no feature, and this repository has multiple apps, so one copy belongs to every app - left here, another app writes it again and the two drift with nothing to notice. Move it to `packages/ui/src/{{tier}}/`.",
233
234
  },
234
235
  },
235
236
  create(context) {
236
237
  const hfs = hfsOf(context)
238
+ if (hfs.apps.length < 2) return {}
237
239
  const folder = unknownLayerFolder(context)
238
240
  const inPackage = inSlot(context, "fe.package.ui") && folder !== null && featureTiers(hfs).has(folder) ? folder : null
239
241
  const tier = kindOfFile(context)
@@ -257,7 +259,7 @@ export const monorepoTierBelongsToItsSide = {
257
259
  *
258
260
  * WHY THIS RULE EXISTS RATHER THAN THE PARAGRAPH ALONE. The law said "the route file mounts and
259
261
  * nothing else" and had said so for as long as the tier list existed. Nothing checked it. A page
260
- * owner was written to `app/<segment>/fleet-page.tsx`, carried through a build, a lint run, a
262
+ * owner was written to `app/<segment>/workers-page.tsx`, carried through a build, a lint run, a
261
263
  * typecheck, four sealed screenshots and an approval, and arrived at the edge of a production write
262
264
  * with every gate green - because every gate was reading rules, and this one was only prose.
263
265
  *
package/icon.mjs CHANGED
@@ -12,6 +12,7 @@
12
12
  * generalises on its own, and the gap between them is exactly where a third step gets invented.
13
13
  */
14
14
 
15
+ import { isClassAttribute, staticText } from "./lib/ast.mjs"
15
16
  import { hfsOf } from "./lib/hfs.mjs"
16
17
  import { fileOf, isComponentFile, isProductSource, kindOfFile, roleOfFile } from "./lib/scope.mjs"
17
18
 
@@ -51,14 +52,6 @@ const HEROICON_PACKAGES = new Set([
51
52
  "@starci/heroicons/16/solid",
52
53
  ])
53
54
 
54
- /**
55
- * Product reactions are attributed artwork rather than glyph-library imports.
56
- *
57
- * Their leaf owns this closed vocabulary and maps it to checked-in Fluent Emoji Flat SVGs under
58
- * `public/reactions/`; callers pass identities, never Unicode pictographs or asset paths.
59
- */
60
- export const REACTION_ASSET_NAMES = new Set(["like", "love", "haha", "wow", "sad", "angry"])
61
-
62
55
  /**
63
56
  * True when the file is the entry (slot role `entry`) of the component owner named `owner` in the layer `kind`.
64
57
  * The slot says which layer and which role; the owner's own folder name is the one fact it cannot say, so it is compared.
@@ -84,21 +77,6 @@ const isGlyphImport = (source) => {
84
77
  /** A glyph size written as a fraction of a step, or as an arbitrary value. */
85
78
  const OFF_SCALE_GLYPH = /\bsize-(?:\d+\.\d+|\[[^\]]+\])/
86
79
 
87
- /** True for a `className` / `class` JSX attribute. */
88
- const isClassAttribute = (node) =>
89
- node.type === "JSXAttribute" && node.name && (node.name.name === "className" || node.name.name === "class")
90
-
91
- /** Static string carried by a JSX attribute, or by a module constant holding a class string. */
92
- const staticText = (value) => {
93
- if (!value) return null
94
- if (value.type === "Literal" && typeof value.value === "string") return value.value
95
- if (value.type === "TemplateLiteral" && value.expressions.length === 0) {
96
- return value.quasis.map((quasi) => quasi.value.cooked).join(" ")
97
- }
98
- if (value.type === "JSXExpressionContainer") return staticText(value.expression)
99
- return null
100
- }
101
-
102
80
  // -- ICON-6 ----------------------------------------------------------------------------------------
103
81
 
104
82
  /** A glyph library is named in one file, and a caller asks for a MEANING instead. */
package/index.mjs CHANGED
@@ -35,7 +35,7 @@ import { recommended as nativeControlsRecommended, rules as nativeControlsRules
35
35
  import { recommended as nextConventionsRecommended, rules as nextConventionsRules } from "./next-conventions.mjs"
36
36
  import { recommended as propsRecommended, rules as propsRules } from "./props-and-slots.mjs"
37
37
  import { recommended as servedLocaleRecommended, rules as servedLocaleRules } from "./served-locale.mjs"
38
- import { recommended as shapeSlotRecommended, rules as shapeSlotRules } from "./shape-slot.mjs"
38
+ import { recommended as drawingRecommended, rules as drawingRules } from "./drawing.mjs"
39
39
  import { recommended as sizeBudgetRecommended, rules as sizeBudgetRules } from "./size-and-state-budget.mjs"
40
40
  import { recommended as sizeGrowthRecommended, rules as sizeGrowthRules } from "./size-growth.mjs"
41
41
  import { recommended as statusColorsRecommended, rules as statusColorsRules } from "./status-colors.mjs"
@@ -77,7 +77,7 @@ const CONTRIBUTIONS = [
77
77
  { law: "next-conventions", rules: nextConventionsRules, recommended: nextConventionsRecommended },
78
78
  { law: "props-and-slots", rules: propsRules, recommended: propsRecommended },
79
79
  { law: "served-locale", rules: servedLocaleRules, recommended: servedLocaleRecommended },
80
- { law: "shape-slot", rules: shapeSlotRules, recommended: shapeSlotRecommended },
80
+ { law: "drawing", rules: drawingRules, recommended: drawingRecommended },
81
81
  { law: "size-and-state-budget", rules: sizeBudgetRules, recommended: sizeBudgetRecommended },
82
82
  { law: "size-growth", rules: sizeGrowthRules, recommended: sizeGrowthRecommended },
83
83
  { law: "status-colors", rules: statusColorsRules, recommended: statusColorsRecommended },
package/lib/ast.mjs CHANGED
@@ -47,20 +47,23 @@ export const calleeName = (node) => {
47
47
  /** The name of a JSX element for a plain tag or a component; null for a member or namespaced name. */
48
48
  export const tagName = (opening) => (opening.name.type === "JSXIdentifier" ? opening.name.name : null)
49
49
 
50
- /** True for a function expression or an arrow function. */
51
- export const isFunction = (node) => node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression"
50
+ /** True for a `className` / `class` JSX attribute. */
51
+ export const isClassAttribute = (node) =>
52
+ node.type === "JSXAttribute" && node.name && (node.name.name === "className" || node.name.name === "class")
52
53
 
53
- /** True when the call is `useEffect(...)` or `useLayoutEffect(...)`, bare or through `React.`. */
54
- export const isEffectCall = (node) => {
55
- const name = calleeName(node.callee)
56
- return name === "useEffect" || name === "useLayoutEffect" || name === "React.useEffect" || name === "React.useLayoutEffect"
54
+ /** Static string carried by a JSX attribute or by a module constant holding a class string. */
55
+ export const staticText = (value) => {
56
+ if (!value) return null
57
+ if (value.type === "Literal" && typeof value.value === "string") return value.value
58
+ if (value.type === "TemplateLiteral" && value.expressions.length === 0) {
59
+ return value.quasis.map((quasi) => quasi.value.cooked).join(" ")
60
+ }
61
+ if (value.type === "JSXExpressionContainer") return staticText(value.expression)
62
+ return null
57
63
  }
58
64
 
59
- /** The callback of an effect call when it is written inline, else null. */
60
- export const effectCallback = (node) => {
61
- const first = node.arguments[0]
62
- return first && isFunction(first) ? first : null
63
- }
65
+ /** True for a function expression or an arrow function. */
66
+ export const isFunction = (node) => node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression"
64
67
 
65
68
  /**
66
69
  * Every node under `root`, depth first, through the visitor keys of the parser in use.
package/lib/config.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The one flat config of a StarCi front end.
3
3
  *
4
- * A repository's `eslint.config.mjs` is a managed file (R05/R16/R17), rendered by `hfs sync` as exactly:
4
+ * A repository's `eslint.config.mjs` is a managed file (R05/R16/R17), rendered by `starci app sync` as exactly:
5
5
  *
6
6
  * import { loadHfs, starciFeConfig } from "@starci/eslint-canon-fe"
7
7
  * export default starciFeConfig({ hfs: loadHfs(import.meta.url) })
package/lib/scope.mjs CHANGED
@@ -63,8 +63,12 @@ export const isConfigModule = (context) => inSlot(context, "fe.modules.config")
63
63
  /** The one place that may call `fetch`: slot `fe.transport.client` (one-app repository) or `fe.package.api.client` (shared package). */
64
64
  export const isApiClient = (context) => inSlot(context, "fe.transport.client", "fe.package.api.client")
65
65
 
66
- /** The file that may declare the one Outcome union: slot `fe.transport.outcome` or `fe.package.api.outcome`. */
67
- export const isOutcomeModule = (context) => inSlot(context, "fe.transport.outcome", "fe.package.api.outcome")
66
+ /** The file that may declare the one Outcome union: the file of the slot the manifest marks `outcomeHome`. */
67
+ export const isOutcomeModule = (context) => {
68
+ const hfs = hfsOf(context)
69
+ const slot = slotOfFile(context)
70
+ return slot !== null && hfs.slot(slot)?.outcomeHome === true
71
+ }
68
72
 
69
73
  /** The basename of a path without directories. */
70
74
  export const baseName = (filename) => normalizePath(filename).split("/").pop() ?? ""