@starci/eslint-canon-fe 8.0.5 → 8.0.6

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,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 8.0.6 - 2026-10-01
4
+
5
+ - Changed: the runtime api layer move and the new pins.
6
+
3
7
  ## 8.0.5 - 2026-10-01
4
8
 
5
9
  - Changed: bundled slots.yaml, sonar-gate.yaml and canon-pins.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starci/eslint-canon-fe",
3
- "version": "8.0.5",
3
+ "version": "8.0.6",
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": "UNLICENSED",
@@ -31,14 +31,14 @@ pins:
31
31
  source: packages/grammar/package.json
32
32
  why: nivo-fe pins 0.4.11 and 0.5.0 in one workspace, starci-next-fe 0.5.1, miamia-fe 0.5.0; the runtime source is 0.8.0 (the brand layer sets `--font-sans` and `--font-mono`; the grammar reads them; 0.7.2 added the Input tel kind and IconButton disclosure props).
33
33
  '@starci/eslint-canon-be':
34
- version: 3.0.5
34
+ version: 3.0.6
35
35
  group: starci
36
36
  install: registry
37
37
  side: be
38
38
  source: packages/eslint/be/package.json
39
39
  why: '3.0.3: its bundled canon-pins copy pins stylelint-canon 2.0.2 and hfs 4.0.3; no rule changed. 3.0.2: its bundled runtime copies (slots, canon-pins, failure codes, the architecture machine) put .starcistacks and .sops.yaml at the app root (app.starcistacks, app.sops) and pin hfs 4.0.2. 3.0.1: its bundled canon-pins copy pins hfs 4.0.1. 3.0.0: `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; the project graph is built per side. 2.0.0 (C0 release): starciBeConfig({ hfs: loadHfs(import.meta.url) }) typed factory and the BE-CONVENTION laws.'
40
40
  '@starci/eslint-canon-fe':
41
- version: 8.0.5
41
+ version: 8.0.6
42
42
  group: starci
43
43
  install: registry
44
44
  side: fe
@@ -71,14 +71,14 @@ pins:
71
71
  source: packages/jest-preset/package.json
72
72
  why: '2.2.2: the unit run writes the lcov Sonar and Codecov import (services only); 2.2.0: 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; 2.1.0: four projects on the test world, the unit kit (mockEntityManager, fakeTransaction, fakeIds, FakeClock, Outcome matchers) and the recordingOutbox claim side.'
73
73
  '@starci/test-world':
74
- version: 1.0.3
74
+ version: 1.0.5
75
75
  group: starci
76
76
  install: registry
77
77
  side: be
78
78
  source: packages/test-world/package.json
79
- why: '1.0.3: the globalSetup registers the path aliases as TypeScript resolves them (the extends chain, paths from the config that declares them, the effective baseUrl), so a tests tsconfig that only extends the side config loads the declaration. 1.0.2: every path a declaration names (`stack`, seeds, the realm, Dockerfiles) resolves from the app root (the directory of hfs.json), where .starcistacks lives, never from the be side. The shared e2e library of every back end (R47, R48): the warm stack behind toxiproxy, the network-edge fakes, the Nest boot, the typed useTestWorld handle, useSandbox for contract specs, the outage lock and the starci-test-stack bin behind the managed test:stack script; a devDependency of every back end. 1.0.1: world.keycloak.events/sessions (the user events of the realm and live sessions through the admin API) and world.infra.postgresql.connection(name) (the database of one connection down while the others serve, under the outage lock).'
79
+ why: '1.0.5: a modules world boots real peer apps beside its modules, world.apps.<name>.during(fn) is the outage of a peer app, and w.keycloak.clientSecret(client) answers the run-generated secret of a confidential realm client. 1.0.4: world.resolve and scope.resolve take any Nest token (class, string or symbol). 1.0.3: the globalSetup registers the path aliases as TypeScript resolves them (the extends chain, paths from the config that declares them, the effective baseUrl), so a tests tsconfig that only extends the side config loads the declaration. 1.0.2: every path a declaration names (`stack`, seeds, the realm, Dockerfiles) resolves from the app root (the directory of hfs.json), where .starcistacks lives, never from the be side. The shared e2e library of every back end (R47, R48): the warm stack behind toxiproxy, the network-edge fakes, the Nest boot, the typed useTestWorld handle, useSandbox for contract specs, the outage lock and the starci-test-stack bin behind the managed test:stack script; a devDependency of every back end. 1.0.1: world.keycloak.events/sessions (the user events of the realm and live sessions through the admin API) and world.infra.postgresql.connection(name) (the database of one connection down while the others serve, under the outage lock).'
80
80
  '@starci/hfs':
