@starci/eslint-canon-fe 8.4.33 → 8.4.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # Changelog
2
2
 
3
+ ## 8.4.34 - 2026-10-23
4
+ - Changed: the bundled canon pins select @starci/hfs 4.7.28 and @starci/cli 1.7.28; the bundled rule catalog drops the per-rule markers `specNa`, `exampleNa` and the unreachable witness.
5
+
3
6
  ## 8.4.33 - 2026-10-23
4
7
  - Changed: the bundled canon pins select @starci/hfs 4.7.27 and @starci/cli 1.7.27.
5
8
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starci/eslint-canon-fe",
3
- "version": "8.4.33",
3
+ "version": "8.4.34",
4
4
  "description": "The StarCi front-end lint canon: every rule its laws publish, gathered as one flat-config plugin and attached by one factory.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -4,7 +4,7 @@
4
4
  // rather than importing node_modules; yaml is a devDependency and a direct import breaks that
5
5
  // install. tests/engine/yaml-vendored-parity.spec.mjs proves the vendored parseYaml/stringifyYaml behave
6
6
  // exactly like yaml@2.9.0 with these options for every tracked *.yaml/*.yml file (parsed values
7
- // deep-equal, stringify output byte-identical). License: engine/yaml-license.json.
7
+ // deep-equal, stringify output byte-identical). License: engine/yaml-license.yaml.
8
8
  // Rebuild only when deliberately upgrading yaml. The entry source is:
9
9
  // import * as YAML from 'yaml';
10
10
  // export function parseYaml(source) {
@@ -29,14 +29,14 @@ pins:
29
29
  source: packages/grammar/package.json
30
30
  why: 'the grammar front ends read: the brand layer sets `--font-sans` and `--font-mono` and the grammar reads them; it carries the Input tel kind, the copyable and read-only Input, the IconButton disclosure props and count badge, ChatBubble, FillViewport, Textarea mentions and Enter-to-send, the leading PrimaryRailLayout rail, the plate-less Image, ListRow, the soft Alert, Progress steps, the floating primary IconButton, FlowGraph, the STATE-8 loading views and the bordered selectable TagGroup chips. The runtime source version wins for a @starci package.'
31
31
  '@starci/eslint-canon-be':
32
- version: 3.4.34
32
+ version: 3.4.35
33
33
  group: starci
34
34
  install: registry
35
35
  side: be
36
36
  source: packages/eslint/be/package.json
37
37
  why: 'the back-end ESLint canon: `loadHfs(import.meta.url)` of be/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the be side, and the project graph is built per side; its bundled runtime copies (slots, canon-pins, failure codes, the architecture machine) put .starcistacks and .sops.yaml at the app root; `starciBeConfig({ hfs: loadHfs(import.meta.url) })` is the typed factory and the BE-CONVENTION laws are its rules.'
38
38
  '@starci/eslint-canon-fe':
39
- version: 8.4.33
39
+ version: 8.4.34
40
40
  group: starci
41
41
  install: registry
42
42
  side: fe
@@ -76,14 +76,14 @@ pins:
76
76
  source: packages/test-world/package.json
77
77
  why: 'the shared e2e library of every back end (R47, R48), a devDependency of every back end: the warm stack behind toxiproxy, the network-edge fakes, the Nest boot, the typed useTestWorld handle, useSandbox for contract specs and the outage lock. Kafka is real own infra (the one digest-pinned apache/kafka KRaft image, a broker listener and proxy per slot, slot-prefixed topics, consumer groups and client ids) and Postgres is schema-per-context (a schema and a login role per context in a shared database). Each worker has its own data slot (<ns>_w<k>) with its own databases, Keycloak realm, leased Redis DB, bucket/collection/topic prefixes, fakes host, toxiproxy proxies and outage lock; a run provisions min(jest workers, workers cap, default 2) slots, and state file protocol 2 pairs exactly with @starci/jest-preset (TEST_WORLD_PAIR_MISMATCH otherwise). A modules world boots real peer apps beside its modules, world.apps.<name>.during(fn) is the outage of a peer app, w.keycloak.clientSecret(client) answers the run-generated secret of a confidential realm client, world.keycloak.events/sessions read the realm user events and live sessions, world.infra.postgresql.connection(name) takes one database connection down while the others serve, and world.resolve and scope.resolve take any Nest token. Every path a declaration names (`stack`, seeds, the realm, Dockerfiles) resolves from the app root (the directory of hfs.json); the globalSetup registers the path aliases as TypeScript resolves them. Its CLI main is exported at ./cli for starci app stack.'
