@starci/eslint-canon-fe 9.13.1 → 9.13.3

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,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 9.13.3 - 2026-11-04
4
+
5
+ - Changed: the runtime copies carry `engine/runtime-root.mjs`, which moves an installed copy's legacy state with its owner and mode (`@starci/hfs` 5.6.1), and the release-5 canon pins.
6
+
7
+ ## 9.13.2 - 2026-11-04
8
+
9
+ - Changed: the runtime copies carry `engine/runtime-root.mjs` with the per-user state base of an installed copy (`@starci/hfs` 5.6.0) and the canon pins of release 4 (`@starci/auth` 0.4.0, `@starci/auth-oidc` 0.1.3).
10
+
3
11
  ## 9.13.1 - 2026-11-04
4
12
 
5
13
  - Changed: the runtime copy of the canon pins names the release-3 versions (`@starci/hfs` 5.5.1, `@starci/jest-preset` 2.7.1, `@starci/eslint-canon-be` 3.15.1, the Grammar family 0.21.18).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starci/eslint-canon-fe",
3
- "version": "9.13.1",
3
+ "version": "9.13.3",
4
4
  "description": "Universal front-end lint laws for TypeScript and Next.js, independent of the selected Grammar family.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,4 +1,5 @@
1
1
  import fs from 'node:fs';
2
+ import os from 'node:os';
2
3
  import path from 'node:path';
3
4
  import { fileURLToPath } from 'node:url';
4
5
  import { parseYaml } from './yaml.mjs';
@@ -24,17 +25,102 @@ export const LOCAL_ROOT_ENV = 'STARCI_LOCAL_ROOT';
24
25
  /** The host-data directory of a runtime root. Git-ignored and never packed; the example fixtures under examples/.runtimes are a separate, public exception. */
25
26
  export const RUNTIME_STATE_DIR = '.runtime';
26
27
  /**
27
- * <root>/.runtime. The root is the `.claude` of the host Source: the checkout, or the copy `starci runtime install` places at
28
- * <app>/.claude. ~/.starci/runtime/node_modules/starci is only the CLI's download cache (it supplies the installer and hosts no
29
- * state), and `starci runtime link` merely points the launcher at a root, so the state follows the code's own location.
28
+ * <root>/.runtime, the state base of a runtime root that is a checkout (the harness repository, or a copy of it placed on the host).
29
+ * An installed or locally sourced copy keeps none (see installedStateBase).
30
30
  */
31
31
  export const runtimeStateDir = (root = skillRoot) => path.join(root, RUNTIME_STATE_DIR);