81
- version: 4.0.5
81
+ version: 4.0.6
82
82
  group: starci
83
83
  install: registry
84
84
  side: both
@@ -818,9 +818,9 @@ slots:
818
818
  tier: integrations
819
819
  owner: true
820
820
  requires: [index.ts, "<provider>.config.ts"]
821
- tests: unit-beside # <name>.service.spec.ts beside each <name>.service.ts, nothing else
821
+ tests: unit-beside # <name>.service.spec.ts beside each <name>.service.ts; plus src/tests/integration/<provider>/ (R112)
822
822
  budget: {indexExports: 60}
823
- rules: [BE_TIER_DIRECTION, BE_ERROR_HOME, BE_SECRET_DEFAULT]
823
+ rules: [BE_TIER_DIRECTION, BE_ERROR_HOME, BE_SECRET_DEFAULT, BE_INTEGRATION_SPEC_MISSING]
824
824
  - id: be.integrations.model
825
825
  profiles: [be]
826
826
  path: "src/modules/integrations/<provider>/model/"
@@ -897,8 +897,8 @@ slots:
897
897
  tracked: tracked
898
898
  tier: e2e
899
899
  tests: e2e
900
- why: one capability module on the real database, no HTTP (SQL, transactions, concurrency, inbox claims) through useTestWorld({ modules }); run by test:integration
901
- rules: [BE_TEST_TOPOLOGY]
900
+ why: one capability module through useTestWorld({ modules }), no HTTP door of ours (SQL, transactions, concurrency, inbox claims on the real database; and every integration client, `<capability>` being its provider folder, against our real infra, a real peer app or the library fake at the network edge, with its success, its ErrorCode refusal mapping and an outage through the world, R112); run by test:integration
901
+ rules: [BE_TEST_TOPOLOGY, BE_INTEGRATION_SPEC_MISSING]
902
902
  - id: be.tests.e2e
903
903
  profiles: [be]
904
904
  path: "src/tests/e2e/<area>/*.e2e-spec.ts"
@@ -1,11 +1,11 @@
1
- // git.mjs — the one spawn the runtime's guard/kernel scripts run git through.
1
+ // scripts/api/git/lib.mjs — the one place the runtime spawns git (scripts/checks/check-layers.mjs enforces it).
2
2
  //
3
- // Every copy spelt the same options by hand - encoding:'utf8', windowsHide:true, sometimes a
4
- // timeout - with two shapes: `git args` in a cwd (shim's pathspec scans, footprint-scan,
5
- // safe-remove's prune) and `git -C dir args` (settle-landed, install, terminal-dedupe's worktree
6
- // list). gitOutput is the throwing shape (stdout text, or an Error on a non-zero exit). gitSpawn keeps the spawnSync(file, args, options) signature so an injected runner or a
7
- // spec's fake takes the same three arguments; runGit is the `-C` convenience; gitResult folds the
8
- // result into settle-landed's {ok, stdout, error} envelope.
3
+ // Every caller spelt the same options by hand - encoding:'utf8', windowsHide:true, sometimes a timeout - with two shapes:
4
+ // `git args` in a cwd and `git -C dir args`. gitOutput is the throwing shape (stdout text, or an Error on a non-zero exit).
5
+ // gitSpawn keeps the spawnSync(file, args, options) signature so an injected runner or a spec's fake takes the same three
6
+ // arguments; runGit is the `-C` convenience; gitResult folds the result into the {ok, stdout, error} envelope; gitRunner
7
+ // folds any caller's runner into {ok, stdout, stderr}. The call files beside this one (worktree-*.mjs, rev-parse.mjs, ...)
8
+ // each name one git verb.
9
9
  import { spawnSync } from 'node:child_process';
10
10
 
