@starci/eslint-canon-fe 8.4.36 → 8.4.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 8.4.38 - 2026-10-24
4
+ - Changed: the bundled runtime copies and canon pins carry the alpha.30 canon (the `zod` pin is `side: both`) and select @starci/hfs 4.7.31 and @starci/cli 1.7.31.
5
+
6
+ ## 8.4.37 - 2026-10-24
7
+ - Changed: the bundled canon pins add the multer 2.5.0 security override and select @starci/hfs 4.7.30 and @starci/cli 1.7.30.
8
+
3
9
  ## 8.4.36 - 2026-10-24
4
10
  - Changed: the bundled canon pins select zod 4.6.7, @starci/hfs 4.7.29 and @starci/cli 1.7.29.
5
11
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starci/eslint-canon-fe",
3
- "version": "8.4.36",
3
+ "version": "8.4.38",
4
4
  "description": "The StarCi front-end lint canon: every rule its laws publish, gathered as one flat-config plugin and attached by one factory.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -29,14 +29,14 @@ pins:
29
29
  source: packages/grammar/package.json
30
30
  why: 'the grammar front ends read: the brand layer sets `--font-sans` and `--font-mono` and the grammar reads them; it carries the Input tel kind, the copyable and read-only Input, the IconButton disclosure props and count badge, ChatBubble, FillViewport, Textarea mentions and Enter-to-send, the leading PrimaryRailLayout rail, the plate-less Image, ListRow, the soft Alert, Progress steps, the floating primary IconButton, FlowGraph, the STATE-8 loading views and the bordered selectable TagGroup chips. The runtime source version wins for a @starci package.'
31
31
  '@starci/eslint-canon-be':
32
- version: 3.4.37
32
+ version: 3.4.39
33
33
  group: starci
34
34
  install: registry
35
35
  side: be
36
36
  source: packages/eslint/be/package.json
37
37
  why: 'the back-end ESLint canon: `loadHfs(import.meta.url)` of be/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the be side, and the project graph is built per side; its bundled runtime copies (slots, canon-pins, failure codes, the architecture machine) put .starcistacks and .sops.yaml at the app root; `starciBeConfig({ hfs: loadHfs(import.meta.url) })` is the typed factory and the BE-CONVENTION laws are its rules.'
38
38
  '@starci/eslint-canon-fe':
39
- version: 8.4.36
39
+ version: 8.4.38
40
40
  group: starci
41
41
  install: registry
42
42
  side: fe
@@ -76,14 +76,14 @@ pins:
76
76
  source: packages/test-world/package.json
77
77
  why: 'the shared e2e library of every back end (R47, R48), a devDependency of every back end: the warm stack behind toxiproxy, the network-edge fakes, the Nest boot, the typed useTestWorld handle, useSandbox for contract specs and the outage lock. Kafka is real own infra (the one digest-pinned apache/kafka KRaft image, a broker listener and proxy per slot, slot-prefixed topics, consumer groups and client ids) and Postgres is schema-per-context (a schema and a login role per context in a shared database). Each worker has its own data slot (<ns>_w<k>) with its own databases, Keycloak realm, leased Redis DB, bucket/collection/topic prefixes, fakes host, toxiproxy proxies and outage lock; a run provisions min(jest workers, workers cap, default 2) slots, and state file protocol 2 pairs exactly with @starci/jest-preset (TEST_WORLD_PAIR_MISMATCH otherwise). A modules world boots real peer apps beside its modules, world.apps.<name>.during(fn) is the outage of a peer app, w.keycloak.clientSecret(client) answers the run-generated secret of a confidential realm client, world.keycloak.events/sessions read the realm user events and live sessions, world.infra.postgresql.connection(name) takes one database connection down while the others serve, and world.resolve and scope.resolve take any Nest token. Every path a declaration names (`stack`, seeds, the realm, Dockerfiles) resolves from the app root (the directory of hfs.json); the globalSetup registers the path aliases as TypeScript resolves them. Its CLI main is exported at ./cli for starci app stack.'
78
78
  '@starci/cli':
79
- version: 1.7.29
79
+ version: 1.7.31
80
80
  group: starci
