@starci/eslint-grammar-core 0.15.34 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +1 -1
  3. package/dist/index.js +2 -0
  4. package/dist/lib/why.js +10 -0
  5. package/dist/mascot.d.ts +28 -0
  6. package/dist/mascot.js +166 -0
  7. package/docs/mascot.md +60 -0
  8. package/package.json +2 -2
  9. package/runtime/bundle.json +74 -0
  10. package/runtime/engine/by-code-unit.mjs +6 -6
  11. package/runtime/engine/model-config.mjs +98 -76
  12. package/runtime/engine/orca-config.mjs +22 -17
  13. package/runtime/engine/release-config.mjs +22 -18
  14. package/runtime/engine/removed-vocabulary.mjs +21 -20
  15. package/runtime/engine/resources-config.mjs +42 -35
  16. package/runtime/engine/runtime-root.mjs +11 -13
  17. package/runtime/engine/secrets.mjs +170 -102
  18. package/runtime/engine/sonar-config.mjs +16 -13
  19. package/runtime/knowledge/hfs/canon-pins.yaml +10 -10
  20. package/runtime/knowledge/hfs/rules.yaml +72 -6
  21. package/runtime/knowledge/hfs/slots.yaml +2 -2
  22. package/runtime/knowledge/patterns/fe/ui.yaml +12 -1
  23. package/runtime/scripts/hfs/allows.mjs +37 -38
  24. package/runtime/scripts/hfs/architecture/config.mjs +302 -268
  25. package/runtime/scripts/hfs/architecture/side-boundary.mjs +2 -5
  26. package/runtime/scripts/hfs/architecture/type-context.mjs +255 -250
  27. package/runtime/scripts/hfs/architecture/typescript.mjs +358 -318
  28. package/runtime/scripts/hfs/declaration-shape.mjs +140 -103
  29. package/runtime/scripts/hfs/declaration-slots.mjs +64 -61
  30. package/runtime/scripts/hfs/edition-slots.mjs +43 -40
  31. package/runtime/scripts/hfs/param-names.mjs +6 -11
  32. package/runtime/scripts/hfs/rule-catalog.mjs +353 -257
  33. package/runtime/scripts/hfs/rule-params-shape.mjs +60 -47
  34. package/runtime/scripts/hfs/slot-app-view.mjs +69 -74
  35. package/runtime/scripts/hfs/slot-classify.mjs +42 -38
  36. package/runtime/scripts/hfs/slot-errors.mjs +8 -7
  37. package/runtime/scripts/hfs/slot-imports.mjs +70 -63
  38. package/runtime/scripts/hfs/slot-manifest-shape.mjs +74 -52
  39. package/runtime/scripts/hfs/slot-match.mjs +122 -111
  40. package/runtime/scripts/hfs/slot-path.mjs +0 -2
  41. package/runtime/scripts/hfs/slot-required.mjs +65 -59
  42. package/runtime/scripts/hfs/slot-semantic-problems.mjs +138 -112
  43. package/runtime/scripts/hfs/slot-side-problems.mjs +23 -19
  44. package/runtime/scripts/hfs/slots.mjs +175 -152
  45. package/runtime/scripts/hfs/typescript-programs.mjs +66 -69
  46. package/runtime/scripts/hfs/view.mjs +38 -40
  47. package/runtime/scripts/lib/glob.mjs +23 -14
  48. package/runtime/scripts/lib/grammar-lint.mjs +33 -37
  49. package/runtime/scripts/lib/i18n.mjs +32 -35
  50. package/runtime/scripts/lib/json-clone.mjs +45 -36
  51. package/runtime/scripts/lib/json.mjs +26 -14
  52. package/runtime/scripts/lib/list.mjs +9 -14
  53. package/runtime/scripts/lib/path-key.mjs +53 -38
  54. package/runtime/scripts/lib/read-text.mjs +30 -17
  55. package/runtime/scripts/lib/regex.mjs +1 -2
  56. package/runtime/scripts/lib/source-phrases.mjs +13 -13
  57. package/runtime/scripts/lib/ts-analysis.mjs +28 -26
  58. package/runtime/scripts/lib/ts-ast.mjs +36 -42
  59. package/runtime/scripts/lib/walk.mjs +33 -27
  60. package/runtime/scripts/lib/yaml-cached.mjs +4 -4
@@ -1,21 +1,24 @@
1
- import {isPlainObject as plain} from './plain-object.mjs';
2
- import {invalid} from './invalid-config.mjs';
3
-
1
+ import { isPlainObject as plain } from './plain-object.mjs';
2
+ import { invalid } from './invalid-config.mjs';
4
3
  /** The keys of the sonar block. */
5
- const SONAR_KEYS=Object.freeze(['organization']);
4
+ const SONAR_KEYS = Object.freeze(['organization']);
6
5
  /** A SonarCloud organization key: lowercase letters, digits, hyphens and underscores, starting with a letter or a digit. */
7
- const ORGANIZATION=/^[a-z0-9][a-z0-9_-]*$/;
8
-
6
+ const ORGANIZATION = /^[a-z0-9][a-z0-9_-]*$/;
9
7
  /**
10
8
  * config.yaml `sonar` - {organization?}: the key of the SonarCloud organization the release's Sonar proof of the example apps analyses in
11
9
  * (the project key is <organization>_<key the example declares>). It is configuration, not a secret; the environment variable
12
10
  * SONAR_ORGANIZATION wins over it, the repository variable of the same name is what CI reads. Null or absent means "not set yet".
13
11
  */