11
11
  /**
@@ -43,11 +43,10 @@ export function gitResult(args, options = {}) {
43
43
  }
44
44
 
45
45
  /**
46
- * git's C-quoted diff path decoded - `"b\303\251"` a `+++ ` header line prints when core.quotePath
47
- * covers the name. Octal escapes become \u00XX for JSON.parse (which answers \n, \t, \" and \\);
48
- * a value not wrapped in quotes passes through unchanged.
46
+ * A caller's git runner folded to (args, opts) -> {ok, stdout, stderr}. `git`: the caller's runner (args, {cwd}) ->
47
+ * {ok|status, stdout|out, stderr|err} (a spec's fake); null: runGit with a 5-minute timeout and a 64 MB buffer.
49
48
  */
50
- export const unquoteDiffPath = (value) =>
51
- /^".*"$/.test(value)
52
- ? JSON.parse(value.replace(/\\([0-7]{3})/g, (_, octal) => `\\u00${Number.parseInt(octal, 8).toString(16).padStart(2, '0')}`))
53
- : value;
49
+ export const gitRunner = (git = null) => (args, opts = {}) => {
50
+ const r = git ? git(args, opts) : runGit(args, { timeout: 300_000, maxBuffer: 64 * 1024 * 1024, ...opts });
51
+ return { ok: r?.ok ?? (!r?.error && r?.status === 0), stdout: String(r?.stdout ?? r?.out ?? '').trim(), stderr: String(r?.stderr ?? r?.err ?? r?.error?.message ?? r?.error ?? '').trim() };
52
+ };
@@ -18,8 +18,12 @@
18
18
  * used by the importers of that file; a re-exporting file nobody imports is a framework entry (a route file) and uses it.
19
19
  * Specs and tests are not in the graph, so an export used only by a spec is dead, on purpose, with one exception (unit test
20
20
  * standard): a `<name>.service.spec.ts` (the only unit spec kind) and a `*.builder.ts` under src/tests/fixtures/builders read from
21
- * disk count as consumers, because a spec can only provide an Inject*() token or a param type the entry exports. Specs of any
22
- * other kind, e2e specs and world files still do not count. A consumer inside the owner is skipped like any inside consumer.
21
+ * disk count as consumers, because a spec can only provide an Inject*() token or a param type the entry exports. An integration
22
+ * spec (slot be.tests.integration, `src/tests/integration/<capability>/*.integration-spec.ts`) counts as a consumer of exactly one
23
+ * owner: the integration (slot be.integrations, `src/modules/integrations/<provider>/`) whose provider folder is its capability
24
+ * folder, because R112 makes that spec register the integration module and reference its ErrorCode enum. Specs of any other kind,
25
+ * an integration spec importing another owner, e2e specs and world files still do not count. A consumer inside the owner is
26
+ * skipped like any inside consumer.
23
27
  */
24
28
  import fs from 'node:fs';
25
29
  import { canonical } from './config.mjs';
@@ -168,13 +172,35 @@ function deadFiles(graph, config) {
168
172
  }
169
173
 
170
174
  const TEST_CONSUMER = /^(?:(?:src|apps)\/.+\.service\.spec\.ts|src\/tests\/fixtures\/builders\/.+\.builder\.ts)$/u;
175
+ /** The slot of integration specs and the slot of the integrations they pair with by folder (knowledge/hfs/slots.yaml). */
176
+ const INTEGRATION_SPEC_SLOT = 'be.tests.integration';
177
+ const INTEGRATION_SLOT = 'be.integrations';
171
178
 