81
81
  install: registry
82
82
  side: both
83
83
  source: packages/cli/package.json
84
84
  why: 'the only StarCi command package; product repositories install it for starci app, while @starci/hfs remains its pinned transitive implementation.'
85
85
  '@starci/hfs':
86
- version: 4.7.29
86
+ version: 4.7.31
87
87
  group: starci
88
88
  install: registry
89
89
  side: both
@@ -338,8 +338,8 @@ pins:
338
338
  'zod':
339
339
  version: 4.6.7
340
340
  group: framework
341
- side: fe
342
- why: 'the schema library of front-end forms and boundary parsing (fe-stack forms), also an npm override of every app root; 4.6.7 (the registry latest on 2026-10-24) closes GHSA-mrhx-c7xc-m3w5 (high, fixed in 4.6.6), which npm audit --audit-level=high reported against 4.4.3.'
341
+ side: both
342
+ why: 'the schema library of front-end forms (fe-stack forms) and, on the back end, of the boundary parsers (R304 and R305: provider responses, model output and queue payloads, never a transport DTO), also an npm override of every app root; 4.6.7 (the registry latest on 2026-10-24) closes GHSA-mrhx-c7xc-m3w5 (high, fixed in 4.6.6), which npm audit --audit-level=high reported against 4.4.3.'
343
343
  '@reduxjs/toolkit':
344
344
  version: 2.11.2
345
345
  group: framework
@@ -471,6 +471,11 @@ pins:
471
471
  group: framework
472
472
  side: be
473
473
  why: 'an npm override of every app root (and of the examples that serve GraphQL), never a declared dependency: @nestjs/graphql 13.x pulls @graphql-tools/utils <=12.0.0, which npm audit --audit-level=high reports (GHSA-7mx3-vvmw-hjmv, prototype pollution in mergeDeep). 12.1.0 is a fixed release on the same major and the lowest one every declared range accepts (@graphql-tools/schema and /merge, used at runtime, require ^12.1.0; NIVO v0.2.1 6a1d8e80 overrides 12.0.4, below that range), so the advisory closes without the breaking @nestjs/graphql 14. DROP this pin and the override when @nestjs/graphql 14 is adopted (owner ruling graphql-tools-utils-override, 2026-10-18).'
474
+ 'multer':
475
+ version: 2.5.0
476
+ group: framework
477
+ side: be
478
+ why: 'an npm override of every app root and of the examples, never a declared dependency: @nestjs/platform-express 11.2.x (and 12.x) depends on exactly multer 2.4.0, which npm audit --audit-level=high reports (GHSA-q7qp-rjg7-q26m, a denial of service through a pending asynchronous fileFilter after request teardown, published 2026-10-24). 2.5.0 is the fixed release on the same major. DROP this pin and the override when a @nestjs/platform-express release depends on multer 2.5.0 or later.'
474
479
  '@nestjs/apollo':
475
480
  version: 13.4.5
476
481
  group: framework
@@ -75,7 +75,7 @@ concerns:
75
75
  home: [fe.hooks.socketio]
76
76
  pattern: FE-REALTIME
77
77
  forbids: [graphql-ws, subscriptions-transport-ws, '@supabase/realtime-js', pusher-js, ably]
78
- why: one socket.io-client singleton per namespace and one side-effect component that dispatches to redux and SWR; lite runs the gateway in the be api process.
78
+ why: one socket.io-client singleton per namespace and one side-effect component that dispatches to redux and SWR; lite runs the gateway in the be api process. A one-way channel served as server-sent events needs no package, only a fetch reader in modules/api/sse and a use<Channel>Stream hook (FE-REALTIME).
79
79
  - id: auth
80
80
  tier: core
81
81
  packages: []
@@ -7,7 +7,7 @@
7
7
  # catalog READ this file through scripts/hfs/slots.mjs; none of them hardcodes a path.
8
8
  # Shape: modules/schemas/hfs-slots.schema.yaml. The repository side is hfs.json: modules/schemas/hfs-repo.schema.yaml.
9
9
  schema: starci/hfs-slots@3
10
- version: 3.5.0
10
+ version: 3.6.0
11
11
 