78
78
  '@starci/cli':
79
- version: 1.7.27
79
+ version: 1.7.28
80
80
  group: starci
81
81
  install: registry
82
82
  side: both
83
83
  source: packages/cli/package.json
84
84
  why: 'the only StarCi command package; product repositories install it for starci app, while @starci/hfs remains its pinned transitive implementation.'
85
85
  '@starci/hfs':
86
- version: 4.7.27
86
+ version: 4.7.28
87
87
  group: starci
88
88
  install: registry
89
89
  side: both
@@ -538,6 +538,7 @@ slots:
538
538
  profiles: [app]
539
539
  exemplifiedBy:
540
540
  - packages/hfs/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml
541
+ - {path: packages/hfs/src/hfs-release-provenance.spec.mjs, edition: full}
541
542
  editionWhy: "ruling:R212/case-2 (with the Work boundary amended by ruling:R212/case-4): lite manages its Supabase database deploy workflow; full owns an optional provider-specific workflow."
542
543
  # The database deploy workflow: workflow_dispatch only (like app.ci-e2e); starci app sync renders it under the managed ci group.
543
544
  path: ".github/workflows/db-deploy.yml"
@@ -548,7 +549,6 @@ slots:
548
549
  liteManagedBy: ci-workflows-lite
549
550
  rules: [HFS_MANAGED_FILE_DRIFT]
550
551
  since: 2.1.0
551
- exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
552
552
  - id: app.ci-stack-deploy
553
553
  since: 3.3.0
554
554
  profiles: [app]
@@ -687,7 +687,8 @@ slots:
687
687
  tier: none
688
688
  tests: none
689
689
  rules: [HFS_PLAINTEXT_SECRET, HFS_SCRIPT_ONE_OFF, BE_SPEC_PLACEMENT, HFS_REPO_LOCAL_CHECK]
690
- exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
690
+ exemplifiedBy:
691
+ - {path: packages/hfs/src/hfs-slot-ownership.spec.mjs, edition: lite}
691
692
  - id: app.sides
692
693
  since: 3.1.0
693
694
  profiles: [app]
@@ -810,6 +811,7 @@ slots:
810
811
  profiles: [app]
811
812
  exemplifiedBy:
812
813
  - packages/hfs/templates/app/skeleton-lite/supabase/
814
+ - {path: packages/hfs/src/hfs-database-access.spec.mjs, edition: full}
813
815
  editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): Supabase is required in lite; full enables it only through a declared connection."
814
816
  # The one Supabase project directory, at the app root beside be/, fe/ and .starcistacks (never under a side).
815
817
  path: "supabase/"
@@ -820,11 +822,11 @@ slots:
820
822
  tier: none
821
823
  tests: none
822
824
  since: 2.1.0
823
- exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
824
825
  - id: app.supabase.config
825
826
  profiles: [app]
826
827
  exemplifiedBy:
827
828
  - packages/hfs/templates/app/skeleton-lite/supabase/config.toml
829
+ - {path: packages/hfs/src/db-config-policy.spec.mjs, edition: full}
828
830
  editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite requires Supabase configuration; full requires it only when Supabase is declared."
829
831
  # The project configuration: auth keys are env(NAME) references, never literals (L08 judges its policy keys).
830
832
  path: "supabase/config.toml"
@@ -835,11 +837,11 @@ slots:
835
837
  tier: none
836
838
  tests: none
837
839
  since: 2.1.0
838
- exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
839
840
  - id: app.supabase.migrations
840
841
  profiles: [app]
841
842
  exemplifiedBy:
842
843
  - packages/hfs/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql
844
+ - {path: packages/hfs/src/db-migration-shape.spec.mjs, edition: full}
843
845
  editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite uses Supabase schema authority and requires a migration; full opts into that provider."
844
846
  # The schema authority of an app on Supabase: forward-only SQL migrations named <timestamp14>_<kebab-name>.sql,
845
847
  # one file per change, immutable once on origin/main (L02-L08 judge them).
@@ -852,7 +854,6 @@ slots:
852
854
  tier: none
853
855
  tests: none
854
856
  since: 2.1.0
855
- exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
856
857
  - id: app.supabase.migrations.archive
857
858
  profiles: [app]