172
- /** Pseudo edges from the unit specs of services and the fixture builders (read from disk, outside the production program) to graph files. */
179
+ /** The provider an integration spec pairs with (its `<capability>` folder), or null when `rel` is no integration spec. */
180
+ function integrationSpecProvider(graph, rel) {
181
+ const classified = graph.resolver.classifyPath(rel);
182
+ return classified.slot === INTEGRATION_SPEC_SLOT && classified.status !== 'forbidden' ? classified.bindings?.capability ?? null : null;
183
+ }
184
+
185
+ /** Whether `to` belongs to the integration of `provider` (slot be.integrations, its `<provider>` binding). */
186
+ function inIntegrationOf(graph, to, provider) {
187
+ const owner = graph.resolver.ownerOf(to);
188
+ return owner?.slot === INTEGRATION_SLOT && owner.bindings?.provider === provider;
189
+ }
190
+
191
+ /**
192
+ * Pseudo edges (read from disk, outside the production program) to graph files: from the unit specs of services and the fixture
193
+ * builders to anything, and from an integration spec only to the integration of its own provider folder.
194
+ */
173
195
  function testConsumerEdges({ context, graph, config }) {
174
196
  const { ts } = context;
175
197
  const options = context.projects?.[0]?.options ?? {};
176
198
  const edges = [];
177
- for (const rel of [...treeOf(config.root).files].filter(file => TEST_CONSUMER.test(file)).sort()) {
199
+ const consumers = [...treeOf(config.root).files]
200
+ .map(file => ({ file, provider: TEST_CONSUMER.test(file) ? null : integrationSpecProvider(graph, file) }))
201
+ .filter(({ file, provider }) => provider !== null || TEST_CONSUMER.test(file))
202
+ .sort((a, b) => a.file.localeCompare(b.file));
203
+ for (const { file: rel, provider } of consumers) {
178
204
  const abs = path.join(config.root, ...rel.split('/'));
179
205
  let text;
180
206
  try { text = fs.readFileSync(abs, 'utf8'); } catch { continue; }
@@ -183,7 +209,7 @@ function testConsumerEdges({ context, graph, config }) {
183
209
  if (!(ts.isImportDeclaration(statement) || ts.isExportDeclaration(statement)) || !statement.moduleSpecifier || !ts.isStringLiteralLike(statement.moduleSpecifier)) continue;
184
210
  const resolved = ts.resolveModuleName(statement.moduleSpecifier.text, abs, options, ts.sys).resolvedModule?.resolvedFileName;
185
211
  const to = resolved ? graph.abs(canonical(resolved)) : null;
186
- if (to) edges.push({ from: rel, to, edge: { declaration: statement }, reexport: false });
212
+ if (to && (provider === null || inIntegrationOf(graph, to, provider))) edges.push({ from: rel, to, edge: { declaration: statement }, reexport: false });
187
213
  }
188
214
  }
189
215
  return edges;
@@ -254,7 +280,7 @@ export function checkDeadExports({ context, graph, config }) {
254
280
  ruleId: 'HFS_UNUSED_EXPORT',
255
281
  path: entry, line, column: 1,
256
282
  name, owner: owner.root, slot: owner.slot,
257
- message: `${entry} exports ${name}, but no production file outside ${owner.root || 'the repository root'} imports it; remove the export (only a service unit spec or a fixture builder counts besides production files).`,
283
+ message: `${entry} exports ${name}, but no production file outside ${owner.root || 'the repository root'} imports it; remove the export (only a service unit spec, a fixture builder, or an integration spec of this integration's own provider folder counts besides production files).`,
258
284
  });
259
285
  }
260
286
  }
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { isIP } from 'node:net';
4
- import { gitOutput } from '../../lib/git.mjs';
4
+ import { gitOutput } from '../../api/git/lib.mjs';
5
5
  import { repositoryName } from '../../lib/repo-identity.mjs';
6
6
  import { braceVariants } from '../../lib/glob.mjs';
7
7
  import { createSlotResolver, loadSlotManifest, openHfs } from '../../lib/hfs-slots.mjs';
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { gitOutput } from '../../lib/git.mjs';
3
+ import { gitOutput } from '../../api/git/lib.mjs';
4
4
 
5
5
  /**
6
6
  * HFS check 5: the files and directories the slot manifest requires (knowledge/hfs/slots.yaml `requires`,
@@ -1,7 +1,7 @@
1
1
  // recorded-lines.mjs - the one home of the file-size ratchet's arithmetic (R20 HFS_SIZE_GROWTH), shared by the back-end
2
2
  // and front-end lint canons (each ships a byte copy in its runtime/ bundle, kept by packages/hfs/scripts/sync-runtime.mjs).
3
3
  import path from 'node:path';
4
- import { runGit } from './git.mjs';
4
+ import { runGit } from '../api/git/lib.mjs';
5
5
 
6
6
  /** The line count of a text; a trailing newline does not open a line. */
7
7
  export const lineCount = (text) => {
@@ -3,7 +3,7 @@
3
3
  // they carry the same README title, the same stack-declaration project name and the same sibling repositories.
4
4
  import fs from 'node:fs';
5
5
  import path from 'node:path';
6
- import { gitOutput } from './git.mjs';
6
+ import { gitOutput } from '../api/git/lib.mjs';
7
7
 
8
8
  const git = (root, args) => gitOutput(args, { cwd: root }).trim();
9
9
  const real = (p) => { try { return fs.realpathSync(p); } catch { return path.resolve(p); } };