12
12
  versioning:
13
13
  # MAJOR.MINOR.PATCH of this manifest. A repository pins only the MAJOR in hfs.json ("hfs": 3).
@@ -41,6 +41,8 @@ versioning:
41
41
  # 3.4.0 is a minor (alpha.25): it ADDS the opt-in be.contract.mcp-tools slot, the committed MCP tool-catalog snapshot of an app.
42
42
  # 3.5.0 is a minor (alpha.27): it ADDS the opt-in app.browser-config slot, the managed browser/tsconfig.json that type-checks the browser
43
43
  # journey (enabled with app.browser by `browser: true`).
44
+ # 3.6.0 is a minor (alpha.30): it ADDS the opt-in be.feature.realtime.sse slot, the server-sent-events door of the realtime kind (owner decision
45
+ # 2026-10-24: lite may serve SSE, and SSE is the default door for one-way push), and lets fe.modules.api hold the SSE stream readers (sse/).
44
46
 
45
47
  # Field meaning, per slot.
46
48
  # id stable name, never changes meaning inside a major.
@@ -217,7 +219,7 @@ ruleParams:
217
219
  kafkajs: [platform/event-bus] # pattern event-bus: Kafka is the only event transport
218
220
  "@nestjs/schedule": [] # no @Cron / @Interval / @Timeout anywhere: a schedule is a BullMQ job scheduler of a queue
219
221
  eventemitter2: []
220
- setInterval: [platform/retry, platform/event-bus, platform/queue, platform/http]
222
+ setInterval: [platform/retry, platform/event-bus, platform/queue, platform/http, platform/realtime]
221
223
  setTimeout: [platform/retry, platform/event-bus, platform/queue, platform/http]
222
224
  "node:timers": [platform/retry, platform/event-bus, platform/queue, platform/http]
223
225
  "node:timers/promises": [platform/retry, platform/event-bus, platform/queue, platform/http]
@@ -279,7 +281,7 @@ ruleParams:
279
281
  # R89 BE_SOURCE_FORM: the closed role-suffix vocabulary of a source file name (<kebab-name>.<suffix>.ts; BE-CONVENTION 1.15)
280
282
  # and the suffixes that are never a role. index.ts, main.ts, app.module.ts and the migrations of be.persistence are the
281
283
  # only names outside it.
282
- suffixes: [module, module-definition, decorators, options, config, connection, service, command, query, handler, templates, compensation, saga-state, saga-step, saga, contracts, port, resolver, controller, gateway, webhook, subscription, consumer, cli, event, queue, projection, projection-entity, processor, step, input, type, args, request, response, mapper, policy, entity, sql, rows, error, messages, log-events, cache-keys, guard, interceptor, filter, client, spec, integration-spec, e2e-spec, contract-spec, global-setup, builder]
284
+ suffixes: [module, module-definition, decorators, options, config, connection, service, schema, command, query, handler, templates, compensation, saga-state, saga-step, saga, contracts, port, resolver, controller, gateway, webhook, subscription, consumer, cli, event, queue, projection, projection-entity, processor, step, input, type, args, request, response, mapper, policy, entity, sql, rows, error, messages, log-events, cache-keys, guard, interceptor, filter, client, spec, integration-spec, e2e-spec, contract-spec, global-setup, builder]
283
285
  # R47 BE_CONTRACT_UNGUARDED: the world's shape vocabulary. `helper` names the function a contract spec imports to erase the values of a JSON
284
286
  # payload (`shapeOf`); a contract spec proves a fake's fixtures by asserting helper(real) equals helper(fixture).
285
287
  contractShape: {helper: shapeOf}
@@ -298,7 +300,7 @@ ruleParams:
298
300
  # options, config, command, query, contracts, port, event, queue, entity, sql, rows, messages, log-events, error, decorators, cache-keys,
299
301
  # projection-entity, connection, and every file with no role: index.ts, main.ts, migrations) is declaration or composition and is never measured.
300
302
  # A unit spec beside such a file (`<name>.<role>.spec.ts`) is legal but not required: the file owes 100, not a spec of its own.