858
859
  # The migrations an existing app retired at its cutover (owner ruling 2026-10-13, migration baseline policy): the live schema is
@@ -888,7 +889,8 @@ slots:
888
889
  tier: none
889
890
  tests: none
890
891
  since: 2.1.0
891
- exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
892
+ exemplifiedBy:
893
+ - {path: packages/hfs/src/db-types-returned-collector.spec.mjs, edition: full}
892
894
  - id: app.supabase.templates
893
895
  profiles: [app]
894
896
  path: "supabase/templates/*.html"
@@ -1025,7 +1027,8 @@ slots:
1025
1027
  tests: none
1026
1028
  coverage: none
1027
1029
  rules: [HFS_UNUSED_SYMBOL]
1028
- exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
1030
+ exemplifiedBy:
1031
+ - {path: packages/hfs/src/hfs-unused-symbol-ledger.spec.mjs, editions: [lite, full]}
1029
1032
  - id: repo.packages
1030
1033
  since: 3.1.0
1031
1034
  profiles: [be, fe]
@@ -1183,8 +1186,9 @@ slots:
1183
1186
  tests: none
1184
1187
  coverage: none
1185
1188
  why: the MCP tool catalog an app's MCP server publishes at one contract major (`<server>@<major>`), the `tools/list` result its clients rely on, committed as a cross-repo interface; the canon emits no MCP catalog, so its shape is judged (R302 HFS_MCP_TOOLS_SNAPSHOT)
1186
- exampleNa: "owner:mcp-tools-snapshot-no-example (2026-10-21): no example app serves an MCP server; the package spec judges the shape"
1187
1189
  rules: [HFS_MCP_TOOLS_SNAPSHOT]
1190
+ exemplifiedBy:
1191
+ - {path: packages/hfs/src/mcp-tools-snapshot.spec.mjs, editions: [lite, full]}
1188
1192
 
1189
1193
  # ----- BE: apps (one slot per app kind; a new kind = a new slot, minor bump) ---------------------------
1190
1194
  # The files of every app (owner ruling 2026-10-14, R245 and R246, since 3.1.0), each with a line budget that
@@ -1449,7 +1453,8 @@ slots:
1449
1453
  coverage: none
1450
1454
  rules: [BE_KIND_ISOLATION, BE_FEATURE_IMPORTS_FEATURE, BE_FEATURE_NOT_COMPOSED]
1451
1455
  since: 1.0.0
1452
- exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
1456
+ exemplifiedBy:
1457
+ - {path: packages/hfs/src/hfs-stacks-swarm.spec.mjs, edition: lite}
1453
1458
  - id: be.feature.realtime.graphql
1454
1459
  profiles: [be]
1455
1460
  editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite uses only the in-process socket.io gateway; the GraphQL subscription door remains full-only."
@@ -1653,7 +1658,8 @@ slots:
1653
1658
  coverage: required
1654
1659
  budget: {indexExports: 60}
1655
1660
  rules: [BE_TIER_DIRECTION, ARCH_OWNER_CYCLE, BE_MODULE_SHAPE, BE_INFRA_OWNER]
1656
- exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
1661
+ exemplifiedBy:
1662
+ - {path: packages/eslint/be/webhook-handoff-inbox.spec.mjs, edition: lite}
1657
1663
  - id: be.platform.queue
1658
1664
  since: 3.1.0
1659
1665
  profiles: [be]
@@ -1792,7 +1798,8 @@ slots:
1792
1798
  tests: unit-beside # a measured slot: <name>.projection.spec.ts beside its projection is legal (the file owes 100 per file; a spec of its own is optional)
1793
1799
  coverage: required
1794
1800
  rules: [BE_PROJECTION_WRITE_OWNER, BE_PROJECTION_SHAPE, BE_TIER_DIRECTION, BE_CONTEXT_COUPLING]
1795
- exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
1801
+ exemplifiedBy:
1802
+ - {path: packages/eslint/be/jobs-projections-coverage.spec.mjs, edition: lite}
1796
1803
  - id: be.integrations
1797
1804
  since: 3.1.0
1798
1805
  profiles: [be]
@@ -1822,6 +1829,7 @@ slots:
1822
1829
  profiles: [be]
1823
1830
  exemplifiedBy:
1824
1831
  - packages/hfs/templates/be/skeleton-lite/src/modules/integrations/supabase/