32
+ /** True when `root` is a copy an install or `starci deps source --local` placed below a node_modules folder, not a checkout. */
33
+ export const installedRoot = (root = skillRoot) => path.resolve(root).split(/[\\/]/).includes('node_modules');
34
+ /**
35
+ * The per-user state base of an installed copy: LOCALAPPDATA (Windows), else XDG_STATE_HOME, else ~/.local/state, then
36
+ * starci-harness/state. It lies outside every package and every product repository, so `npm ci` and a package copy never delete a
37
+ * running stack's data, and every installed copy on the host shares one machine.sqlite and one stack/<project>/<env> per environment.
38
+ */
39
+ export const installedStateBase = (env = process.env) => path.join(env['LOCALAPPDATA'] || env['XDG_STATE_HOME'] || path.join(os.homedir(), '.local', 'state'), 'starci-harness', 'state');
32
40
  /**
33
41
  * The per-host state base, the one owner of every host-state location: machine.sqlite, projects/<ledger_id>/runtime.sqlite, archive/,
34
- * artifacts/ (the blob store), guards/, host-lock/ and the other process state sit under it. Default <runtime root>/.runtime;
35
- * LOCAL_ROOT_ENV replaces it wholesale. Nothing is read from or moved out of the earlier %LOCALAPPDATA%/StarCi and ~/.starci locations.
42
+ * artifacts/ (the blob store), guards/, host-lock/, stack/<project>/<env>/ and the other process state sit under it. LOCAL_ROOT_ENV
43
+ * replaces it wholesale; else a checkout keeps it in <runtime root>/.runtime and an installed copy in installedStateBase.
44
+ */
45
+ export const starciLocalRoot = (env = process.env, root = skillRoot) => {
46
+ if (env[LOCAL_ROOT_ENV])
47
+ return path.resolve(env[LOCAL_ROOT_ENV]);
48
+ return installedRoot(root) ? installedStateBase(env) : runtimeStateDir(root);
49
+ };
50
+ /** The refusal of a legacy state base a running process still holds (a stack's bind mounts, an open log). */
51
+ export class LegacyStateHeld extends Error {
52
+ code = 'HFS_STATE_LEGACY_HELD';
53
+ legacy;
54
+ constructor(legacy, cause) {
55
+ super(`${legacy} holds the state of a running stack and cannot move out of the package yet; run \`starci app stack down --env <env>\` for each running environment, then any \`starci app\` verb moves it`, { cause });
56
+ this.legacy = legacy;
57
+ }
58
+ }
59
+ const HELD = new Set(['EPERM', 'EBUSY', 'EACCES']);
60
+ const isHeld = (error) => HELD.has(String(error?.code));
61
+ /** Gives `to` and everything below it the mode, owner and group of the matching entry of `from` (a copy keeps neither on its own). */
62
+ function keepOwnership(from, to) {
63
+ const source = fs.lstatSync(from);
64
+ const target = fs.lstatSync(to);
65
+ if (!source.isSymbolicLink())
66
+ fs.chmodSync(to, source.mode & 0o7777);
67
+ if (source.uid !== target.uid || source.gid !== target.gid)
68
+ fs.lchownSync(to, source.uid, source.gid);
69
+ if (source.isDirectory())
70
+ for (const name of fs.readdirSync(from))
71
+ keepOwnership(path.join(from, name), path.join(to, name));
72
+ }
73
+ /** Moves one entry by rename, which keeps its owner and mode; across volumes (EXDEV) it is copied, its ownership restored, then removed. */
74
+ function moveEntry(from, to) {
75
+ try {
76
+ fs.renameSync(from, to);
77
+ return;
78
+ }
79
+ catch (error) {
80
+ if (error?.code !== 'EXDEV')
81
+ throw error;
82
+ }
83
+ fs.cpSync(from, to, { recursive: true, verbatimSymlinks: true, errorOnExist: true, force: false });
84
+ keepOwnership(from, to);
85
+ fs.rmSync(from, { recursive: true, force: true });
86
+ }
87
+ /**
88
+ * Moves the state an installed copy kept inside itself (<copy>/.runtime, before installedStateBase) out to the state base, once: the
89
+ * folder is first renamed to <copy>/.runtime.adopting (a rename a running stack's open files refuse, LegacyStateHeld), then each entry
90
+ * is moved with its owner and mode (a bind folder a container user owns stays that user's) and the folder removed. An entry the state base already holds is kept beside it under legacy-<stamp>/, never overwritten. An
91
+ * interrupted adoption resumes from .runtime.adopting. Null when there is nothing to adopt: a checkout, LOCAL_ROOT_ENV set, or no
92
+ * legacy folder.
36
93
  */