301
- logicRoles: [service, policy, projection, guard, mapper, client, filter, interceptor, processor, step, saga, saga-step, compensation, consumer, webhook]
303
+ logicRoles: [service, schema, policy, projection, guard, mapper, client, filter, interceptor, processor, step, saga, saga-step, compensation, consumer, webhook]
302
304
  # R203 BE_FEATURE_THIN. THE roles a feature-tier file is judged thin by: a file `<name>.<role>.ts` of one of these roles inside a slot of tier
303
305
  # feature maps parameters and makes ONE delegating call into modules/ (a service, the command or query bus, the event bus, a queue producer): no
304
306
  # branch, loop, ternary, try or throw, no computation; orchestration (step order, compensation, claim, fencing, retry) lives in platform/*.
@@ -1434,9 +1436,9 @@ slots:
1434
1436
  since: 1.0.0
1435
1437
  - id: be.feature.realtime
1436
1438
  profiles: [be]
1437
- editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite runs socket.io in the api process, one instance without Redis; full may scale its gateway."
1439
+ editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4) and ruling:R212/case-5 (owner decision 2026-10-24): lite serves server-sent events (the default door for one-way push) or runs socket.io in the api process, one instance without Redis; full may scale its gateway."
1438
1440
  path: "src/features/realtime/<channel>/"
1439
- presence: opt-in # a push channel: reads and pushes only (a socket gateway or a GraphQL subscription), no domain write
1441
+ presence: opt-in # a push channel: reads and pushes only (an SSE controller, a socket gateway or a GraphQL subscription), no domain write
1440
1442
  pattern: realtime # declaring the pattern (hfs.json sides.be.patterns) enables the kind (knowledge/patterns/be/realtime.yaml)
1441
1443
  trigger: realtime
1442
1444
  tracked: tracked
@@ -1447,9 +1449,9 @@ slots:
1447
1449
  tests: none
1448
1450
  lite:
1449
1451
  why: >-
1450
- a push channel on the same socket.io gateway as full (owner ruling 2026-10-13): lite runs the gateway inside the be api
1451
- process, one instance and no Redis adapter; the front end listens through hooks/socketio
1452
- (knowledge/patterns/be/realtime.yaml, liteAdapter)
1452
+ a push channel: the server-sent-events door (be.feature.realtime.sse, the default for one-way push, owner decision 2026-10-24)
1453
+ or the same socket.io gateway as full (owner ruling 2026-10-13), both run inside the be api process, one instance and no
1454
+ Redis adapter; the front end listens through a use<Channel>Stream hook over modules/api/sse, or hooks/socketio (knowledge/patterns/be/realtime.yaml, liteAdapter)
1453
1455
  coverage: none
1454
1456
  rules: [BE_KIND_ISOLATION, BE_FEATURE_IMPORTS_FEATURE, BE_FEATURE_NOT_COMPOSED]
1455
1457
  since: 1.0.0
@@ -1457,14 +1459,14 @@ slots:
1457
1459
  - {path: packages/hfs/src/hfs-stacks-swarm.spec.mjs, edition: lite}
1458
1460
  - id: be.feature.realtime.graphql
1459
1461
  profiles: [be]
1460
- editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite uses only the in-process socket.io gateway; the GraphQL subscription door remains full-only."
1462
+ editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite pushes through the SSE door or the in-process socket.io gateway; the GraphQL subscription door remains full-only."
1461
1463
  path: "src/features/realtime/<channel>/transport/graphql/"
1462
1464
  presence: opt-in # a GraphQL subscription over graphql-ws
1463
- litePresence: forbidden # ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite pushes through the api's in-process socket.io gateway
1465
+ litePresence: forbidden # ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite pushes through the api's SSE door or its in-process socket.io gateway
1464
1466
  pattern: realtime
1465
1467
  tracked: tracked
1466
1468
  tier: feature