1832
+ - {path: packages/hfs/src/be-supabase-jwt-verified.spec.mjs, edition: full}
1825
1833
  editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite requires the Supabase Auth and Storage integration; full opts in through a connection."
1826
1834
  # The only be import of @supabase/* (L10): the Auth admin and Storage admin client, plus the JWKS-based verification of
1827
1835
  # the Supabase access token the guard chain reads (L16). The connection itself is ordinary Postgres (the shared
@@ -1839,7 +1847,6 @@ slots:
1839
1847
  rules: [BE_TIER_DIRECTION, BE_ERROR_HOME, BE_SECRET_DEFAULT]
1840
1848
  coverage: required
1841
1849
  since: 2.1.0
1842
- exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
1843
1850
 
1844
1851
  # ----- BE: tests (owner test layout 2026-09-30). Unit specs are <name>.service.spec.ts beside their service and nothing else; everything else
1845
1852
  # lives under src/tests/, one folder per kind, and the folder and the file suffix always agree (a file whose suffix does
@@ -1872,7 +1879,6 @@ slots:
1872
1879
  overridden in the DI container. It is the only test location that may import typeorm's DataSource or testcontainers,
1873
1880
  call migrate/runMigrations/synchronize, or write process.env.
1874
1881
  rules: [BE_TEST_TOPOLOGY, BE_TEST_CONSTRAINTS_ON]
1875
- exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
1876
1882
 
1877
1883
  - id: be.tests.world.kit
1878
1884
  since: 3.1.0
@@ -2269,7 +2275,8 @@ slots:
2269
2275
  tests: none
2270
2276
  why: the canonical GraphQL client output of codegen is local and never committed; this specific slot overrides the tracked API parent
2271
2277
  rules: [FE_WIRE_GENERATED]
2272
- exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
2278
+ exemplifiedBy:
2279
+ - {path: packages/hfs/src/r52-wire-generated.spec.mjs, editions: [lite, full]}
2273
2280
  - id: fe.transport.client
2274
2281
  since: 3.1.0
2275
2282
  profiles: [fe]
@@ -209,7 +209,7 @@ export function checkBackendSupabase(input) {
209
209
  };
210
210
  }
211
211
 