14
- export function validateSonar(sonar){
15
- if(sonar===null)return;
16
- const bad=invalid('sonar');
17
- if(!plain(sonar))bad(' must be {organization?: <SonarCloud organization key>} or null.');
18
- for(const key of Object.keys(sonar))if(!SONAR_KEYS.includes(key))bad(` has unknown key ${key} (allowed: ${SONAR_KEYS.join(', ')}).`);
19
- const value=sonar.organization;
20
- if(value!==undefined&&value!==null&&!(typeof value==='string'&&ORGANIZATION.test(value)))bad('.organization must be a SonarCloud organization key (lowercase letters, digits, - and _) or null.');
12
+ export function validateSonar(sonar) {
13
+ if (sonar === null)
14
+ return;
15
+ const bad = invalid('sonar');
16
+ if (!plain(sonar))
17
+ return bad(' must be {organization?: <SonarCloud organization key>} or null.');
18
+ for (const key of Object.keys(sonar))
19
+ if (!SONAR_KEYS.includes(key))
20
+ bad(` has unknown key ${key} (allowed: ${SONAR_KEYS.join(', ')}).`);
21
+ const value = sonar.organization;
22
+ if (value !== undefined && value !== null && !(typeof value === 'string' && ORGANIZATION.test(value)))
23
+ bad('.organization must be a SonarCloud organization key (lowercase letters, digits, - and _) or null.');
21
24
  }
@@ -17,7 +17,7 @@ pins:
17
17
  # --- @starci packages. Every @starci package is a PUBLISHED npm package (install: registry): a product repository
18
18
  # installs the exact pinned version from the npm registry in the root and every workspace package.json, never `file:`.
19
19
  '@starci/heroicons':
20
- version: 0.3.4
20
+ version: 0.3.5
21
21
  group: starci
22
22
  install: registry
23
23
  side: fe
@@ -31,35 +31,35 @@ pins:
31
31
  source: packages/event-stream/package.json
32
32
  why: 'the one server-sent-events client of every front end (fe-stack event-stream, owner:event-stream-package): the transport client layer (modules/api/sse/clients) creates one instance for the app with the bearer token source and the stream operations follow on it; it carries Last-Event-ID resume, the capped doubling backoff, the heartbeat timeout, the single 401 renewal and the useSWRSubscription adapter, so no app keeps a hand-written reader.'
33
33
  '@starci/grammar':
34
- version: 0.15.34
34
+ version: 0.16.0
35
35
  group: starci
36
36
  install: registry
37
37
  side: fe
38
38
  source: packages/grammar/package.json
39
39
  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.'
40
40
  '@starci/eslint-canon-be':
41
- version: 3.8.21
41
+ version: 3.9.0
42
42
  group: starci
43
43
  install: registry
44
44
  side: be
45
45
  source: packages/eslint-canon-be/package.json
46
46
  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.'
47
47
  '@starci/eslint-canon-fe':
48
- version: 9.5.23
48
+ version: 9.6.0
49
49
  group: starci
50
50
  install: registry
51
51
  side: fe
52
52
  source: packages/eslint-canon-fe/package.json
53
53
  why: 'the front-end ESLint canon: `loadHfs(import.meta.url)` of fe/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the fe 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; the front end has no tests (FE_NO_TESTS R97 in hfs) and no-vietnamese-in-source (R91) applies.'
54
54
  '@starci/eslint-grammar-core':
55
- version: 0.15.34
55
+ version: 0.16.0
56
56
  group: starci
57
57
  install: registry
58
58
  side: fe
59
59
  source: packages/eslint-grammar-core/package.json
60
60
  why: 'the StarCi Grammar component API lint, versioned with @starci/grammar; front ends compose this family package with the universal FE canon.'
61
61
  '@starci/eslint-grammar-offset-pop':
62
- version: 0.15.34
62
+ version: 0.16.0
63
63
  group: starci
64
64
  install: registry
65
65
  side: fe
@@ -85,28 +85,28 @@ pins:
85
85
  side: both
86
86
  source: packages/prettier-config/package.json
87
87
  '@starci/jest-preset':
88
- version: 2.5.2
88
+ version: 2.5.3
89
89
  group: starci
90
90
  install: registry
91
91
  side: be
92
92
  source: packages/jest-preset/package.json
93
93
  why: 'the Jest preset of a back end: the world runner runs up to min(--maxWorkers, slots) world files at once, each in its own process bound to one data slot of the test world (STARCI_TEST_WORLD_SLOT), and reads test-world state protocol 2 only: it pairs exactly with @starci/test-world, and any other state file is JEST_PRESET_WORLD_PAIR_MISMATCH. The unit run writes the lcov Sonar and Codecov import (services only); the integration, e2e and contract projects run every spec file in a worker process of its own (world-runner.cjs), so a process-global framework registry (@nestjs/graphql type metadata) never leaks between e2e files; it ships the unit kit (mockEntityManager, fakeTransaction, fakeQueryBuilder, queueQueryBuilders, fakeIds, FakeClock, Outcome matchers) and the recordingOutbox claim side.'
94
94
  '@starci/test-world':
95
- version: 1.3.2
95
+ version: 1.3.3
96
96
  group: starci
97
97
  install: registry
98
98
  side: be
99
99
  source: packages/test-world/package.json
100
100
  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. A fake declared https (defineHttpFake) serves TLS with the run''s ephemeral in-memory fake CA and publishes its PEM as w.fake.<name>.ca for the app''s own client to trust; verification is never turned off. 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.'
101
101
  '@starci/ai-router':
102
- version: 0.4.0
102
+ version: 0.4.1
103
103
  group: starci
104
104
  install: registry
105
105
  side: be
106
106
  source: packages/ai-router/package.json
107
107
  why: 'the AI pool router of a back end: a NestJS module that routes OpenAI-compatible chat, stream and embeddings calls across keys, models and providers, with @starci/ai-router/testing for its specs; @starci/jest-preset compiles it as an ESM-only dependency.'
108
108
  '@starci/hfs':
109
- version: 4.11.22
109
+ version: 4.12.0
110
110
  group: starci
111
111
  install: registry
112
112
  side: both
@@ -19,7 +19,7 @@
19
19
  schema: starci/hfs-rules@3
20
20
  version: 3.1.0
21
21
  gates:
22
- pre-commit: the managed L0 hook runs app hygiene, managed-file drift, ESLint per side and stylelint on staged source, and prettier on staged files; typecheck and targeted specs belong to the op gate under R225
22
+ pre-commit: the managed L0 hook runs app check --staged, managed-file drift, ESLint per side and stylelint on staged source, and prettier on staged files; typecheck and targeted specs belong to the op gate under R225
23
23
  pre-push: the managed app hook invokes the bundled push policy under R225; it starts no source check, build or test
24
24
  settle: the op gate `starci gate run` over the op's changed files (merge guard, starci app lint --changed, codegen, tsc, the slice's specs; only findings new against the workflow's previous checkpoint block), re-read at starci kernel settle, which refuses a red done; a green settle commits the op's checkpoint on wf-<workflowId>
25
25
  land: the workflow's finish, the only time main is touched - gate.mjs over the whole workflow branch against its merge-base with main, the merge guard, review.verify of the exact head that lands, the branch rebased onto main, then main fast-forwarded and pushed
@@ -918,7 +918,7 @@ rules:
918
918
  code: "BE_TEST_TOPOLOGY"
919
919
  editions: [lite, full]
920
920
  docs: [docs/quality-bar.md#r47-test-topology]
921
- law: "One jest config, projects `unit`, `integration`, `e2e` and `contract`, live by folder, `diagnostics: false`; unit specs are `<name>.<role>.spec.ts` beside the file they test: required for the roles of `ruleParams.be.unitRoles` (`<name>.service.spec.ts` beside a `<name>.service.ts`, `<name>.cli.spec.ts` beside a cli command `<name>.cli.ts` of `src/features/cli`), optional for any other logic role (`ruleParams.be.logicRoles`) inside a slot whose `coverage` is required and beside a logic file such a slot declares (`logic`: `passport.tokens.spec.ts` beside `passport.tokens.ts`), except that the commit adding a measured logic file stages its `<name>.<role>.spec.ts` beside it (owner 2026-10-20, judged by `starci app hygiene` in the pre-commit hook), and nowhere else but beside a webhook or realtime door that has no service of its own (`<provider>.webhook.spec.ts`, `<channel>.gateway.spec.ts`, `<x>.subscription.spec.ts`) (every unit-tested subject has exactly one; any other `*.spec.ts` outside `be/src/tests/{world,integration,e2e,contract}` is a finding, including specs in apps), and the unit project collects coverage from the logic roles of the measured roots and the logic files their slots declare only (the scope derived from the slot manifest, R204) with a per-file threshold of 100 on lines, branches, functions and statements (`test` runs `--coverage`; `importHelpers` and `tslib`); `be/src/tests/world/` is the ONLY test infrastructure: `global-setup.ts` starts the app's own stack (`.starcistacks/<env>`, named by `stack` in `test-world.config.ts`, the declaration of the test-world library in its one named form `export const { useTestWorld, useSandbox } = defineTestWorld({...})` (a default export is not read): every service it declares runs REAL, behind toxiproxy, images and versions read from the stack at run time, never spelled in test source) or attaches to a warm one that answers, and runs the migrations once over the cli app's connections (the runner of the cli migrate command); `global-teardown.ts` stops only what its setup started; `use-test-world.ts` exports `useTestWorld({ apps } | { modules })` (`world.apps.<name>.api`, `world.db.<connection>`, `world.infra.<service>` with `latency(ms)`, `cut()` and `restore()` on the real service, `world.fake.<provider>`, `world.waitFor`) and `fakes/<provider>/` holds the network-edge fakes (`failNext`, `replayWebhook`, `delay`, payload fixtures) of external SaaS the team does not operate ONLY: a fake of a service the stack declares is refused, except stateless compute that needs special hardware or an external model (GPU inference, a self-hosted embedding model), whose `stacks` entry in `test-world.config.ts` declares `{ fakedBy: <fake>, reason }` with a non-empty `reason` (a stateful service, or one with a persistent volume, is never accepted); it alone owns containers, DataSource, migrations and `process.env`; nothing under `be/src/tests` overrides a provider or DI token; `be/src/tests/integration/<capability>/*.integration-spec.ts` (`{ modules }`, with the real peer apps its client calls beside them as `apps`; real infrastructure, no HTTP door of ours), `be/src/tests/e2e/<area>/*.e2e-spec.ts` (`{ apps }`) and `be/src/tests/contract/<provider>/*.contract-spec.ts` (provider sandboxes, skipped without sandbox config, run only by `test:contract`, never by `test` or `test:e2e`) agree folder with suffix; an integration or e2e spec passes `useTestWorld` one object literal with plain keys (no variable, spread or computed key) and calls it; the contract layer has no skip either: a contract spec writes `sandbox.describe(...)` from the test-world library's `useSandbox(...)`, and when the sandbox config is absent that describe registers one failing test naming the missing keys (the body never runs), and every `fakes/<provider>/` that serves payload fixtures (`payloads/*.json`) has a `contract/<provider>/*.contract-spec.ts` that references those fixtures and asserts `shapeOf(real)` equals `shapeOf(fixture)` (the helper `shapeOf` is named by `ruleParams.be.contractShape` of the slot manifest; fakes the test-world library provides are guarded by the library); no `.test.ts`, `int-spec` or `harness-spec`, no `live/` and no `be/src/tests/e2e/world`."
921
+ law: "One jest config, projects `unit`, `integration`, `e2e` and `contract`, live by folder, `diagnostics: false`; unit specs are `<name>.<role>.spec.ts` beside the file they test: required for the roles of `ruleParams.be.unitRoles` (`<name>.service.spec.ts` beside a `<name>.service.ts`, `<name>.cli.spec.ts` beside a cli command `<name>.cli.ts` of `src/features/cli`), optional for any other logic role (`ruleParams.be.logicRoles`) inside a slot whose `coverage` is required and beside a logic file such a slot declares (`logic`: `passport.tokens.spec.ts` beside `passport.tokens.ts`), except that the commit adding a measured logic file stages its `<name>.<role>.spec.ts` beside it (owner 2026-10-20, judged by `starci app check --staged` in the pre-commit hook), and nowhere else but beside a webhook or realtime door that has no service of its own (`<provider>.webhook.spec.ts`, `<channel>.gateway.spec.ts`, `<x>.subscription.spec.ts`) (every unit-tested subject has exactly one; any other `*.spec.ts` outside `be/src/tests/{world,integration,e2e,contract}` is a finding, including specs in apps), and the unit project collects coverage from the logic roles of the measured roots and the logic files their slots declare only (the scope derived from the slot manifest, R204) with a per-file threshold of 100 on lines, branches, functions and statements (`test` runs `--coverage`; `importHelpers` and `tslib`); `be/src/tests/world/` is the ONLY test infrastructure: `global-setup.ts` starts the app's own stack (`.starcistacks/<env>`, named by `stack` in `test-world.config.ts`, the declaration of the test-world library in its one named form `export const { useTestWorld, useSandbox } = defineTestWorld({...})` (a default export is not read): every service it declares runs REAL, behind toxiproxy, images and versions read from the stack at run time, never spelled in test source) or attaches to a warm one that answers, and runs the migrations once over the cli app's connections (the runner of the cli migrate command); `global-teardown.ts` stops only what its setup started; `use-test-world.ts` exports `useTestWorld({ apps } | { modules })` (`world.apps.<name>.api`, `world.db.<connection>`, `world.infra.<service>` with `latency(ms)`, `cut()` and `restore()` on the real service, `world.fake.<provider>`, `world.waitFor`) and `fakes/<provider>/` holds the network-edge fakes (`failNext`, `replayWebhook`, `delay`, payload fixtures) of external SaaS the team does not operate ONLY: a fake of a service the stack declares is refused, except stateless compute that needs special hardware or an external model (GPU inference, a self-hosted embedding model), whose `stacks` entry in `test-world.config.ts` declares `{ fakedBy: <fake>, reason }` with a non-empty `reason` (a stateful service, or one with a persistent volume, is never accepted); it alone owns containers, DataSource, migrations and `process.env`; nothing under `be/src/tests` overrides a provider or DI token; `be/src/tests/integration/<capability>/*.integration-spec.ts` (`{ modules }`, with the real peer apps its client calls beside them as `apps`; real infrastructure, no HTTP door of ours), `be/src/tests/e2e/<area>/*.e2e-spec.ts` (`{ apps }`) and `be/src/tests/contract/<provider>/*.contract-spec.ts` (provider sandboxes, skipped without sandbox config, run only by `test:contract`, never by `test` or `test:e2e`) agree folder with suffix; an integration or e2e spec passes `useTestWorld` one object literal with plain keys (no variable, spread or computed key) and calls it; the contract layer has no skip either: a contract spec writes `sandbox.describe(...)` from the test-world library's `useSandbox(...)`, and when the sandbox config is absent that describe registers one failing test naming the missing keys (the body never runs), and every `fakes/<provider>/` that serves payload fixtures (`payloads/*.json`) has a `contract/<provider>/*.contract-spec.ts` that references those fixtures and asserts `shapeOf(real)` equals `shapeOf(fixture)` (the helper `shapeOf` is named by `ruleParams.be.contractShape` of the slot manifest; fakes the test-world library provides are guarded by the library); no `.test.ts`, `int-spec` or `harness-spec`, no `live/` and no `be/src/tests/e2e/world`."
922
922
  kinds: [check, codemod, lint]
923
923
  gates: [land, ci]
924
924
  failureCodes: ["BE_TEST_TOPOLOGY", "BE_CONTRACT_UNGUARDED", "BE_UNIT_SPEC_NOT_STAGED"]
@@ -3619,7 +3619,7 @@ rules:
3619
3619
  docs: [docs/lite-edition.md#edition-validation]
3620
3620
  templateNa: "The hfs.json edition declaration is generated programmatically by packages/hfs/scaffold/app.mjs; no managed declaration template exists. Forbidden paths are rejected rather than scaffolded."
3621
3621
  code: "HFS_EDITION_INVALID"
3622
- law: "hfs.json names an edition the manifest knows (full, or lite; absent means full). Lite is a minimal production (case-10): the same monorepo, code structure, slots, patterns and back-end unit specs, one stateless instance type that scales horizontally, its own PostgreSQL db service, Passport identity and S3-compatible storage (ruling:lite-self-hosted-postgres-passport-minio), and one minimal instance of each backing service a declared capability needs (hfs.json sides.be.backing: Redis for the queue, with AOF persistence); vector search uses pgvector in the db service (owner:lite-vector-is-pgvector, which excludes Qdrant from lite); a backing service is declared or absent, never on by default. The queue is BullMQ on Redis in both editions, fed by the Postgres outbox; pg_cron ticks enqueue through that outbox; the event bus keeps its lite adapter (pgmq-backed topics or in-process delivery) because full's event bus is Kafka, not Redis; cache and locks are Postgres unless Redis is declared; the api and sidecars run on docker-swarm on a VPS. The realtime pattern serves SSE (the default for one-way push), the same socket.io gateway or the restricted GraphQL subscription inside the be api process (scales horizontally and stateless: the memory hub is for a single replica or tests only, and replicas > 1 use the Redis hub and the sharded @socket.io/redis-adapter when Redis is declared, else the Postgres hub and @socket.io/postgres-adapter; both shared socket adapters force websocket-only connections with no sticky sessions; R212 case-9, which amends the realtime clauses of case-2, case-3 and case-5; the GraphQL subscription door is R212 case-7 and owner:lite-graphql-subscription-door), 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 and backing components other than its db service (runtime/{config,env}, seeds and the sealed secrets of its environments are allowed). Contract specs and `test:contract` are allowed (owner 2026-10-14); it has no e2e spec, `test:e2e`, `test:stack` or `test:browser` script, e2e workflow, browser journey, Work catalog, shell, features, resources, UAT or br records, test container, toxiproxy, browser-runner or Kafka dependency, client library of an undeclared backing service (HFS_BACKING_UNDECLARED), declared backing service without a consumer (HFS_BACKING_UNUSED), or a supabase/ tree (LITE_DB_MIGRATION). 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."
3622
+ law: "hfs.json names an edition the manifest knows (full, or lite; absent means full). Lite is a minimal production (case-10): the same monorepo, code structure, slots, patterns and back-end unit specs, one stateless instance type that scales horizontally, its own PostgreSQL db service, Passport identity and S3-compatible storage (ruling:lite-self-hosted-postgres-passport-minio), and one minimal instance of each backing service a declared capability needs (hfs.json sides.be.backing: Redis for the queue, with AOF persistence); vector search uses pgvector in the db service (owner:lite-vector-is-pgvector, which excludes Qdrant from lite); a backing service is declared or absent, never on by default. The queue is BullMQ on Redis in both editions, fed by the Postgres outbox; pg_cron ticks enqueue through that outbox; the event bus keeps its lite adapter (pgmq-backed topics or in-process delivery) because full's event bus is Kafka, not Redis; cache and locks are Postgres unless Redis is declared; the api and sidecars run on docker-swarm on a VPS. The realtime pattern serves SSE (the default for one-way push), the same socket.io gateway or the restricted GraphQL subscription inside the be api process (scales horizontally and stateless: the memory hub is for a single replica or tests only, and replicas > 1 use the Redis hub and the sharded @socket.io/redis-adapter when Redis is declared, else the Postgres hub and @socket.io/postgres-adapter; both shared socket adapters force websocket-only connections with no sticky sessions; R212 case-9, which amends the realtime clauses of case-2, case-3 and case-5; the GraphQL subscription door is R212 case-7 and owner:lite-graphql-subscription-door), 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 and backing components other than its db service (runtime/{config,env}, seeds and the sealed secrets of its environments are allowed). Contract specs and `test:contract` are allowed (owner 2026-10-14); it has no e2e spec, `test:e2e`, `test:world` or `test:browser` script, e2e workflow, browser journey, Work catalog, shell, features, resources, UAT or br records, test container, toxiproxy, browser-runner or Kafka dependency, client library of an undeclared backing service (HFS_BACKING_UNDECLARED), declared backing service without a consumer (HFS_BACKING_UNUSED), or a supabase/ tree (LITE_DB_MIGRATION). 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."
3623
3623
  kinds: [check]
3624
3624
  gates: [settle, land, ci]
3625
3625
  failureCodes: ["HFS_EDITION_INVALID", "HFS_EDITION_FORBIDDEN_PRESENT", "HFS_STARCIWORK_LITE_FORBIDDEN", "HFS_BACKING_UNDECLARED", "HFS_BACKING_UNUSED"]
@@ -4157,7 +4157,7 @@ rules:
4157
4157
  code: "ENV_VALUE_SHAPE"
4158
4158
  editions: [lite, full]
4159
4159
  docs: [docs/host-secrets.md#r249]
4160
- law: "A stored environment value parses as the type its `EnvSource` reader declares (an integer, `true`/`false`, an absolute URL, a duration `250`/`30s`/`5m`/`2h`, a member of the declared enum, a nonempty string or secret): `starci app secret verify <env>` decrypts every member in memory and reports each key and its violated shape, never a value, and a concrete (non-placeholder) value of a `*.env.example` parses the same way."
4160
+ law: "A stored environment value parses as the type its `EnvSource` reader declares (an integer, `true`/`false`, an absolute URL, a duration `250`/`30s`/`5m`/`2h`, a member of the declared enum, a nonempty string or secret): `starci app secret verify --env <env>` decrypts every member in memory and reports each key and its violated shape, never a value, and a concrete (non-placeholder) value of a `*.env.example` parses the same way."
4161
4161
  kinds: [check]
4162
4162
  gates: [settle, land, ci]
4163
4163
  failureCodes: ["ENV_VALUE_SHAPE"]
@@ -5242,7 +5242,7 @@ rules:
5242
5242
  - {path: packages/hfs/src/mcp-tools-snapshot.spec.mjs, editions: [lite, full], codes: [HFS_MCP_TOOLS_SNAPSHOT]}
5243
5243
  - id: R303
5244
5244
  code: "HFS_BYPASS_MARKER"
5245
- law: "A product repository holds no bypass of a check (owner ruling loosening-always-red): no inline disable of any tool (eslint, stylelint, tslint, prettier, biome), no ts-ignore, ts-expect-error or ts-nocheck directive, no Sonar exclusion comment, no istanbul, c8 or v8 coverage ignore, no skipped, focused or todo test or skip option, and no failure swallowed in a hook, script or workflow (`|| true`, `; true`, `continue-on-error`, `set +e`, `HUSKY=0`, `--no-verify`, `fail_ci_if_error: false`, `--passWithNoTests`, `--coverageThreshold`, `--no-error-on-unmatched-pattern`, a `--max-warnings` budget). A marker is read where it acts: a suppression word opens a comment, a test marker stands in code, a swallowed failure stands in a shell line or in the string a script hands a shell; the same words quoted as data are no bypass. The runtime tree is judged by the same library (R236). A front end has no specs, but its source is still judged for the inline disables. `starci app check` reads the tracked tree and `starci app hygiene` (the managed pre-commit hook) the staged files from the index, so the bypass fails at the commit."
5245
+ law: "A product repository holds no bypass of a check (owner ruling loosening-always-red): no inline disable of any tool (eslint, stylelint, tslint, prettier, biome), no ts-ignore, ts-expect-error or ts-nocheck directive, no Sonar exclusion comment, no istanbul, c8 or v8 coverage ignore, no skipped, focused or todo test or skip option, and no failure swallowed in a hook, script or workflow (`|| true`, `; true`, `continue-on-error`, `set +e`, `HUSKY=0`, `--no-verify`, `fail_ci_if_error: false`, `--passWithNoTests`, `--coverageThreshold`, `--no-error-on-unmatched-pattern`, a `--max-warnings` budget). A marker is read where it acts: a suppression word opens a comment, a test marker stands in code, a swallowed failure stands in a shell line or in the string a script hands a shell; the same words quoted as data are no bypass. The runtime tree is judged by the same library (R236). A front end has no specs, but its source is still judged for the inline disables. `starci app check` reads the tracked tree and `starci app check --staged` (the managed pre-commit hook) the staged files from the index, so the bypass fails at the commit."
5246
5246
  kinds: [check]
5247
5247
  gates: [pre-commit, land, ci]
5248
5248
  failureCodes: ["HFS_BYPASS_MARKER"]
@@ -5803,7 +5803,7 @@ rules:
5803
5803
  ruling: "The lite database is self-hosted PostgreSQL in the app stack; its migrations live in db/migrations and starci app db migrate applies them."
5804
5804
  - id: R333
5805
5805
  code: "BE_ENTITY_SCHEMA_DRIFT"
5806
- law: "Every TypeORM entity column of an app's entity list (be/src/modules/platform/database/entities.ts) matches the schema its SQL migrations build: `starci app db drift` applies db/bootstrap.sql and every active db/migrations/*.sql to a scratch database (a container of the app's db image, or the empty database --url-env names), reads information_schema.columns and pg_catalog, and refuses a missing table, a missing column, a type that differs once both are normalised to PostgreSQL's format_type names (an enum by its type name), and a nullability that differs. A database column no entity maps is fine. A host that cannot provide the scratch database gets no verdict (exit 2, needs a database), never a green."
5806
+ law: "Every TypeORM entity column of an app's entity list (be/src/modules/platform/database/entities.ts) matches the schema its SQL migrations build: `starci app db drift` applies db/bootstrap.sql and every active db/migrations/*.sql to a scratch database (a container of the app's db image, or the empty database --db-url-env names), reads information_schema.columns and pg_catalog, and refuses a missing table, a missing column, a type that differs once both are normalised to PostgreSQL's format_type names (an enum by its type name), and a nullability that differs. A database column no entity maps is fine. A host that cannot provide the scratch database gets no verdict (exit 2, needs a database), never a green."
5807
5807
  kinds: [check]
5808
5808
  gates: [ci]
5809
5809
  failureCodes: ["BE_ENTITY_SCHEMA_DRIFT"]
@@ -5890,3 +5890,69 @@ rules:
5890
5890
  - {path: packages/eslint-canon-be/src/event-bus-lite.spec.ts, edition: lite, codes: [BE_EVENT_BUS_LITE_SHAPE]}
5891
5891
  witness:
5892
5892
  - {path: packages/eslint-canon-be/src/event-bus-lite.spec.ts, edition: lite, codes: [BE_EVENT_BUS_LITE_SHAPE]}
5893
+ - id: R337
5894
+ code: "HFS_AUTH_COOKIE_DOMAIN_MISSING"
5895
+ law: "In a lite app whose api uses the skeleton's passport (a tracked src/modules/integrations/passport/passport.config.ts), a docker-swarm environment whose rendered stack serves the web and the api on different hosts gives the api an AUTH_COOKIE_DOMAIN: a declared value in services.api.environment, a row of the environment's runtime/env/KEYS.md or a key sealed under its secrets/. Otherwise the refresh cookie is host-only on the api host, is absent from the Cookie header the web server forwards to the api, the api refuses the session and the user loops back to sign-in. The hosts are read from the rendered stack (web: NEXT_PUBLIC_SITE_URL, AUTH_WEB_ORIGIN, deploy.verify.http, HTTP_SECURITY_ALLOWED_ORIGINS; api: AUTH_JWT_ISSUER, GOOGLE_CALLBACK_URL, NEXT_PUBLIC_API_URL, deploy.verify.http), ports ignored, an unknown host is not judged. The finding names both hosts and their common parent domain as the value to set. A declared AUTH_COOKIE_DOMAIN that is not a bare domain (no scheme, port, path, leading dot or IP) or is not a parent of the web and the api hosts is a finding too. Same host: no key is required."
5896
+ kinds: [check]
5897
+ gates: [settle, land, ci]
5898
+ failureCodes: ["HFS_AUTH_COOKIE_DOMAIN_MISSING"]
5899
+ enforcers:
5900
+ - {kind: hfs, id: stacks-auth-cookie-domain, at: scripts/hfs/rules/stacks-auth-cookie-domain.mjs}
5901
+ editions: [lite]
5902
+ docs:
5903
+ - contexts/project/stacks.md
5904
+ proves:
5905
+ - {path: packages/hfs/src/hfs-stacks-auth-cookie-domain.spec.mjs, edition: lite}
5906
+ exemplifiedBy:
5907
+ - {path: packages/hfs/templates/app/skeleton-lite/.starcistacks, edition: lite}
5908
+ - {path: packages/hfs/templates/be/skeleton-lite/src/modules/integrations/passport/passport.config.ts, edition: lite}
5909
+ - {path: packages/hfs/src/hfs-stacks-auth-cookie-domain.spec.mjs, edition: lite}
5910
+ witness:
5911
+ - {path: packages/hfs/src/hfs-stacks-auth-cookie-domain.spec.mjs, edition: lite}
5912
+
5913
+ - id: R338
5914
+ code: "FE_GENERATED_ART_OUTSIDE_FRAME"
5915
+ law: "Generated art, a .webp or .png under the public /images/ tree, renders only through Grammar's Mascot (the character size scale sm, md, lg, hero) or inside a MediaFrame; a bare Image, img or other src-taking element drawing it takes whatever box its page gives, so the same art comes out at a different size on every screen."
5916
+ kinds: [lint]
5917
+ gates: [pre-commit, settle, land, ci]
5918
+ failureCodes: ["FE_GENERATED_ART_OUTSIDE_FRAME"]
5919
+ enforcers:
5920
+ - {kind: eslint-grammar-core, id: generated-art-in-mascot-or-media-frame}
5921
+ editions: [lite, full]
5922
+ docs: [knowledge/patterns/fe/ui.yaml#FE-UI]
5923
+ templateNa: "A template draws no generated art; the art is the app's."
5924
+ proves:
5925
+ - packages/eslint-grammar-core/src/mascot.spec.ts
5926
+ exemplifiedBy:
5927
+ - {path: packages/eslint-grammar-core/src/mascot.spec.ts, editions: [lite, full], codes: [FE_GENERATED_ART_OUTSIDE_FRAME]}
5928
+ witness:
5929
+ - {path: packages/eslint-grammar-core/src/mascot.spec.ts, editions: [lite, full], codes: [FE_GENERATED_ART_OUTSIDE_FRAME]}
5930
+ cases:
5931
+ - id: case-1
5932
+ date: "2026-10-30"
5933
+ source: owner
5934
+ quote: "Generated art renders at one consistent size across screens (relayed by the nivo-lite app)."
5935
+ ruling: "Grammar's Mascot owns the character size scale (sm 96px, md 160px, lg 240px, hero up to its cap), always contained on a transparent ground; generated art under /images/ renders through Mascot or inside MediaFrame, never a bare Image."
5936
+ - id: R339
5937
+ code: "FE_MASCOT_ASSET_SLOT_MISSING"
5938
+ law: "Every Grammar Mascot names its assetSlot (a non-empty value), tying the rendered art to the asset workflow's request; an element with a spread attribute is not judged."
5939
+ kinds: [lint]
5940
+ gates: [pre-commit, settle, land, ci]
5941
+ failureCodes: ["FE_MASCOT_ASSET_SLOT_MISSING"]
5942
+ enforcers:
5943
+ - {kind: eslint-grammar-core, id: mascot-asset-slot-required}
5944
+ editions: [lite, full]
5945
+ docs: [knowledge/patterns/fe/ui.yaml#FE-UI]
5946
+ templateNa: "A template draws no mascot; the art is the app's."
5947
+ proves:
5948
+ - packages/eslint-grammar-core/src/mascot.spec.ts
5949
+ exemplifiedBy:
5950
+ - {path: packages/eslint-grammar-core/src/mascot.spec.ts, editions: [lite, full], codes: [FE_MASCOT_ASSET_SLOT_MISSING]}
5951
+ witness:
5952
+ - {path: packages/eslint-grammar-core/src/mascot.spec.ts, editions: [lite, full], codes: [FE_MASCOT_ASSET_SLOT_MISSING]}
5953
+ cases:
5954
+ - id: case-1
5955
+ date: "2026-10-30"
5956
+ source: owner
5957
+ quote: "A Mascot reuses the provenance props of Image and MediaFrame; a Mascot without an assetSlot is a finding (relayed by the nivo-lite app)."
5958
+ ruling: "Mascot takes AssetSlotProps (assetSlot, assetSha256, assetPrompt) and emits them as data-asset-* on its root; the lint requires assetSlot on every Mascot."
@@ -51,7 +51,7 @@ versioning:
51
51
  # 3.10.0 is a minor (2026-10-29): it ADDS the lite be.platform.database.entities slot, the entity registry (slot fields role and importedBy), so the
52
52
  # tier matrix and the public-entry rules admit exactly its <c>Entities imports and its deep importers (persistence.md BE-PERSISTENCE-3).
53
53
  # 3.11.0 (ruling:lite-official-postgres-image, 2026-10-29) is, like 3.9.0, a change for lite only: it ADDS the managed app.db.image slot
54
- # (db/Dockerfile, written by `starci app sync --write`), narrows ruleParams.db.extensions to the extensions that image carries, and gives
54
+ # (db/Dockerfile, written by `starci app sync --execute`), narrows ruleParams.db.extensions to the extensions that image carries, and gives
55
55
  # @nestjs/schedule and cron one owner, platform/jobs (R335).
56
56
  # 3.13.0 (rulings lite-pgmq-bus-bullmq-jobs and lite-db-image-in-stacks, 2026-10-29) is a change for lite only: ruleParams.db.extensions
57
57
  # admits pgmq, the event bus, ruleParams.db.queueOutbox is retired with the pg_cron half of R258, the lite event-bus functions replace
@@ -940,7 +940,7 @@ slots:
940
940
 
941
941
  # ----- app root: the database of a lite app (ruling:lite-self-hosted-postgres-passport-minio): a self-hosted PostgreSQL in the app's
942
942
  # stack (the `db` service, built from the managed .starcistacks/images/db/Dockerfile: the official postgres image plus pgvector and pgmq, ruling:lite-db-image-in-stacks), migrated by
943
- # `starci app db migrate --url-env <NAME>`. A full app keeps its TypeORM migrations under be/ and has no such folder.
943
+ # `starci app db migrate --db-url-env <NAME>`. A full app keeps its TypeORM migrations under be/ and has no such folder.
944
944
  - id: app.db
945
945
  profiles: [app]
946
946
  editions: [lite]
@@ -24,7 +24,7 @@ rules:
24
24
  - id: FE-UI
25
25
  title: Compose the selected Grammar
26
26
  kind: mandatory
27
- hfsRules: [R93, R244, R313, R314, R315, R316, R317, R318, R319, R320]
27
+ hfsRules: [R93, R244, R313, R314, R315, R316, R317, R318, R319, R320, R338, R339]
28
28
  requirement: |-
29
29
  Components compose the selected Grammar through its published entry and props; application CSS carries
30
30
  layout only (css.yaml). A missing visual capability is added to that Grammar before the app consumes it.
@@ -59,6 +59,15 @@ rules:
59
59
  (owner:agent-avatar-initials-bot-badge). Apps neither declare nor import an avatar-generation
60
60
  library and draw no avatar visuals; the family owns the renderer API (the StarCi recipe is
61
61
  groupIdentity in its playbook).
62
+ - id: case-4
63
+ date: "2026-10-30"
64
+ source: owner
65
+ when: A screen shows generated character art (a mascot)
66
+ write: |-
67
+ Render it with the selected Grammar's Mascot at a step of its size scale (sm, md, lg, or hero for a split
68
+ screen), passing the asset slot it was requested under (assetSlot, and assetSha256 once delivered); a scene
69
+ or illustration is framed by MediaFrame. Generated art under /images/ never renders through a bare Image or
70
+ img (R338, R339).
62
71
  verification:
63
72
  automated:
64
73
  - FE_VENDOR_BOUNDARY
@@ -81,5 +90,7 @@ rules:
81
90
  - FE_SINGLE_COMMIT_PER_RENDER
82
91
  - FE_NO_COMMIT_IN_COLLECTION_ITEM
83
92
  - FE_CONTROL_HEIGHT
93
+ - FE_GENERATED_ART_OUTSIDE_FRAME
94
+ - FE_MASCOT_ASSET_SLOT_MISSING
84
95
  manual: []
85
96
  relatedExamples: [full-app-shell, lite-app-shell]
@@ -1,7 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import { braceVariants, globExpression } from '../lib/glob.mjs';
3
3
  import { declaredLogicOf } from './manifest-shape.mjs';
4
-
5
4
  /**
6
5
  * What a slot's `requires`, `allows` and `forbids` entries (knowledge/hfs/slots.yaml) say about a file, read through the
7
6
  * resolver of scripts/hfs/slots.mjs. The machine checks that judge "this folder holds only what its slot allows"
@@ -14,65 +13,65 @@ import { declaredLogicOf } from './manifest-shape.mjs';
14
13
  */
15
14
  const VAR = /<([A-Za-z][A-Za-z0-9-]*)>/g;
16
15
  const SPEC = /\.spec\.ts$/;
17
-
18
16
  /**
19
17
  * Whether `relative` is the unit spec `<stem>.spec.ts` of a measured file beside it that the slot names: a file the slot declares in
20
18
  * `logic`, or a `requires`/`allows` file of a logic role (ruleParams.be.logicRoles), in a slot whose coverage is required and whose tests
21
19
  * are unit-beside. The slot admits that spec without listing it, as unit-test-colocated (R47) accepts it.
22
20
  */
23
21
  function isMeasuredSpec(resolver, classified, slot, relative, named) {
24
- if (!SPEC.test(relative) || slot.coverage !== 'required' || slot.tests !== 'unit-beside') return false;
25
- const subject = relative.replace(SPEC, '.ts');
26
- if (declaredLogicOf(slot).includes(subject)) return true;
27
- const role = /\.([a-z0-9-]+)\.ts$/.exec(subject)?.[1];
28
- // An app resolver keeps no rule parameters of its own: a path on a side reads that side's.
29
- const params = classified.side ? resolver.sides?.[classified.side]?.ruleParams() : resolver.ruleParams?.();
30
- return (params?.logicRoles ?? []).includes(role) && named.some(matcher => matcher.expression.test(subject));
22
+ if (!SPEC.test(relative) || slot.coverage !== 'required' || slot.tests !== 'unit-beside')
23
+ return false;
24
+ const subject = relative.replace(SPEC, '.ts');
25
+ if (declaredLogicOf(slot).includes(subject))
26
+ return true;
27
+ const role = /\.([a-z0-9-]+)\.ts$/.exec(subject)?.[1];
28
+ // An app resolver keeps no rule parameters of its own: a path on a side reads that side's.
29
+ const params = classified.side ? resolver.sides?.[classified.side]?.ruleParams() : resolver.ruleParams?.();
30
+ return (params?.logicRoles ?? []).includes(role) && named.some((matcher) => matcher.expression.test(subject));
31
31
  }
32
-
33
32
  function matchersOf(entries, bindings) {
34
- return (entries ?? []).flatMap(entry => {
35
- const directory = entry.endsWith('/');
36
- const body = directory ? entry.slice(0, -1) : entry;
37
- const filled = body.replace(VAR, (whole, name) => bindings[name] ?? '*');
38
- return braceVariants(filled).map(variant => ({ entry, directory, expression: globExpression(directory ? `${variant}/**` : variant) }));
39
- });
33
+ return (entries ?? []).flatMap((entry) => {
34
+ const directory = entry.endsWith('/');
35
+ const body = directory ? entry.slice(0, -1) : entry;
36
+ const filled = body.replace(VAR, (_whole, name) => bindings[name] ?? '*');
37
+ return braceVariants(filled).map((variant) => ({ entry, directory, expression: globExpression(directory ? `${variant}/**` : variant) }));
38
+ });
40
39
  }
41
-
42
40
  /** The path of `file` below the instance root of the slot that owns it. */
43
41
  export function relativeToRoot(file, root) {
44
- return root && file.startsWith(`${root}/`) ? file.slice(root.length + 1) : path.posix.basename(file);
42
+ return root && file.startsWith(`${root}/`) ? file.slice(root.length + 1) : path.posix.basename(file);
45
43
  }
46
-
47
44
  /**
48
45
  * Whether the slot that owns `file` names it in `requires` or `allows` (and does not forbid it); null when no enabled slot
49
46
  * owns it. Unlike allowsFile it judges a slot that lists only `requires` too, so a folder closed by its slot (an app's
50
47
  * src/) is read from the slot and never from a check's own list.
51
48
  */
52
49
  export function slotAdmitsFile(resolver, file) {
53
- const classified = resolver.classifyPath(file);
54
- if (classified.status === 'no-slot' || classified.status === 'ambiguous' || !classified.slot) return null;
55
- const slot = resolver.slot(classified.slot);
56
- const relative = relativeToRoot(classified.path, classified.root);
57
- const named = matchersOf([...(slot.requires ?? []), ...(slot.allows ?? [])], classified.bindings);
58
- const hit = named.some(matcher => matcher.expression.test(relative)) || isMeasuredSpec(resolver, classified, slot, relative, named);
59
- const forbidden = matchersOf(slot.forbids, classified.bindings).some(matcher => matcher.expression.test(relative) || matcher.expression.test(`${relative}/`));
60
- return hit && !forbidden;
50
+ const classified = resolver.classifyPath(file);
51
+ if (classified.status === 'no-slot' || classified.status === 'ambiguous' || !classified.slot)
52
+ return null;
53
+ const slot = resolver.slot(classified.slot);
54
+ const relative = relativeToRoot(classified.path, classified.root);
55
+ const named = matchersOf([...(slot.requires ?? []), ...(slot.allows ?? [])], classified.bindings);
56
+ const hit = named.some(matcher => matcher.expression.test(relative)) || isMeasuredSpec(resolver, classified, slot, relative, named);
57
+ const forbidden = matchersOf(slot.forbids, classified.bindings).some(matcher => matcher.expression.test(relative) || matcher.expression.test(`${relative}/`));
58
+ return hit && !forbidden;
61
59
  }
62
-
63
60
  /**
64
61
  * {slot, root, relative, allowed, entry?, forbiddenBy?} for a repository-relative file, or null when no enabled slot owns
65
62
  * it or the slot names no `allows`. `allowed` is true when the relative path matches a `requires` or `allows` entry.
66
63
  */
67
64
  export function allowsFile(resolver, file) {
68
- const classified = resolver.classifyPath(file);
69
- if (classified.status === 'no-slot' || classified.status === 'ambiguous' || !classified.slot) return null;
70
- const slot = resolver.slot(classified.slot);
71
- if (!slot?.allows?.length) return null;
72
- const relative = relativeToRoot(classified.path, classified.root);
73
- const named = matchersOf([...(slot.requires ?? []), ...slot.allows], classified.bindings);
74
- const hit = named.find(matcher => matcher.expression.test(relative));
75
- const spec = !hit && isMeasuredSpec(resolver, classified, slot, relative, named);
76
- const forbidden = matchersOf(slot.forbids, classified.bindings).find(matcher => matcher.expression.test(relative) || matcher.expression.test(`${relative}/`));
77
- return { slot: slot.id, root: classified.root, relative, allowed: (Boolean(hit) || spec) && !forbidden, entry: hit?.entry, forbiddenBy: forbidden?.entry, allows: slot.allows };
65
+ const classified = resolver.classifyPath(file);
66
+ if (classified.status === 'no-slot' || classified.status === 'ambiguous' || !classified.slot)
67
+ return null;
68
+ const slot = resolver.slot(classified.slot);
69
+ if (!slot?.allows?.length)
70
+ return null;
71
+ const relative = relativeToRoot(classified.path, classified.root);
72
+ const named = matchersOf([...(slot.requires ?? []), ...slot.allows], classified.bindings);
73
+ const hit = named.find(matcher => matcher.expression.test(relative));
74
+ const spec = !hit && isMeasuredSpec(resolver, classified, slot, relative, named);
75
+ const forbidden = matchersOf(slot.forbids, classified.bindings).find(matcher => matcher.expression.test(relative) || matcher.expression.test(`${relative}/`));
76
+ return { slot: slot.id, root: classified.root, relative, allowed: (Boolean(hit) || spec) && !forbidden, entry: hit?.entry, forbiddenBy: forbidden?.entry, allows: slot.allows };
78
77
  }