1467
- goesTo: "src/features/realtime/<channel>/transport/websocket/ (be.feature.realtime.websocket): both editions push through the socket.io gateway; lite runs it in the api process"
1469
+ goesTo: "src/features/realtime/<channel>/transport/sse/ (be.feature.realtime.sse, the default for one-way push) or transport/websocket/ (be.feature.realtime.websocket): lite runs either door in the api process"
1468
1470
  requires: ["<channel>-graphql.module.ts", "<channel>.subscription.ts", "<channel>.subscription.spec.ts"]
1469
1471
  allows: ["<channel>-graphql.module.ts", "<channel>-graphql.module-definition.ts", "<channel>-graphql.options.ts", "<channel>.subscription.ts", "<channel>.subscription.spec.ts", dto/]
1470
1472
  tests: unit-beside # the door's own spec beside the door (a subscription has no service of its own)
@@ -1474,14 +1476,14 @@ slots:
1474
1476
  since: 1.0.0
1475
1477
  - id: be.feature.realtime.dto
1476
1478
  profiles: [be]
1477
- editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): GraphQL subscription DTOs exist only with the full subscription door; lite uses socket.io contracts."
1479
+ editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): GraphQL subscription DTOs exist only with the full subscription door; lite uses SSE or socket.io contracts."
1478
1480
  path: "src/features/realtime/<channel>/transport/graphql/dto/"
1479
1481
  presence: opt-in
1480
1482
  litePresence: forbidden # the dto of the graphql door, which lite does not serve
1481
1483
  pattern: realtime
1482
1484
  tracked: tracked
1483
1485
  tier: feature
1484
- goesTo: "src/features/realtime/<channel>/transport/websocket/ (be.feature.realtime.websocket): both editions push through the socket.io gateway; lite runs it in the api process"
1486
+ goesTo: "src/features/realtime/<channel>/transport/sse/ (be.feature.realtime.sse, the default for one-way push) or transport/websocket/ (be.feature.realtime.websocket): lite runs either door in the api process"
1485
1487
  allows: ["<name>.{input,type,args}.ts"]
1486
1488
  tests: none
1487
1489
  coverage: none
@@ -1502,6 +1504,22 @@ slots:
1502
1504
  composedBy: [api]
1503
1505
  rules: [BE_REALTIME_SHAPE, BE_REALTIME_WRITES, BE_REALTIME_TOPIC_SCOPE, BE_DEFAULT_DENY]
1504
1506
  since: 1.0.0
1507
+ - id: be.feature.realtime.sse
1508
+ since: 3.6.0
1509
+ profiles: [be]
1510
+ path: "src/features/realtime/<channel>/transport/sse/"
1511
+ presence: opt-in # a server-sent-events door: one @Sse method, the default door for one-way push (owner decision 2026-10-24), served in both editions
1512
+ pattern: realtime
1513
+ tracked: tracked
1514
+ tier: feature
1515
+ requires: ["<channel>-sse.module.ts", "<channel>.controller.ts", "<channel>.controller.spec.ts"]
1516
+ allows: ["<channel>-sse.module.ts", "<channel>-sse.module-definition.ts", "<channel>-sse.options.ts", "<channel>.controller.ts", "<channel>.controller.spec.ts"]
1517
+ tests: unit-beside # the door's own spec beside the door (an SSE controller has no service of its own)
1518
+ coverage: none
1519
+ composedBy: [api]
1520
+ rules: [BE_REALTIME_SHAPE, BE_REALTIME_WRITES, BE_REALTIME_TOPIC_SCOPE, BE_DEFAULT_DENY, BE_REST_DOOR_EVIDENCE]
1521
+ exemplifiedBy:
1522
+ - {path: packages/eslint/be/webhooks-realtime-coverage.spec.mjs, editions: [lite, full]}
1505
1523
  - id: be.cli
1506
1524
  since: 3.1.0
1507
1525
  profiles: [be]
@@ -1552,7 +1570,7 @@ slots:
1552
1570
  requires: [index.ts]
1553
1571
  # <name>.templates.ts holds the capability's static templates, copy and presets as TS constants (owner ruling 2026-10-13);
1554
1572
  # user-editable ones are seed rows, large or user media live in object storage.