212
- function checkJwtPolicy(input) {
212
+ export function checkJwtPolicy(input) {
213
213
  const kit = machineKit(input);
214
214
  const violations = [];
215
215
  let verifyCalls = 0;
@@ -84,7 +84,7 @@ export const MANIFEST_KINDS = [APP_KIND, RUNTIME_KIND];
84
84
  export const manifestKind = (m) => (isPlainObject(m) && m.kind !== undefined ? m.kind : APP_KIND);
85
85
  const strList = (v) => stringList(v);
86
86
 
87
- const APP_SLOT_KEYS = ['id', 'profiles', 'path', 'presence', 'tracked', 'tier', 'tests', 'owner', 'appKind', 'minInstances', 'requiredWhen', 'requiredInstances', 'requires', 'pattern', 'trigger', 'allows', 'forbids', 'layers', 'kinds', 'roles', 'composedBy', 'budget', 'managedBy', 'liteManagedBy', 'rules', 'goesTo', 'why', 'since', 'perConnection', 'parent', 'editions', 'editionWhy', 'litePresence', 'lite', 'provider', 'coverage', 'entries', 'outcomeHome', 'templateNa', 'exampleNa', 'exemplifiedBy'];
87
+ const APP_SLOT_KEYS = ['id', 'profiles', 'path', 'presence', 'tracked', 'tier', 'tests', 'owner', 'appKind', 'minInstances', 'requiredWhen', 'requiredInstances', 'requires', 'pattern', 'trigger', 'allows', 'forbids', 'layers', 'kinds', 'roles', 'composedBy', 'budget', 'managedBy', 'liteManagedBy', 'rules', 'goesTo', 'why', 'since', 'perConnection', 'parent', 'editions', 'editionWhy', 'litePresence', 'lite', 'provider', 'coverage', 'entries', 'outcomeHome', 'templateNa', 'exemplifiedBy'];
88
88
  /** The fields a slot's `lite` overlay may hold: the same keys it would carry in the slot body, resolved under edition lite. */
89
89
  const LITE_OVERLAY_KEYS = ['path', 'requires', 'allows', 'forbids', 'minInstances', 'requiredInstances', 'why'];
90
90
  /** A runtime slot has no app kind, side composition, layer or managed template; it may name the generator of a generated copy. */
@@ -218,8 +218,6 @@ function slotCoverageEvidenceProblems(slot, at) {
218
218
  const bad = [];
219
219
  if (slot.templateNa !== undefined && !(typeof slot.templateNa === 'string' && slot.templateNa.trim()))
220
220
  bad.push(`${at}: templateNa must be non-empty text`);
221
- if (slot.exampleNa !== undefined && !(typeof slot.exampleNa === 'string' && slot.exampleNa.trim()))
222
- bad.push(`${at}: exampleNa must be non-empty text`);
223
221
  if (slot.exemplifiedBy === undefined) return bad;
224
222
  const refs = slot.exemplifiedBy;
225
223
  const valid = Array.isArray(refs) && refs.length > 0 && refs.every(slotEvidenceRefValid)
@@ -17,7 +17,13 @@ const RULE_KINDS = Object.freeze(['codemod', 'lint', 'check', 'design']);
17
17
  const FINDING_CODE = /^[A-Z][A-Z0-9]*(_[A-Z0-9]+)+$/;
18
18
  const ENFORCER_ID = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
19
19
  const FILE_ENFORCERS = new Set(['machine', 'hfs', 'work-validate', 'sonar', 'runtime']);
20
- const RULE_KEYS = new Set(['id', 'code', 'law', 'scope', 'kinds', 'gates', 'failureCodes', 'editions', 'codeEditions', 'enforcers', 'cases', 'docs', 'exemplifiedBy', 'proves', 'witness', 'editionWhy', 'amendedBy', 'templateNa', 'specNa', 'exampleNa']);
20
+ /**
21
+ * The enforcer kinds that run only inside the StarCi runtime repository (`starci runtime check`, `starci work`): no published package carries
22
+ * them, so no package spec can hold them (owner ruling no-runtime-specs: the runtime has no specs of its own; the package clean-test and the
23
+ * scaffold e2e are its tests), and no example app is a product they judge. A rule is runtime-judged when every enforcer it has is of these kinds.
24
+ */
25
+ export const RUNTIME_ONLY_ENFORCERS = Object.freeze(['work-validate', 'runtime']);
26
+ const RULE_KEYS = new Set(['id', 'code', 'law', 'scope', 'kinds', 'gates', 'failureCodes', 'editions', 'codeEditions', 'enforcers', 'cases', 'docs', 'exemplifiedBy', 'proves', 'witness', 'editionWhy', 'amendedBy', 'templateNa']);
21
27
  const CASE_KEYS = new Set(['id', 'date', 'source', 'ruling', 'quote', 'supersedes', 'status', 'staleWhen', 'amendedBy']);
22
28
  const CASE_DATE = /^\d{4}-\d{2}-\d{2}$/;
23
29
  const ENFORCER_KEYS = new Set(['kind', 'id', 'status', 'at', 'editions', 'editionWhy']);
@@ -116,13 +122,6 @@ function witnessProblems(rule, label, bad) {
116
122
  const value = rule.witness;
117
123
  if (value === undefined) return;
118
124
  const at = `${label}.witness`;
119
- if (isPlainObject(value)) {
120
- unknownKeys(value, new Set(['status', 'reason', 'date']), at, bad);
121
- if (value.status !== 'unreachable') bad.push(`${at}.status must be unreachable`);
122
- textProblem(value.reason, `${at}.reason`, bad);
123
- dateProblem(value.date, `${at}.date`, bad);
124
- return;
125
- }
126
125
  if (!nonEmptyList(value, at, bad)) return;
127
126
  value.forEach((item, index) => witnessEntryProblems(item, `${at}[${index}]`, bad));
128
127
  }
@@ -283,8 +282,6 @@ function checkRule(rule, index, catalog, codeOwner, bad) {
283
282
  if (rule.amendedBy !== undefined) caseReferenceProblems(rule.amendedBy, `${label}.amendedBy`, bad);
284
283
  optionalText(rule, 'editionWhy', label, bad);
285
284
  optionalText(rule, 'templateNa', label, bad);
286
- optionalText(rule, 'specNa', label, bad);
287
- optionalText(rule, 'exampleNa', label, bad);
288
285
  for (const key of ['docs', 'exemplifiedBy', 'proves']) evidenceProblems(rule, key, label, bad);
289
286
  witnessProblems(rule, label, bad);
290
287
  enumListProblems(rule, label, 'kinds', RULE_KINDS, bad);