37
- export const starciLocalRoot = (env = process.env, root = skillRoot) => (env[LOCAL_ROOT_ENV] ? path.resolve(env[LOCAL_ROOT_ENV]) : runtimeStateDir(root));
94
+ export function adoptLegacyState(env = process.env, root = skillRoot, now = () => new Date()) {
95
+ if (env[LOCAL_ROOT_ENV] || !installedRoot(root))
96
+ return null;
97
+ const from = runtimeStateDir(root);
98
+ const parked = `${from}.adopting`;
99
+ if (!fs.existsSync(parked)) {
100
+ if (!fs.existsSync(from))
101
+ return null;
102
+ try {
103
+ fs.renameSync(from, parked);
104
+ }
105
+ catch (error) {
106
+ if (isHeld(error))
107
+ throw new LegacyStateHeld(from, error);
108
+ throw error;
109
+ }
110
+ }
111
+ const to = installedStateBase(env);
112
+ const aside = path.join(to, `legacy-${now().toISOString().replace(/[^0-9]/g, '').slice(0, 14)}`);
113
+ const moved = [];
114
+ const kept = [];
115
+ for (const name of fs.readdirSync(parked).sort()) {
116
+ const taken = fs.existsSync(path.join(to, name));
117
+ fs.mkdirSync(taken ? aside : to, { recursive: true });
118
+ moveEntry(path.join(parked, name), path.join(taken ? aside : to, name));
119
+ (taken ? kept : moved).push(name);
120
+ }
121
+ fs.rmSync(parked, { recursive: true, force: true });
122
+ return { from, to, moved, kept };
123
+ }
38
124
  /**
39
125
  * Read a runtime contract document. `parts` are path segments under `skillRoot`
40
126
  * (e.g. readModuleJson('modules', 'models', 'kinds.yaml')).
@@ -38,7 +38,7 @@ pins:
38
38
  source: packages/next-data/package.json
39
39
  why: 'the SWR data helpers of every front end (fe-stack data, owner:next-data-package): pollingRestart, the viewer and tenant key helpers, viewerSwrConfig and the viewer cache; each operation hook stays in the app and the package holds no component.'
40
40
  '@starci/grammar':
41
- version: 0.21.18
41
+ version: 0.21.20
42
42
  group: starci
43
43
  install: registry
44
44
  side: fe
@@ -52,28 +52,28 @@ pins:
52
52
  source: packages/next-i18n/package.json
53
53
  why: 'the next-intl stack of every front end (fe-stack i18n, owner:next-i18n-package): defineI18n over the locales of the app, the locale-negotiating proxy, the locale-aware navigation, the request config that loads the catalogs of the app and the locale read of the [locale] layout; the app keeps its message catalogs and writes none of the stack.'
54
54
  '@starci/eslint-canon-be':
55
- version: 3.15.1
55
+ version: 3.15.3
56
56
  group: starci
57
57
  install: registry
58
58
  side: be
59
59
  source: packages/eslint-canon-be/package.json
60
60
  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.'
61
61
  '@starci/eslint-canon-fe':
62
- version: 9.13.1
62
+ version: 9.13.3
63
63
  group: starci
64
64
  install: registry
65
65
  side: fe
66
66
  source: packages/eslint-canon-fe/package.json
67
67
  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.'
68
68
  '@starci/eslint-grammar-core':
69
- version: 0.21.18
69
+ version: 0.21.20
70
70
  group: starci
71
71
  install: registry
72
72
  side: fe
73
73
  source: packages/eslint-grammar-core/package.json
74
74
  why: 'the StarCi Grammar component API lint, versioned with @starci/grammar; front ends compose this family package with the universal FE canon.'
75
75
  '@starci/eslint-grammar-offset-pop':
76
- version: 0.21.18
76
+ version: 0.21.20
77
77
  group: starci
78
78
  install: registry
79
79
  side: fe
@@ -127,14 +127,14 @@ pins:
127
127
  source: packages/nest-kit/package.json
128
128
  why: 'the module definition of every back-end module: defineConfigurableModule and defineCapabilityModule return the module base class, the options token and the options types with their types stated, so a <x>.module-definition.ts is one call that the lint on stated export types (R368) and the duplicate-code check (R21) both accept; @starci/jest-preset compiles it as an ESM-only dependency.'
129
129
  '@starci/auth':
130
- version: 0.3.0
130
+ version: 0.4.0
131
131
  group: starci
132
132
  install: registry
133
133
  side: both
134
134
  source: packages/auth/package.json
135
135
  why: 'the lite sign-in: the NestJS module (EdDSA access tokens with a multi-key JWKS, passwords, Google OAuth with state and PKCE, rotating refresh sessions, the sign-in budget, the refresh cookie), the front-end client and its React bindings, the door contracts and test doubles; the one importer of the identity libraries, reached by a back end only through integrations/passport; @starci/jest-preset compiles it as an ESM-only dependency.'
136
136
  '@starci/auth-oidc':
137
- version: 0.1.2
137
+ version: 0.1.3
138
138
  group: starci
139
139
  install: registry
140
140
  side: both
@@ -155,7 +155,7 @@ pins:
155
155
  source: packages/mail/package.json
156
156
  why: 'outgoing mail of a back end: a NestJS module and the Mailer port over SMTP (connect, greeting, socket and total timeouts, STARTTLS required by default, recipient, message and attachment checks, a shared SMTP target policy, coded errors, typed escaping render helpers returning { subject, text, html }), with @starci/mail/testing for its specs; the only importer of nodemailer (R90).'
157
157
  '@starci/hfs':
158
- version: 5.5.1
158
+ version: 5.6.1
159
159
  group: starci
160
160
  install: registry
161
161
  side: both