1555
- allows: ["<capability>.module.ts", "<capability>.module-definition.ts", "<capability>.config.ts", "<capability>.options.ts", errors/, messages/, persistence/, policies/, "*.service.ts", "*.service.spec.ts", "*.contracts.ts", "*.templates.ts"]
1573
+ allows: ["<capability>.module.ts", "<capability>.module-definition.ts", "<capability>.config.ts", "<capability>.options.ts", errors/, messages/, persistence/, policies/, "*.service.ts", "*.service.spec.ts", "*.contracts.ts", "*.templates.ts", "*.schema.ts", "*.schema.spec.ts"]
1556
1574
  forbids: [testing/, exceptions/, utils/, helpers/, shared/]
1557
1575
  tests: unit-beside # <name>.service.spec.ts beside each <name>.service.ts, nothing else
1558
1576
  coverage: required
@@ -2254,15 +2272,17 @@ slots:
2254
2272
  # The browser's calls to the back end (owner ruling 2026-10-13, fe-stack graphql/rest/codegen, FE-API-GRAPHQL): the auth and
2255
2273
  # no-auth Apollo clients with their link chain (graphql/clients), one file per operation (graphql/queries/query-<x>.ts,
2256
2274
  # graphql/mutations/mutation-<x>.ts with its gql document map and typed request/response in types/), the generated wire types
2257
- # (graphql/__generated__) and the axios calls (rest/<area>/*.ts) for uploads, presigned URLs and auth endpoints. A repository has
2275
+ # (graphql/__generated__), the axios calls (rest/<area>/*.ts) for uploads, presigned URLs and auth endpoints and the fetch readers of
2276
+ # server-sent-events streams (sse/<area>/*.ts, one file per stream, the bearer token from modules/auth). A repository has
2258
2277
  # exactly ONE client folder and ONE GraphQLResponse union: the app's (fe.transport.client/outcome) when one app calls the back end,
2259
2278
  # or the api package's (fe.package.api.client/outcome) when several do (FE_TRANSPORT_OWNER, machine).
2260
2279
  requires: [index.ts]
2261
2280
  allows: [index.ts, graphql/index.ts, graphql/types.ts, "graphql/clients/**", "graphql/queries/query-*.ts", "graphql/queries/types/*.ts",
2262
- "graphql/mutations/mutation-*.ts", "graphql/mutations/types/*.ts", "graphql/__generated__/**", "rest/index.ts", "rest/<area>/*.ts"]
2281
+ "graphql/mutations/mutation-*.ts", "graphql/mutations/types/*.ts", "graphql/__generated__/**", "rest/index.ts", "rest/<area>/*.ts",
2282
+ "sse/index.ts", "sse/<area>/*.ts"]
2263
2283
  roles: {entry: index.ts, query: "query-*.ts", mutation: "mutation-*.ts"}
2264
2284
  # A hook imports its one operation file directly (one file per operation, FE-API-GRAPHQL), never through a barrel of every operation.
2265
- entries: ["graphql/queries/query-*.ts", "graphql/queries/types/*.ts", "graphql/mutations/mutation-*.ts", "graphql/mutations/types/*.ts"]
2285
+ entries: ["graphql/queries/query-*.ts", "graphql/queries/types/*.ts", "graphql/mutations/mutation-*.ts", "graphql/mutations/types/*.ts", "sse/<area>/*.ts"]
2266
2286
  tests: none
2267
2287
  rules: [FE_TRANSPORT_OWNER, FE_HTTP_STATUS_COLLAPSE, FE_WIRE_GENERATED]
2268
2288
  - id: fe.transport.generated
@@ -3,6 +3,7 @@
3
3
  * HFS_UNUSED_EXPORT an export of an owner's public entry that no production file outside the owner imports is dead.
4
4
  * HFS_UNUSED_FILE a production source file that no root reaches is dead. Roots: the files an app slot requires
5
5
  * (main.ts, app.module.ts), every route file (slot tier `route`), the public entry of every package,
6
+ * and an api app operation table `operations.ts` (the contract emit reads it by path),
6
7
  * and a source file a framework config of the app names by a relative string literal
7
8
  * (next.config.ts pointing next-intl at its request config). Specs and tests are not in the graph,
8
9
  * so a file only a spec imports is dead, on purpose. Type-only imports reach a file.
