@starci/eslint-canon-be 3.2.19 → 3.3.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.
- package/CHANGELOG.md +11 -0
- package/README.md +46 -4
- package/app-composition.mjs +176 -0
- package/config-owner.mjs +34 -19
- package/feature-thin.mjs +59 -17
- package/index.mjs +2 -0
- package/lib/app-roles.mjs +48 -0
- package/lib/webhook-ack.mjs +87 -0
- package/lib/webhook-handoff.mjs +138 -0
- package/lib/webhook-verifiers.mjs +147 -0
- package/module-layering.mjs +2 -2
- package/module-shape.mjs +6 -5
- package/package.json +1 -1
- package/runtime/knowledge/hfs/canon-pins.yaml +4 -4
- package/runtime/knowledge/hfs/rules.yaml +41 -13
- package/runtime/knowledge/hfs/slots.yaml +99 -43
- package/runtime/knowledge/patterns/fe/folder.yaml +3 -0
- package/runtime/scripts/api/git/cat-file.mjs +5 -0
- package/runtime/scripts/api/git/diff.mjs +5 -0
- package/runtime/scripts/hfs/architecture/backend-app-files.mjs +110 -0
- package/runtime/scripts/hfs/architecture/backend.mjs +20 -3
- package/runtime/scripts/hfs/architecture/background-unowned.mjs +157 -41
- package/runtime/scripts/hfs/architecture/canon-clones.mjs +240 -0
- package/runtime/scripts/hfs/architecture/canon-jsx-clones.mjs +143 -0
- package/runtime/scripts/hfs/architecture/clones.mjs +17 -8
- package/runtime/scripts/hfs/architecture/connection-map.mjs +14 -12
- package/runtime/scripts/hfs/architecture/context-map.mjs +25 -14
- package/runtime/scripts/hfs/architecture/context-owner.mjs +1 -2
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +14 -9
- package/runtime/scripts/hfs/architecture/frontend-block-render.mjs +104 -0
- package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +30 -11
- package/runtime/scripts/hfs/architecture/i18n-keys.mjs +16 -3
- package/runtime/scripts/hfs/architecture/machine-ast.mjs +17 -0
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +21 -12
- package/runtime/scripts/hfs/architecture/register-once.mjs +13 -5
- package/runtime/scripts/hfs/architecture/required-files.mjs +2 -1
- package/runtime/scripts/hfs/architecture/staged-consumers.mjs +37 -0
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +3 -6
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +1 -6
- package/runtime/scripts/hfs/architecture/tiers.mjs +8 -1
- package/runtime/scripts/hfs/architecture/typescript.mjs +1 -8
- package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +1 -11
- package/runtime/scripts/hfs/manifest-shape.mjs +27 -2
- package/runtime/scripts/hfs/path-findings.mjs +32 -2
- package/runtime/scripts/hfs/rules/secrets.mjs +13 -0
- package/runtime/scripts/hfs/slot-imports.mjs +3 -1
- package/runtime/scripts/hfs/slot-required.mjs +13 -0
- package/runtime/scripts/hfs/slot-semantic-problems.mjs +2 -1
- package/runtime/scripts/lib/ts-analysis.mjs +20 -0
- package/webhooks.mjs +141 -69
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
# modules/kernel/allowlist.yaml, held by RT_ALLOWLIST_SPRAWL. Codes are quoted: the failure-code
|
|
18
18
|
# scanner reads this file as an emitter of every code it names.
|
|
19
19
|
schema: starci/hfs-rules@3
|
|
20
|
-
version: 3.
|
|
20
|
+
version: 3.1.0
|
|
21
21
|
gates:
|
|
22
22
|
pre-commit: the managed .husky/pre-commit at the app root - starci app hygiene (staged .starciwork and .starcistacks paths, secrets guard), typecheck, ESLint per side and stylelint on the staged files, prettier --check, the unit specs the staged files touch
|
|
23
23
|
pre-push: typecheck, lint (starci app lint), format:check and the unit specs affected since origin/main; runs every pre-commit rule again
|
|
@@ -112,12 +112,16 @@ rules:
|
|
|
112
112
|
- {kind: work-validate, id: identity-custody-role, at: scripts/work/validate/check-example-work.mjs}
|
|
113
113
|
- id: R10
|
|
114
114
|
code: "HFS_STACKS_SHAPE"
|
|
115
|
-
law: "`.starcistacks` has the standard shape and the host Sonar owner."
|
|
115
|
+
law: "`.starcistacks` has the edition-aware standard shape and the host Sonar owner; Swarm stacks have digest-pinned images, healthchecks, rollout, rollback and restart policies, versioned external secrets, ingress-only published ports and declared schedulers for replicated jobs. Recurring work is app-owned; host crontab installation is forbidden. Lite declares Supabase as managed backing and runs no duplicate backing-service component."
|
|
116
116
|
kinds: [check, codemod]
|
|
117
117
|
gates: [settle, land, ci]
|
|
118
118
|
failureCodes: ["HFS_STACKS_SHAPE"]
|
|
119
119
|
enforcers:
|
|
120
120
|
- {kind: hfs, id: stacks-shape, at: scripts/hfs/rules/stacks.mjs}
|
|
121
|
+
- {kind: hfs, id: stacks-runtime, at: scripts/hfs/rules/stacks-runtime.mjs}
|
|
122
|
+
- {kind: hfs, id: stacks-evidence-runtime, at: scripts/gates/stacks-gate.mjs}
|
|
123
|
+
- {kind: hfs, id: starcistacks-runtime, at: scripts/gates/starcistacks-runtime.mjs}
|
|
124
|
+
- {kind: hfs, id: starcistacks-runtime-gate, at: scripts/gates/starcistacks.mjs}
|
|
121
125
|
- id: R11
|
|
122
126
|
code: "HFS_SONAR_CONFIG"
|
|
123
127
|
law: "Sonar config is generated, with no host URL, importing the be lcov with the logic of be/src/modules (the coverage scope derived from the slot manifest) as the only coverage scope."
|
|
@@ -258,7 +262,7 @@ rules:
|
|
|
258
262
|
- {kind: eslint-fe, id: dead-exports}
|
|
259
263
|
- id: R26
|
|
260
264
|
code: "BE_TIER_DIRECTION"
|
|
261
|
-
law: "The tier direction matrix; a shared package never imports an app."
|
|
265
|
+
law: "The tier direction matrix; a shared package never imports an app. Front-end page and layout owners may import overlay entries; every other cross-owner feature-to-feature edge is forbidden, including layout-to-page and overlay-to-overlay. Components never import overlays."
|
|
262
266
|
kinds: [lint, design]
|
|
263
267
|
gates: [pre-push, settle, land, ci]
|
|
264
268
|
failureCodes: ["BE_TIER_DIRECTION", "FE_TIER_DIRECTION", "ARCH_PACKAGE_IMPORTS_APP"]
|
|
@@ -297,7 +301,7 @@ rules:
|
|
|
297
301
|
- {kind: eslint-be, id: application-never-imports-transport}
|
|
298
302
|
- id: R30
|
|
299
303
|
code: "BE_PUBLIC_SURFACE"
|
|
300
|
-
law: "Every owner has one `index.ts`; cross-owner imports use it, same-owner imports are relative and never go through it; it holds only named `export { }` lines, no `export *`, no alias re-export (`export { X as Y }`, `export const Y = X`, `export type Y = X`, `export interface Y extends X {}`: one declaration has one name),
|
|
304
|
+
law: "Every owner has one `index.ts`; cross-owner imports use it, same-owner imports are relative and never go through it; it holds only named `export { }` lines, no `export *`, no alias re-export (`export { X as Y }`, `export const Y = X`, `export type Y = X`, `export interface Y extends X {}`: one declaration has one name), within the owner slot's `indexExports` budget (60 for domain and feature owners, 80 for `be.integrations` provider entries); cross-package imports use the package name and a declared export."
|
|
301
305
|
kinds: [lint, codemod]
|
|
302
306
|
gates: [pre-push, settle, land, ci]
|
|
303
307
|
failureCodes: ["BE_PUBLIC_SURFACE", "ARCH_OWNER_EXPORT_BYPASS", "ARCH_OWNER_EXPORT_STAR", "HFS_ALIAS_REEXPORT", "ARCH_PACKAGE_EXPORT_BYPASS"]
|
|
@@ -423,7 +427,7 @@ rules:
|
|
|
423
427
|
- {kind: eslint-be, id: auth-door-strict-rate-tier}
|
|
424
428
|
- id: R43
|
|
425
429
|
code: "BE_CONFIG_OWNER"
|
|
426
|
-
law: "Only `platform/config`
|
|
430
|
+
law: "Only the `EnvSource` declaration of `platform/config`, app `main.ts`, the matching `<app>.env.ts` and the test world read `process.env`; app environment reads through `EnvSource` and capability config helpers live in `main.ts` or the matching `<app>.env.ts`; options files and composition groups receive typed options."
|
|
427
431
|
kinds: [lint, design]
|
|
428
432
|
gates: [pre-commit, pre-push, settle, land, ci]
|
|
429
433
|
failureCodes: ["BE_CONFIG_OWNER"]
|
|
@@ -804,7 +808,7 @@ rules:
|
|
|
804
808
|
- {kind: eslint-be, id: no-ambient-clock}
|
|
805
809
|
- id: R80
|
|
806
810
|
code: "BE_INBOX_DEDUPE_MISSING"
|
|
807
|
-
law: "Every service method that takes a delivery (a public method of a `*.service.ts` whose first parameter has an `eventId`) makes `claim(source, eventId)` on the `Inbox` port its first awaited expression and returns early when the claim answers `false`; consumers
|
|
811
|
+
law: "Every service method that takes a delivery (a public method of a `*.service.ts` whose first parameter has an `eventId`) makes `claim(source, eventId)` on the `Inbox` port its first awaited expression and returns early when the claim answers `false`; consumers stay thin doors (R88) and dispatch one command to the handler whose service claims. SignedWebhook controllers verify first and call one domain recording intake directly (R182, R183); a reactor or job dispatches the processing command after the inbox receipt commits."
|
|
808
812
|
kinds: [lint, design]
|
|
809
813
|
gates: [pre-commit, pre-push, settle, land, ci]
|
|
810
814
|
failureCodes: ["BE_INBOX_DEDUPE_MISSING"]
|
|
@@ -961,7 +965,7 @@ rules:
|
|
|
961
965
|
- {kind: eslint-fe, id: frontend-source-layout}
|
|
962
966
|
- id: R92
|
|
963
967
|
code: "FE_COMPONENT_API"
|
|
964
|
-
law: "A component exposes the canon surface: typed named props and slots,
|
|
968
|
+
law: "A component exposes the canon surface: typed named props and slots, an exact sibling XBase presentational twin for every connected block, status through the slot view, no className or CSS doors, no resting twin or placeholder prop, class names in the colocated file; a pure component reaches no hook, router, locale or API, but may render a list of blocks supplied through render props without importing blocks or owning their data."
|
|
965
969
|
kinds: [lint, design]
|
|
966
970
|
gates: [pre-commit, pre-push, settle, land, ci]
|
|
967
971
|
failureCodes: ["FE_COMPONENT_API", "FE_COMPONENT_WORLD_OWNERSHIP", "FE_WORLD_OWNER_RENDER_BOUNDARY", "FE_CONNECTED_BLOCK_RENDER_PAIR", "FE_PURE_REACHES_DATA", "FE_PURE_WORLD_HOOK", "FE_PURE_WORLD_IMPORT"]
|
|
@@ -1606,7 +1610,7 @@ rules:
|
|
|
1606
1610
|
- {kind: eslint-be, id: sql-returning}
|
|
1607
1611
|
- id: R182
|
|
1608
1612
|
code: "BE_WEBHOOK_SHAPE"
|
|
1609
|
-
law: "A provider webhook door (`src/features/webhooks/<provider>/transport/http/<provider>.webhook.ts`) is one `@Controller` with one `@Post` handler declared `@Public({ reason: PublicReason.SignedWebhook })
|
|
1613
|
+
law: "A provider webhook door (`src/features/webhooks/<provider>/transport/http/<provider>.webhook.ts`) is one `@Controller` with exactly one `@Post` handler and optionally one `@Get` subscription challenge handler, each declared `@Public({ reason: PublicReason.SignedWebhook })`. It injects only the `WebhookSignatureService` of `platform/http-security` and services of `modules/domain`, holds no branch, loop or `try`, and its POST calls proof then exactly one domain intake that durably records the inbox receipt and outbox event in its transaction. A combined domain proof and intake call is allowed. POST returns nothing or exactly the own-feature exported PROVIDER_ACK, a literal object or string declared with `as const` or a readonly literal type; computed values, inline return objects and cross-feature constants are refused. GET starts with a declared domain verifier call and returns exactly that result, with no intake. The door and its HTTP module import or inject no CommandBus, QueryBus of @nestjs/cqrs or feature command handler; the resolved called intake dispatches no command/query bus or feature handler. A reactor or job consumes the durable receipt and dispatches after commit; unresolved helpers and dynamic receivers require semantic review."
|
|
1610
1614
|
kinds: [lint]
|
|
1611
1615
|
gates: [pre-commit, pre-push, settle, land, ci]
|
|
1612
1616
|
failureCodes: ["BE_WEBHOOK_SHAPE"]
|
|
@@ -1614,7 +1618,7 @@ rules:
|
|
|
1614
1618
|
- {kind: eslint-be, id: webhook-shape}
|
|
1615
1619
|
- id: R183
|
|
1616
1620
|
code: "BE_WEBHOOK_UNVERIFIED"
|
|
1617
|
-
law: "The first statement of a webhook
|
|
1621
|
+
law: "The first statement of a webhook POST is an unconditional verification call to `WebhookSignatureService.verify` or a domain verifier, awaited when async, with no data access or other call before proof. A domain verifier is a method on a domain service whose declared class name ends in `Verifier` or `verify`, or a domain service method declared by symbol in the adjacent HTTP module's exported `webhookVerifiers` array. A domain method may verify with per-connection secrets before intake in the same call; invalid proof throws before intake. A fixed PROVIDER_ACK never replaces proof. The optional GET challenge handler starts with a declared domain verifier call (directly returned or bound to a const and then returned), awaited when async, and echoes only its exact result."
|
|
1618
1622
|
kinds: [lint]
|
|
1619
1623
|
gates: [pre-commit, pre-push, settle, land, ci]
|
|
1620
1624
|
failureCodes: ["BE_WEBHOOK_UNVERIFIED"]
|
|
@@ -1776,7 +1780,7 @@ rules:
|
|
|
1776
1780
|
- {kind: runtime, id: ci-upload, at: scripts/hfs/runtime-rules/ci-upload.mjs}
|
|
1777
1781
|
- id: R203
|
|
1778
1782
|
code: "BE_FEATURE_THIN"
|
|
1779
|
-
law: "A feature file (a role of ruleParams.be.thinRoles inside a slot of tier feature: 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/ (a service, the command or query bus, the event bus, a queue producer): no branch, loop, ternary, try or throw and no computation. Business logic lives in modules/**, where every logic file owes 100 per file; orchestration (step order, compensation, claim, fencing, retry) lives in platform/*; the feature layer is proven by integration and e2e. The gate calls a kind requires are not delegation: a webhook's
|
|
1783
|
+
law: "A feature file (a role of ruleParams.be.thinRoles inside a slot of tier feature: 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/ (a service, the command or query bus, the event bus, a queue producer): no branch, loop, ternary, try or throw and no computation. Business logic lives in modules/**, where every logic file owes 100 per file; orchestration (step order, compensation, claim, fencing, retry) lives in platform/*; the feature layer is proven by integration and e2e. The gate calls a kind requires are not delegation: a webhook's first verification call of R183 when followed by one separate intake, and a job step's `JobClaims` fence (`advance`, `runKey`); a combined domain verification and intake call counts as the delegation. A logger call, a pure mapper function and a modules function in argument position are free too; a mapper may map a collection item by item (`map`) and write a date as text (`toISOString`), and a cli group command shows its help. A webhook POST may return the declared literal PROVIDER_ACK and its optional GET may echo only its first domain verifier result; neither is an extra delegation. A webhook door and its HTTP module import or inject no CommandBus, QueryBus of @nestjs/cqrs or feature command handler: the inbox recording intake is the handoff, and a reactor or job dispatches after commit."
|
|
1780
1784
|
kinds: [lint]
|
|
1781
1785
|
gates: [pre-commit, pre-push, settle, land, ci]
|
|
1782
1786
|
failureCodes: ["BE_FEATURE_THIN"]
|
|
@@ -1846,12 +1850,14 @@ rules:
|
|
|
1846
1850
|
- {kind: runtime, id: slot-id-shape, at: scripts/checks/check-slot-id-shape.mjs}
|
|
1847
1851
|
- id: R212
|
|
1848
1852
|
code: "HFS_EDITION_INVALID"
|
|
1849
|
-
law: "hfs.json names an edition the manifest knows (full, or lite; absent means full). Lite
|
|
1853
|
+
law: "hfs.json names an edition the manifest knows (full, or lite; absent means full). Lite follows the 2026-10-13 ruling: the same monorepo, code structure, slots, patterns and back-end unit specs, with only backing infrastructure swapped for managed Supabase (Postgres, Auth, Storage, Supabase Queues/pgmq for queue and event bus, Postgres locks, pg_cron); the api and sidecars run on docker-swarm on a VPS. The realtime pattern uses the same socket.io gateway inside the be api process (one instance, no Redis adapter), and both editions share knowledge/hfs/fe-stack.yaml. Lite requires application-stacks.yaml and permits environment metadata, runbooks, infra/swarm and encrypted-secret custody, while refusing infra/compose, infra/k8s, infra/terraform, runtime, seeds and components duplicating Supabase backing services. It has no e2e or contract spec, `test:e2e`, `test:contract`, `test:stack` or `test:browser` script, e2e workflow, browser journey, Work catalog, shell, features, resources, UAT or br records, test container, toxiproxy, browser-runner, BullMQ, Kafka, Redis or graphql-ws dependency, or supabase/functions. Its .starciwork is optional and admits only .gitignore, workspace.yaml and brand/**; workspace.yaml is optional and carries project identity and repo roles for the Work tools."
|
|
1850
1854
|
kinds: [check]
|
|
1851
1855
|
gates: [pre-push, settle, land, ci]
|
|
1852
|
-
failureCodes: ["HFS_EDITION_INVALID", "HFS_EDITION_FORBIDDEN_PRESENT"]
|
|
1856
|
+
failureCodes: ["HFS_EDITION_INVALID", "HFS_EDITION_FORBIDDEN_PRESENT", "HFS_STARCIWORK_LITE_FORBIDDEN"]
|
|
1853
1857
|
enforcers:
|
|
1854
1858
|
- {kind: hfs, id: edition, at: scripts/hfs/rules/edition.mjs}
|
|
1859
|
+
- {kind: hfs, id: lite-brand-work, at: scripts/hfs/path-findings.mjs}
|
|
1860
|
+
- {kind: work-validate, id: lite-brand-work, at: scripts/work/validate/work-boundary-check.mjs}
|
|
1855
1861
|
cases:
|
|
1856
1862
|
- id: case-1
|
|
1857
1863
|
date: "2026-10-12"
|
|
@@ -1869,9 +1875,15 @@ rules:
|
|
|
1869
1875
|
quote: "As the student proposes. (translated from Vietnamese; the owner adopted the proposed HFS standard for front end and back end, lite = full)"
|
|
1870
1876
|
ruling: "One HFS tree for both editions: the repository root, be/ (apps, contracts, features, modules, tests) and fe/apps/<app>/src have the same shape in lite and full, and code above the platform/integrations seam is the same. The editions differ only behind that seam: the database and its migration authority (lite supabase/migrations; full TypeORM entities and migrations in be.persistence), the auth provider (lite Supabase Auth through fe.modules.auth and a JWKS-verified back end; full the app's identity provider), object storage (lite Supabase Storage; full S3-compatible storage), queue and jobs (lite pgmq and pg_cron; full BullMQ) behind platform/queue, realtime (lite the socket.io gateway in the api process, one instance; full socket.io scaled with a Redis adapter), the event-bus adapter (lite outbox and pgmq; full outbox and Kafka) and hosting; lite has no e2e. Migration baseline at the cutover of an existing app: the live schema is dumped into one baseline migration written to the current standard and marked applied on production without a re-run, the older migrations move to app.supabase.migrations.archive (supabase/migrations/_archive/, never applied and never judged), and new migrations append after the baseline. Assets: static templates, copy and presets are TS constants in the owning domain capability (<x>.templates.ts) and user-editable ones are seed rows; small build-time binaries such as fonts live in be.assets (be/src/assets/<kind>/); large and user media live in object storage and are fetched at runtime; there is no be/resources/."
|
|
1871
1877
|
|
|
1878
|
+
- id: case-4
|
|
1879
|
+
date: "2026-10-14"
|
|
1880
|
+
source: owner
|
|
1881
|
+
quote: "In a lite app, .starciwork keeps only the brand records."
|
|
1882
|
+
ruling: "Amended by the owner on 2026-10-14: under lite, .starciwork is optional when the app has no brand records and admits only .gitignore, workspace.yaml and brand/** in the existing brand tooling layout, including drawing records, references and assets. workspace.yaml is optional and keeps the project id and repo roles so Work root detection and render-proof role mapping can tie brand records to the front end. index.yaml, shell/, features/, _resources/, uat and br remain forbidden. Lite readers read workspace.yaml normally and skip the full-edition catalog, shell and feature metadata; lite scaffolding creates .gitignore and workspace.yaml, with brand/ only when present. Full edition is unchanged."
|
|
1883
|
+
|
|
1872
1884
|
- id: R213
|
|
1873
1885
|
code: "DB_MIGRATION_SHAPE"
|
|
1874
|
-
law: "A migration is `supabase/migrations/<ts14>_<kebab>.sql`; the stamp is a valid UTC instant
|
|
1886
|
+
law: "A migration is `supabase/migrations/<ts14>_<kebab>.sql`; the stamp is a valid UTC instant, active stamps are unique, and a new migration sorts strictly after the committed active migrations in HEAD. Repository history includes the archive: moving an active migration to the archive retires it from ordering, so a replacement baseline may predate archived stamps and later migrations append after the baseline. Wall-clock time, commit time and remote-branch stamps do not set the ordering boundary. A migration already on the base is immutable (a diff or an unarchived removal is a finding); a file that does not parse as PostgreSQL is a shape failure."
|
|
1875
1887
|
kinds: [check]
|
|
1876
1888
|
gates: [pre-push, settle, land, ci]
|
|
1877
1889
|
failureCodes: ["DB_MIGRATION_SHAPE"]
|
|
@@ -2156,3 +2168,19 @@ rules:
|
|
|
2156
2168
|
enforcers:
|
|
2157
2169
|
- {kind: hfs, id: fe-stack, at: scripts/hfs/rules/fe-stack.mjs}
|
|
2158
2170
|
- {kind: eslint-fe, id: fe-stack-import}
|
|
2171
|
+
- id: R245
|
|
2172
|
+
code: "BE_APP_OPTIONS_COMPOSITION"
|
|
2173
|
+
law: "An app's `src/` holds five app roles (owner ruling 2026-10-14): `main.ts` builds `EnvSource` once, parses inline or imports `<app>.env.ts`, builds options and boots; `<app>.env.ts` holds typed, fail-fast environment parsing only, via `EnvSource` and capability parse helpers, with no logic, module composition, business code or feature imports; only `main.ts` and `<app>.env.ts` may read the app's environment; `<app>.options.ts` holds env-derived options plus port bindings (provider maps such as `{ approvalGate: X }`) only, with no parsing; neither the env nor options file holds an array listing `*Module` classes, module or queue registrations (`register`, `forRoot`, `registerQueue`) or scheduler entries, or an `imports`, `modules`, `schedulers` or `processors` list; the env file also refuses standalone module or queue registration calls; `compose/<area>.ts` exports the module list of one area (and the providers of its global ports when it has any), composition only, with no business logic and no environment read; `app.module.ts` imports the groups and spreads them and binds the app-wide guards and filter."
|
|
2174
|
+
kinds: [lint, codemod]
|
|
2175
|
+
gates: [pre-commit, pre-push, settle, land, ci]
|
|
2176
|
+
failureCodes: ["BE_APP_OPTIONS_COMPOSITION"]
|
|
2177
|
+
enforcers:
|
|
2178
|
+
- {kind: eslint-be, id: app-options-composition-free}
|
|
2179
|
+
- id: R246
|
|
2180
|
+
code: "BE_APP_COMPOSITION_BUDGET"
|
|
2181
|
+
law: "Each app file stays within the line budget its `be.app.*` slot names (`budget`, keyed by the slot entry the file matches): `main.ts` 80, `<app>.env.ts` 150, `<app>.options.ts` 200, `compose/<area>.ts` 120, `app.module.ts` 250. The budget is absolute, not a ratchet: an entrypoint that outgrows it moves typed, fail-fast parsing into `<app>.env.ts`; an app root that outgrows it moves an area into a new `compose/<area>.ts` group, never into the env or options file."
|
|
2182
|
+
kinds: [lint, codemod]
|
|
2183
|
+
gates: [pre-commit, pre-push, settle, land, ci]
|
|
2184
|
+
failureCodes: ["BE_APP_COMPOSITION_BUDGET"]
|
|
2185
|
+
enforcers:
|
|
2186
|
+
- {kind: eslint-be, id: app-file-budget}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
# catalog READ this file through scripts/hfs/slots.mjs; none of them hardcodes a path.
|
|
8
8
|
# Shape: modules/schemas/hfs-slots.schema.yaml. The repository side is hfs.json: modules/schemas/hfs-repo.schema.yaml.
|
|
9
9
|
schema: starci/hfs-slots@3
|
|
10
|
-
version: 3.
|
|
10
|
+
version: 3.1.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).
|
|
@@ -31,6 +31,12 @@ versioning:
|
|
|
31
31
|
# is the socket.io gateway in both editions), fe.route holds pages, layouts and the one health/live route handler, and the slots
|
|
32
32
|
# marked `since: 3.0.0` are added: the Apollo, SWR, RHF, zustand and socket.io hooks, redux, modules/auth, modules/toast, the
|
|
33
33
|
# providers, be.assets and the migration archive.
|
|
34
|
+
# 3.1.0 is a minor (owner ruling 2026-10-14, R245 and R246): every be.app.* slot allows `<app>.env.ts` and the composition groups
|
|
35
|
+
# `compose/<area>.ts` and budgets its app files (`app.module.ts` 250, `<app>.options.ts` 200, `<app>.env.ts` 150,
|
|
36
|
+
# `compose/<area>.ts` 120, `main.ts` 80); `<app>.env.ts` exports interface <App>Env and read<App>Env(env: EnvSource): <App>Env
|
|
37
|
+
# for fail-fast env parsing only, and `<app>.options.ts` holds env-derived options and port bindings only, with no parsing
|
|
38
|
+
# or environment reads (BE_APP_OPTIONS_COMPOSITION). Secrets use EnvSource.secret(): <NAME>_FILE first, plain <NAME> only
|
|
39
|
+
# when the file key is absent; .starcistacks owns custody. Capability <capability>.config.ts roles stay unchanged.
|
|
34
40
|
|
|
35
41
|
# Field meaning, per slot.
|
|
36
42
|
# id stable name, never changes meaning inside a major.
|
|
@@ -139,6 +145,7 @@ naming:
|
|
|
139
145
|
# ---------------------------------------------------------------------------------------------------------
|
|
140
146
|
# Tiers and the dependency direction matrix (owner granularity; type-only imports count; no cycles anywhere)
|
|
141
147
|
# mayImport lists the tiers a file of that tier may import. Importing inside the same owner is always allowed.
|
|
148
|
+
# mayImportKinds narrows a permitted tier edge to its named from/to folder kinds; other kind pairs remain forbidden.
|
|
142
149
|
# ---------------------------------------------------------------------------------------------------------
|
|
143
150
|
tiers:
|
|
144
151
|
be:
|
|
@@ -155,7 +162,7 @@ tiers:
|
|
|
155
162
|
package: {mayImport: [package]}
|
|
156
163
|
fe:
|
|
157
164
|
route: {mayImport: [feature, foundation, modules, transport, package]} # app/** adapters
|
|
158
|
-
feature: {mayImport: [components, hooks, foundation, modules, transport, package]}
|
|
165
|
+
feature: {mayImport: [feature, components, hooks, foundation, modules, transport, package], mayImportKinds: {feature: {from: [pages, layouts], to: [overlays]}}} # pages and layouts compose overlays and blocks; other feature-to-feature edges stay forbidden
|
|
159
166
|
# A component never reaches data: config, routes, i18n and types are foundation; every other module and the api
|
|
160
167
|
# transport are reached through a hook (owner decision 2026-09-29).
|
|
161
168
|
components: {mayImport: [components, hooks, foundation, package], lowerLayerOnly: true} # blocks, composites, branches, leaves
|
|
@@ -501,6 +508,33 @@ slots:
|
|
|
501
508
|
tests: none
|
|
502
509
|
managedBy: docker-ignore
|
|
503
510
|
rules: [HFS_MANAGED_FILE_DRIFT]
|
|
511
|
+
- id: app.deploy
|
|
512
|
+
profiles: [app]
|
|
513
|
+
# Production delivery tooling for the app's own processes: compose files, Caddyfile, *.env.example and ops shell scripts.
|
|
514
|
+
# Lite still keeps its backing services on Supabase and its encrypted custody in app.starcistacks; R06 scans every file here.
|
|
515
|
+
path: "deploy/"
|
|
516
|
+
presence: optional
|
|
517
|
+
tracked: tracked
|
|
518
|
+
tier: none
|
|
519
|
+
tests: none
|
|
520
|
+
rules: [HFS_PLAINTEXT_SECRET]
|
|
521
|
+
- id: app.ci-deploy
|
|
522
|
+
profiles: [app]
|
|
523
|
+
# Product-owned delivery workflows; the literal app.ci-db path remains the managed database workflow's more specific slot.
|
|
524
|
+
path: ".github/workflows/<app>-deploy.yml"
|
|
525
|
+
presence: optional
|
|
526
|
+
tracked: tracked
|
|
527
|
+
tier: none
|
|
528
|
+
tests: none
|
|
529
|
+
rules: [HFS_PLAINTEXT_SECRET]
|
|
530
|
+
- id: app.ci-authorize-deploy-key
|
|
531
|
+
profiles: [app]
|
|
532
|
+
path: ".github/workflows/authorize-deploy-key.yml"
|
|
533
|
+
presence: optional
|
|
534
|
+
tracked: tracked
|
|
535
|
+
tier: none
|
|
536
|
+
tests: none
|
|
537
|
+
rules: [HFS_PLAINTEXT_SECRET]
|
|
504
538
|
- id: app.browser
|
|
505
539
|
profiles: [app]
|
|
506
540
|
# The browser journey of the product (Playwright, owner ruling: one minimal journey per example app, run manually): the config, its
|
|
@@ -536,6 +570,17 @@ slots:
|
|
|
536
570
|
path: scripts/codegen.mjs
|
|
537
571
|
liteManagedBy: codegen-lite
|
|
538
572
|
rules: [HFS_SCRIPT_ONE_OFF, BE_SPEC_PLACEMENT, HFS_REPO_LOCAL_CHECK] # one-off codemods and fix-* scripts are agent output, not tooling
|
|
573
|
+
- id: app.scripts.delivery
|
|
574
|
+
profiles: [app]
|
|
575
|
+
editions: [lite]
|
|
576
|
+
# Product-owned environment, secret and build tooling, kept apart from lite's managed codegen script.
|
|
577
|
+
# Full already owns these files through app.scripts; its generic *.mjs pattern must not compete with build-*.mjs.
|
|
578
|
+
path: "scripts/{env-sync.mjs,with-secrets.mjs,set-secrets.ps1,build-*.mjs}"
|
|
579
|
+
presence: optional
|
|
580
|
+
tracked: tracked
|
|
581
|
+
tier: none
|
|
582
|
+
tests: none
|
|
583
|
+
rules: [HFS_PLAINTEXT_SECRET, HFS_SCRIPT_ONE_OFF, BE_SPEC_PLACEMENT, HFS_REPO_LOCAL_CHECK]
|
|
539
584
|
- id: app.sides
|
|
540
585
|
profiles: [app]
|
|
541
586
|
# The two sides. Everything below be/ and fe/ is judged by the side's own slots (profiles be and fe) with the side folder as its root;
|
|
@@ -558,23 +603,13 @@ slots:
|
|
|
558
603
|
"features/<feature>/ui/<name>/assets/<approved-file>", "features/<feature>/uat/<name>/{index,accounts,fixtures}.yaml",
|
|
559
604
|
"features/<feature>/uat/<name>/{seed,cleanup}.sql", "_resources/{identities,environments,fixtures}/<slug>/resource.yaml"]
|
|
560
605
|
forbids: [evidence/, logs/, reports/, captures/, runs/, "kernel-*/", "*.sqlite*", _derived/]
|
|
561
|
-
#
|
|
606
|
+
# Owner amendment 2026-10-14: lite keeps workspace.yaml for project identity and repo roles, plus brand drawing records,
|
|
607
|
+
# references and assets. No brand means no Work tree is needed; workspace.yaml is legal but optional.
|
|
608
|
+
litePresence: optional
|
|
562
609
|
lite:
|
|
563
|
-
requires: [.gitignore
|
|
564
|
-
allows: [workspace.yaml,
|
|
610
|
+
requires: [.gitignore]
|
|
611
|
+
allows: [workspace.yaml, "brand/**"]
|
|
565
612
|
rules: [HFS_AGENT_DATA_TRACKED, HFS_IDENTITY_CUSTODY]
|
|
566
|
-
- id: app.starciwork.uat
|
|
567
|
-
profiles: [app]
|
|
568
|
-
# The UAT records of the full edition: they are proven on the e2e world (identities, fixtures, br/ac evidence) that lite does
|
|
569
|
-
# not have, so the trees are forbidden paths under lite only. In full this slot does not exist and .starciwork answers for them.
|
|
570
|
-
editions: [lite]
|
|
571
|
-
path: ".starciwork/{_resources/identities,_resources/fixtures,features/<feature>/uat,features/<feature>/br}/"
|
|
572
|
-
presence: forbidden
|
|
573
|
-
tracked: external
|
|
574
|
-
tier: none
|
|
575
|
-
tests: none
|
|
576
|
-
goesTo: "the full edition: a lite app has no e2e world, so no UAT records (no identities or fixtures resources, no br/ac evidence, no uat directories)"
|
|
577
|
-
since: 2.1.0
|
|
578
613
|
- id: app.starcistacks
|
|
579
614
|
profiles: [app]
|
|
580
615
|
# The stack declarations and the sealed custody of the app, at the app root beside be/, fe/ and .starciwork (never under a side).
|
|
@@ -584,16 +619,16 @@ slots:
|
|
|
584
619
|
tier: none
|
|
585
620
|
tests: none
|
|
586
621
|
requires: [application-stacks.yaml]
|
|
587
|
-
allows: [application-stacks.yaml, "<env>/README.md", "<env>/environment.json", "<env>/infra/{compose,k8s,terraform}/**", "<env>/infra/metadata.json",
|
|
622
|
+
allows: [application-stacks.yaml, "<env>/README.md", "<env>/environment.json", "<env>/infra/{compose,k8s,terraform,swarm}/**", "<env>/infra/metadata.json",
|
|
588
623
|
"<env>/runtime/{config,env}/**", "<env>/runtime/env/KEYS.md", "<env>/secrets/<slug>.enc", "<env>/secrets/.gitkeep", "<env>/seeds/**"]
|
|
589
624
|
forbids: ["<env>/runtime/files/**", "**/*.enc outside <env>/secrets/", DESIGN.md, deployment.json, "k8s/ at root"]
|
|
590
|
-
# Lite
|
|
591
|
-
#
|
|
592
|
-
#
|
|
625
|
+
# Lite keeps full's code structure with backing infrastructure replaced by Supabase (owner ruling 2026-10-13). The api and
|
|
626
|
+
# sidecars run on docker-swarm; its value-free stack belongs in <env>/infra/swarm/. The checker also refuses components whose
|
|
627
|
+
# images duplicate Supabase services: postgres, redis, kafka, keycloak, minio, rabbitmq, mysql and mongo.
|
|
593
628
|
lite:
|
|
594
|
-
requires: []
|
|
595
|
-
allows: [application-stacks.yaml, "<env>/README.md", "<env>/environment.json", "<env>/secrets/<slug>.enc", "<env>/secrets/.gitkeep"]
|
|
596
|
-
forbids: ["<env>/
|
|
629
|
+
requires: [application-stacks.yaml]
|
|
630
|
+
allows: [application-stacks.yaml, "<env>/README.md", "<env>/environment.json", "<env>/infra/swarm/**", "<env>/secrets/<slug>.enc", "<env>/secrets/.gitkeep"]
|
|
631
|
+
forbids: ["<env>/infra/{compose,k8s,terraform}/**", "<env>/runtime/**", "<env>/seeds/**", "**/*.enc outside <env>/secrets/", DESIGN.md, deployment.json, "k8s/ at root"]
|
|
597
632
|
rules: [HFS_STACKS_SHAPE, HFS_STACKS_IN_SIDE, HFS_PLAINTEXT_SECRET, HFS_IDENTITY_CUSTODY]
|
|
598
633
|
- id: app.sops
|
|
599
634
|
profiles: [app]
|
|
@@ -637,7 +672,7 @@ slots:
|
|
|
637
672
|
goesTo: "<lanes root>/<project>/<lane>/ (outside every repository) for a lane; a Kernel workflow's worktree is created and owned by Orca outside the app checkout, never tracked, and removed by the runtime's host-side controller once the workflow's finish marked it release-pending"
|
|
638
673
|
- id: app.plaintext-env
|
|
639
674
|
profiles: [app]
|
|
640
|
-
path: "{.env,.env
|
|
675
|
+
path: "{.env,.env.*,secrets.env,.secrets/,**/*.pem,**/*.key}"
|
|
641
676
|
presence: forbidden
|
|
642
677
|
tracked: external
|
|
643
678
|
tier: none
|
|
@@ -959,6 +994,23 @@ slots:
|
|
|
959
994
|
rules: [BE_CONTRACT_BREAKING]
|
|
960
995
|
|
|
961
996
|
# ----- BE: apps (one slot per app kind; a new kind = a new slot, minor bump) ---------------------------
|
|
997
|
+
# The files of every app (owner ruling 2026-10-14, R245 and R246, since 3.1.0), each with a line budget that
|
|
998
|
+
# BE_APP_COMPOSITION_BUDGET holds absolutely (not a ratchet):
|
|
999
|
+
# main.ts the entry: builds EnvSource once, imports and calls read<App>Env from <app>.env.ts, builds options and boots.
|
|
1000
|
+
# <app>.env.ts exports interface <App>Env and read<App>Env(env: EnvSource): <App>Env (for example ApiEnv/readApiEnv);
|
|
1001
|
+
# fail-fast environment parsing only, via EnvSource and unchanged capability parse helpers; no localhost
|
|
1002
|
+
# or default host fallback, logic, module composition, business code or feature imports. Only main.ts
|
|
1003
|
+
# and this app file may read process.env or EnvSource. The interface declares its own readonly parsed
|
|
1004
|
+
# members with parser-derived types, structurally compatible with the app's options;
|
|
1005
|
+
# the role name stays <app>.env.ts, never env.ts. Secrets use EnvSource.secret(): <NAME>_FILE first
|
|
1006
|
+
# for .starcistacks custody, plain <NAME> only when the file key is absent. A declared file supplies
|
|
1007
|
+
# exact contents; an empty or unreadable path stops boot without fallback.
|
|
1008
|
+
# <app>.options.ts the options: env-derived options plus port bindings (provider maps such as { approvalGate: X }) only;
|
|
1009
|
+
# no environment parsing or process.env/EnvSource reads; module, queue or scheduler composition here or in <app>.env.ts is
|
|
1010
|
+
# BE_APP_OPTIONS_COMPOSITION.
|
|
1011
|
+
# compose/<area>.ts a composition group: exports the module list of one area (an imports array, and the providers of the
|
|
1012
|
+
# area's global ports when it has any); composition only, no business logic or process.env/EnvSource read.
|
|
1013
|
+
# app.module.ts the composition root: imports the groups and spreads them, binds the app-wide guards and filter.
|
|
962
1014
|
- id: be.app.api
|
|
963
1015
|
profiles: [be]
|
|
964
1016
|
path: "apps/<app>/src/"
|
|
@@ -969,11 +1021,11 @@ slots:
|
|
|
969
1021
|
owner: true
|
|
970
1022
|
minInstances: 1
|
|
971
1023
|
requires: [main.ts, app.module.ts]
|
|
972
|
-
allows: [main.ts, app.module.ts, "<app>.options.ts", operations.ts]
|
|
1024
|
+
allows: [main.ts, app.module.ts, "<app>.env.ts", "<app>.options.ts", "compose/<area>.ts", operations.ts]
|
|
973
1025
|
tests: none # an app is proven by the e2e world (useTestWorld({ apps })), never by a unit spec
|
|
974
1026
|
coverage: none
|
|
975
|
-
budget: {app.module.ts: 250, main.ts: 80}
|
|
976
|
-
rules: [BE_APP_COMPOSITION_ONLY, BE_APP_BUSINESS_ROLE, BE_DEFAULT_DENY, BE_ERROR_MASKED, BE_FEATURE_NOT_COMPOSED, BE_CONTEXT_OWNER]
|
|
1027
|
+
budget: {app.module.ts: 250, main.ts: 80, "<app>.env.ts": 150, "<app>.options.ts": 200, "compose/<area>.ts": 120}
|
|
1028
|
+
rules: [BE_APP_COMPOSITION_ONLY, BE_APP_BUSINESS_ROLE, BE_APP_OPTIONS_COMPOSITION, BE_APP_COMPOSITION_BUDGET, BE_DEFAULT_DENY, BE_ERROR_MASKED, BE_FEATURE_NOT_COMPOSED, BE_CONTEXT_OWNER]
|
|
977
1029
|
- id: be.app.worker
|
|
978
1030
|
profiles: [be]
|
|
979
1031
|
path: "apps/<app>/src/"
|
|
@@ -983,17 +1035,19 @@ slots:
|
|
|
983
1035
|
tier: app
|
|
984
1036
|
owner: true
|
|
985
1037
|
requires: [main.ts, app.module.ts]
|
|
986
|
-
allows: [main.ts, app.module.ts, "<app>.options.ts"]
|
|
1038
|
+
allows: [main.ts, app.module.ts, "<app>.env.ts", "<app>.options.ts", "compose/<area>.ts"]
|
|
987
1039
|
tests: none # an app is proven by the e2e world (useTestWorld({ apps })), never by a unit spec
|
|
988
1040
|
coverage: none
|
|
989
|
-
|
|
1041
|
+
budget: {app.module.ts: 250, main.ts: 80, "<app>.env.ts": 150, "<app>.options.ts": 200, "compose/<area>.ts": 120}
|
|
1042
|
+
rules: [BE_APP_COMPOSITION_ONLY, BE_APP_OPTIONS_COMPOSITION, BE_APP_COMPOSITION_BUDGET, BE_BACKGROUND_UNOWNED, BE_CONTEXT_OWNER]
|
|
990
1043
|
- id: be.app.cli
|
|
991
1044
|
profiles: [be]
|
|
992
1045
|
# The ONE app of every one-off action (migrate, seed, sync, backup, any operator command): be/apps/cli, on nest-commander.
|
|
993
1046
|
# main.ts boots with CommandFactory.run (BE_CLI_BOOTSTRAP); app.module.ts composes the platform and the cli feature root
|
|
994
|
-
# (src/features/cli/cli.module.ts); cli.
|
|
995
|
-
# among them (the test world migrates through the same list). It is required as soon as the
|
|
996
|
-
# or tracks a command, with its image apps/cli/Dockerfile (BE_CLI_REQUIRED): one image,
|
|
1047
|
+
# (src/features/cli/cli.module.ts); cli.env.ts exports CliEnv and readCliEnv(env: EnvSource): CliEnv, and cli.options.ts carries the env-derived values,
|
|
1048
|
+
# the connections it migrates among them (the test world migrates through the same list). It is required as soon as the
|
|
1049
|
+
# back end declares a connection or tracks a command, with its image apps/cli/Dockerfile (BE_CLI_REQUIRED): one image,
|
|
1050
|
+
# run as `cli <group> <command>`.
|
|
997
1051
|
path: "apps/<app>/src/"
|
|
998
1052
|
appKind: cli
|
|
999
1053
|
presence: opt-in
|
|
@@ -1002,10 +1056,11 @@ slots:
|
|
|
1002
1056
|
tier: app
|
|
1003
1057
|
owner: true
|
|
1004
1058
|
requires: [main.ts, app.module.ts]
|
|
1005
|
-
allows: [main.ts, app.module.ts, "<app>.options.ts"]
|
|
1059
|
+
allows: [main.ts, app.module.ts, "<app>.env.ts", "<app>.options.ts", "compose/<area>.ts"]
|
|
1006
1060
|
tests: none # an app is proven by the e2e world (useTestWorld({ apps })), never by a unit spec
|
|
1007
1061
|
coverage: none
|
|
1008
|
-
|
|
1062
|
+
budget: {app.module.ts: 250, main.ts: 80, "<app>.env.ts": 150, "<app>.options.ts": 200, "compose/<area>.ts": 120}
|
|
1063
|
+
rules: [BE_CLI_REQUIRED, BE_CLI_BOOTSTRAP, BE_CLI_OWNER, BE_SCHEMA_AUTHORITY, BE_APP_OPTIONS_COMPOSITION, BE_APP_COMPOSITION_BUDGET]
|
|
1009
1064
|
|
|
1010
1065
|
# ----- BE: features ------------------------------------------------------------------------------------
|
|
1011
1066
|
# src/features/ is grouped by TRIGGER KIND (owner decision 2026-10-02): features/api/<feature>/ (a request: application/ and
|
|
@@ -1515,7 +1570,7 @@ slots:
|
|
|
1515
1570
|
requires: [index.ts, "<provider>.config.ts"]
|
|
1516
1571
|
tests: unit-beside # <name>.service.spec.ts beside each <name>.service.ts; plus src/tests/integration/<provider>/ (R112)
|
|
1517
1572
|
coverage: required
|
|
1518
|
-
budget: {indexExports:
|
|
1573
|
+
budget: {indexExports: 80} # a provider's port surface includes clients, config, options, tokens and errors
|
|
1519
1574
|
rules: [BE_TIER_DIRECTION, BE_ERROR_HOME, BE_SECRET_DEFAULT, BE_INTEGRATION_SPEC_MISSING]
|
|
1520
1575
|
- id: be.integrations.model
|
|
1521
1576
|
profiles: [be]
|
|
@@ -1789,7 +1844,7 @@ slots:
|
|
|
1789
1844
|
allows: [index.ts, "use<name>.ts", "<domain>.shared.ts"] # React hooks only; <domain>.shared.ts = sanctioned non-hook helpers
|
|
1790
1845
|
roles: {entry: index.ts, shared: "<domain>.shared.ts"}
|
|
1791
1846
|
tests: none
|
|
1792
|
-
budget: {file: 200, dataHooks: 6, useState: 6}
|
|
1847
|
+
budget: {file: 200, barrel: 400, dataHooks: 6, useState: 6} # barrel: index.ts containing only export ... from statements
|
|
1793
1848
|
rules: [FE_HOOKS_ARE_HOOKS, FE_DATA_FRESHNESS]
|
|
1794
1849
|
- id: fe.hooks.swr
|
|
1795
1850
|
profiles: [fe]
|
|
@@ -1806,7 +1861,7 @@ slots:
|
|
|
1806
1861
|
"api/rest/queries/useGet*Swr.ts", "api/rest/mutations/use{Post,Put,Patch,Delete}*Swr.ts"]
|
|
1807
1862
|
roles: {entry: index.ts, effects: SwrSideEffects.tsx}
|
|
1808
1863
|
tests: none
|
|
1809
|
-
budget: {file: 200}
|
|
1864
|
+
budget: {file: 200, barrel: 400}
|
|
1810
1865
|
since: 3.0.0
|
|
1811
1866
|
rules: [FE_HOOKS_ARE_HOOKS, FE_DATA_FRESHNESS]
|
|
1812
1867
|
- id: fe.hooks.rhf
|
|
@@ -1822,7 +1877,7 @@ slots:
|
|
|
1822
1877
|
allows: [index.ts, "use*Form.ts"]
|
|
1823
1878
|
roles: {entry: index.ts}
|
|
1824
1879
|
tests: none
|
|
1825
|
-
budget: {file: 200}
|
|
1880
|
+
budget: {file: 200, barrel: 400}
|
|
1826
1881
|
since: 3.0.0
|
|
1827
1882
|
rules: [FE_HOOKS_ARE_HOOKS]
|
|
1828
1883
|
- id: fe.hooks.zustand
|
|
@@ -1837,7 +1892,7 @@ slots:
|
|
|
1837
1892
|
allows: [index.ts, store.ts, "use*.ts"]
|
|
1838
1893
|
roles: {entry: index.ts, store: store.ts}
|
|
1839
1894
|
tests: none
|
|
1840
|
-
budget: {file: 200}
|
|
1895
|
+
budget: {file: 200, barrel: 400}
|
|
1841
1896
|
since: 3.0.0
|
|
1842
1897
|
rules: [FE_HOOKS_ARE_HOOKS]
|
|
1843
1898
|
- id: fe.hooks.socketio
|
|
@@ -1853,7 +1908,7 @@ slots:
|
|
|
1853
1908
|
allows: [index.ts, sockets.ts, SocketIoSideEffects.tsx, connectionStore.ts, "enums/*.ts", "types/*.ts", "use*.ts"]
|
|
1854
1909
|
roles: {entry: index.ts, effects: SocketIoSideEffects.tsx}
|
|
1855
1910
|
tests: none
|
|
1856
|
-
budget: {file: 200}
|
|
1911
|
+
budget: {file: 200, barrel: 400}
|
|
1857
1912
|
since: 3.0.0
|
|
1858
1913
|
rules: [FE_HOOKS_ARE_HOOKS]
|
|
1859
1914
|
- id: fe.hooks.effects
|
|
@@ -1869,7 +1924,7 @@ slots:
|
|
|
1869
1924
|
allows: [index.ts, "use*.ts", "*SideEffect.tsx", "*SideEffects.tsx"]
|
|
1870
1925
|
roles: {entry: index.ts, effects: "*SideEffect.tsx"}
|
|
1871
1926
|
tests: none
|
|
1872
|
-
budget: {file: 200}
|
|
1927
|
+
budget: {file: 200, barrel: 400}
|
|
1873
1928
|
since: 3.0.0
|
|
1874
1929
|
rules: [FE_HOOKS_ARE_HOOKS]
|
|
1875
1930
|
- id: fe.hooks.reuseables
|
|
@@ -1884,7 +1939,7 @@ slots:
|
|
|
1884
1939
|
allows: [index.ts, "use*.ts"]
|
|
1885
1940
|
roles: {entry: index.ts}
|
|
1886
1941
|
tests: none
|
|
1887
|
-
budget: {file: 200}
|
|
1942
|
+
budget: {file: 200, barrel: 400}
|
|
1888
1943
|
since: 3.0.0
|
|
1889
1944
|
rules: [FE_HOOKS_ARE_HOOKS]
|
|
1890
1945
|
- id: fe.redux
|
|
@@ -1909,6 +1964,7 @@ slots:
|
|
|
1909
1964
|
tier: modules
|
|
1910
1965
|
owner: true
|
|
1911
1966
|
requiredInstances: {capability: [config, i18n, routes]}
|
|
1967
|
+
# A populated fe.modules instance with no .ts or .tsx files (CSS/assets only) needs no index.ts.
|
|
1912
1968
|
requires: [index.ts]
|
|
1913
1969
|
tests: none
|
|
1914
1970
|
budget: {file: 400}
|
|
@@ -174,6 +174,9 @@ rules:
|
|
|
174
174
|
component.tsx. Pure blocks, composites, branches and leaves use index.tsx only and never gain an
|
|
175
175
|
empty twin. classNames.ts is present only when the unit owns class strings. Budgets: component.tsx 300 lines,
|
|
176
176
|
connected index.tsx 200 lines, at most 6 data hooks and 6 useState per connected unit.
|
|
177
|
+
A composition-only feature may render an existing public pure component owner directly instead of creating a
|
|
178
|
+
forwarding component.tsx. The route-state overlay host of FE-BOUNDARY-5 renders components/branches/OverlayHost;
|
|
179
|
+
a connected block still requires its exact private Base twin.
|
|
177
180
|
rationale: |
|
|
178
181
|
Stable basenames make class names and the connected and presentational split discoverable without forwarding
|
|
179
182
|
twins, and budgets keep a unit reviewable.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// cat-file.mjs — `git cat-file <args>`: the type, size or content of repository objects (-e, -p, --batch).
|
|
2
|
+
import { runGit } from './lib.mjs';
|
|
3
|
+
|
|
4
|
+
/** The spawn result {status, stdout, stderr, error} of `git cat-file ...args`; options are runGit's (cwd or dir, config, timeout, maxBuffer, input, encoding, env, git). */
|
|
5
|
+
export const catFile = (args = [], options = {}) => runGit(['cat-file', ...args], options);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// diff.mjs — `git diff <args>`: the changes between commits, the index and the working tree.
|
|
2
|
+
import { runGit } from './lib.mjs';
|
|
3
|
+
|
|
4
|
+
/** The spawn result {status, stdout, stderr, error} of `git diff ...args`; options are runGit's (cwd or dir, config, timeout, maxBuffer, input, encoding, env, git). */
|
|
5
|
+
export const diff = (args = [], options = {}) => runGit(['diff', ...args], options);
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { allowsFile } from '../allows.mjs';
|
|
3
|
+
import { relativePath, unwrapExpression } from './typescript.mjs';
|
|
4
|
+
import { sourceLocation } from '../../lib/ts-ast.mjs';
|
|
5
|
+
|
|
6
|
+
const PROCESS_MODULES = new Set(['process', 'node:process']);
|
|
7
|
+
const PROCESS_GLOBAL = new Set(['process']);
|
|
8
|
+
const GLOBAL_HOLDERS = new Set(['globalThis', 'global']);
|
|
9
|
+
const REFLECT_GLOBAL = new Set(['Reflect']);
|
|
10
|
+
|
|
11
|
+
/** A static property name, including `object['env']` and `object[`env`]`. */
|
|
12
|
+
export function accessName(ts, node) {
|
|
13
|
+
if (ts.isPropertyAccessExpression(node)) return node.name.text;
|
|
14
|
+
if (ts.isElementAccessExpression(node) && ts.isStringLiteralLike(node.argumentExpression)) return node.argumentExpression.text;
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function bindingName(ts, element) {
|
|
19
|
+
const name = element.propertyName ?? element.name;
|
|
20
|
+
if (ts.isIdentifier(name) || ts.isStringLiteralLike(name)) return name.text;
|
|
21
|
+
return ts.isComputedPropertyName(name) && ts.isStringLiteralLike(name.expression) ? name.expression.text : null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A global name is not a same-named local variable or parameter. */
|
|
25
|
+
function isGlobalName(ts, checker, node, names) {
|
|
26
|
+
if (!ts.isIdentifier(node) || !names.has(node.text)) return false;
|
|
27
|
+
const declarations = checker?.getSymbolAtLocation(node)?.getDeclarations() ?? [];
|
|
28
|
+
return declarations.every(declaration => declaration.getSourceFile().isDeclarationFile);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function recordProcessBindings(ts, clause, add, report) {
|
|
32
|
+
if (clause?.name) add(clause.name);
|
|
33
|
+
const bindings = clause?.namedBindings;
|
|
34
|
+
if (bindings && ts.isNamespaceImport(bindings)) add(bindings.name);
|
|
35
|
+
if (!bindings || !ts.isNamedImports(bindings)) return;
|
|
36
|
+
for (const element of bindings.elements) {
|
|
37
|
+
const name = (element.propertyName ?? element.name).text;
|
|
38
|
+
if (name === 'env') report(element);
|
|
39
|
+
else if (name === 'default') add(element.name);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function processImportSymbols(ts, checker, sourceFile, report) {
|
|
44
|
+
const imports = new Set();
|
|
45
|
+
const add = name => {
|
|
46
|
+
const symbol = checker.getSymbolAtLocation(name);
|
|
47
|
+
if (symbol) imports.add(symbol);
|
|
48
|
+
};
|
|
49
|
+
for (const statement of sourceFile.statements) {
|
|
50
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteralLike(statement.moduleSpecifier)
|
|
51
|
+
|| !PROCESS_MODULES.has(statement.moduleSpecifier.text)) continue;
|
|
52
|
+
recordProcessBindings(ts, statement.importClause, add, report);
|
|
53
|
+
}
|
|
54
|
+
return imports;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Process bindings are resolved by symbol, so a nested parameter cannot masquerade as an imported process. */
|
|
58
|
+
function processAccess(ts, checker, sourceFile, report) {
|
|
59
|
+
const imports = processImportSymbols(ts, checker, sourceFile, report);
|
|
60
|
+
const isProcess = expression => {
|
|
61
|
+
const node = unwrapExpression(ts, expression);
|
|
62
|
+
if (!node) return false;
|
|
63
|
+
if (ts.isIdentifier(node)) return imports.has(checker.getSymbolAtLocation(node))
|
|
64
|
+
|| isGlobalName(ts, checker, node, PROCESS_GLOBAL);
|
|
65
|
+
return accessName(ts, node) === 'process'
|
|
66
|
+
&& isGlobalName(ts, checker, node.expression, GLOBAL_HOLDERS);
|
|
67
|
+
};
|
|
68
|
+
return node => {
|
|
69
|
+
if (accessName(ts, node) === 'env' && isProcess(node.expression)) report(node);
|
|
70
|
+
if (ts.isVariableDeclaration(node) && node.initializer && isProcess(node.initializer) && ts.isObjectBindingPattern(node.name)
|
|
71
|
+
&& node.name.elements.some(element => bindingName(ts, element) === 'env')) report(node);
|
|
72
|
+
if (ts.isCallExpression(node) && accessName(ts, node.expression) === 'get'
|
|
73
|
+
&& isGlobalName(ts, checker, node.expression.expression, REFLECT_GLOBAL)
|
|
74
|
+
&& isProcess(node.arguments[0]) && node.arguments[1] && ts.isStringLiteralLike(node.arguments[1]) && node.arguments[1].text === 'env') report(node);
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The declaration, rather than the caller's name, identifies EnvSource readers and configuration helpers. */
|
|
79
|
+
function appEnvironmentCall(config, ts, checker, call) {
|
|
80
|
+
const declaration = checker.getResolvedSignature(call)?.declaration;
|
|
81
|
+
if (!declaration) return false;
|
|
82
|
+
const relative = relativePath(config.root, declaration.getSourceFile().fileName);
|
|
83
|
+
if (ts.isMethodDeclaration(declaration) && ts.isClassDeclaration(declaration.parent) && declaration.parent.name?.text === 'EnvSource') {
|
|
84
|
+
return config.hfs.tierOf(relative) === 'platform' && path.posix.basename(config.hfs.ownerOf(relative)?.root ?? '') === 'config';
|
|
85
|
+
}
|
|
86
|
+
if (!ts.isFunctionDeclaration(declaration) && !ts.isArrowFunction(declaration) && !ts.isFunctionExpression(declaration)) return false;
|
|
87
|
+
return allowsFile(config.hfs, relative)?.entry === '<app>.env.ts'
|
|
88
|
+
|| (path.posix.basename(relative).endsWith('.config.ts') && config.hfs.slotOf(relative) !== null);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Composition groups and options consume parsed options; only the app's matching env role and main read them. */
|
|
92
|
+
export function checkApplicationEnvironment(config, context, sourceFile, fileName, violations) {
|
|
93
|
+
const role = allowsFile(config.hfs, relativePath(config.root, fileName));
|
|
94
|
+
if (role?.allowed && (role.entry === 'main.ts' || role.entry === '<app>.env.ts')) return;
|
|
95
|
+
const { ts } = context;
|
|
96
|
+
const checker = context.checkerFor(fileName);
|
|
97
|
+
const report = node => violations.push({
|
|
98
|
+
ruleId: 'BE_APP_COMPOSITION_ONLY',
|
|
99
|
+
path: relativePath(config.root, fileName),
|
|
100
|
+
...sourceLocation(sourceFile, node),
|
|
101
|
+
message: "Application environment reads live in main.ts or the matching <app>.env.ts. Options files and compose/<area>.ts receive the typed result of read<App>Env(env: EnvSource).",
|
|
102
|
+
});
|
|
103
|
+
const checkProcess = processAccess(ts, checker, sourceFile, report);
|
|
104
|
+
const visit = node => {
|
|
105
|
+
checkProcess(node);
|
|
106
|
+
if (ts.isCallExpression(node) && appEnvironmentCall(config, ts, checker, node)) report(node);
|
|
107
|
+
ts.forEachChild(node, visit);
|
|
108
|
+
};
|
|
109
|
+
visit(sourceFile);
|
|
110
|
+
}
|