@@ -24,7 +25,7 @@
24
25
  * Specs and tests are not in the graph, so an export used only by a spec is dead, on purpose, with these exceptions (unit test
25
26
  * standard): the unit spec of every unit role (ruleParams.be.unitRoles: `<name>.service.spec.ts`, `<name>.cli.spec.ts`) and of every logic role
26
27
  * (ruleParams.be.logicRoles: `<name>.policy.spec.ts`, `<name>.client.spec.ts`, ...), a
27
- * unit spec of a webhook, gateway or subscription door (`.webhook.spec.ts`, `.gateway.spec.ts`, `.subscription.spec.ts`: a door has no service of its own), a
28
+ * unit spec of a webhook, gateway, subscription or SSE controller door (`.webhook.spec.ts`, `.gateway.spec.ts`, `.subscription.spec.ts`, `.controller.spec.ts`: a door has no service of its own), a
28
29
  * `*.builder.ts` under src/tests/fixtures/builders, and a file of the test world (slots be.tests.world and be.tests.world.kit, which
29
30
  * open and migrate the real databases of the e2e run) read from disk or staged in the git index count as consumers, because a spec can only provide an
30
31
  * Inject*() token or a param type the entry exports, and the world wires the same capabilities the apps do. An integration
@@ -47,6 +48,8 @@ export const DEAD_EXPORT_RULE_IDS = ['HFS_UNUSED_EXPORT', 'HFS_UNUSED_FILE'];
47
48
  const PACKAGE_ENTRIES = ['src/index.ts', 'src/index.tsx', 'index.ts', 'index.tsx'];
48
49
  const OWNER_ENTRIES = ['index.ts', 'index.tsx'];
49
50
  const CHAIN_DEPTH = 6;
51
+ /** The file of an api app that the contract emit reads by path for the typed operation table (BE-OPERATIONS-1): no import reaches it, the emit does. */
52
+ const OPERATION_TABLE_FILE = "operations.ts";
50
53
  /** Tiers whose files are not production source that an app serves: judged by no dead-file rule. */
51
54
  const UNJUDGED_TIERS = new Set(['none', 'e2e', 'fixtures']);
52
55
  const CONFIG_EXTENSIONS = ['', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '/index.ts', '/index.tsx', '/index.js'];
@@ -144,7 +147,7 @@ function rootFiles(graph, config) {
144
147
  if (!node.owner || node.tier !== 'app') continue;
145
148
  const slot = graph.resolver.slot(node.owner.slot);
146
149
  const inside = rel.slice(withoutSlash(node.owner.root).length + 1);
147
- if ((slot?.requires ?? []).includes(inside)) roots.add(rel);
150
+ if ((slot?.requires ?? []).includes(inside) || inside === OPERATION_TABLE_FILE) roots.add(rel);
148
151
  }
149
152
  for (const [key, owner] of graph.ownerRoots) {
150
153
  if (owner.tier !== 'package') continue;
@@ -177,8 +180,8 @@ function deadFiles(graph, config) {
177
180
  return { violations, judged: judgedFiles.length, roots: roots.size };
178
181
  }
179
182
 
180
- /** The unit spec of a webhook, gateway or subscription door: a door has no service of its own, so its spec is the only unit spec of its kind. */
181
- const DOOR_SPEC = /^src\/features\/(?:webhooks|realtime)\/.+\.(?:webhook|gateway|subscription)\.spec\.ts$/u;
183
+ /** The unit spec of a webhook, gateway, subscription or SSE controller door: a door has no service of its own, so its spec is the only unit spec of its kind. */
184
+ const DOOR_SPEC = /^src\/features\/(?:webhooks|realtime)\/.+\.(?:webhook|gateway|subscription|controller)\.spec\.ts$/u;
182
185
  const BUILDER_CONSUMER = /^src\/tests\/fixtures\/builders\/.+\.builder\.ts$/u;
183
186
  /** The test world slots (knowledge/hfs/slots.yaml): their files wire the real capabilities of the e2e run. */
184
187
  const WORLD_SLOTS = new Set(['be.tests.world', 'be.tests.world.kit']);