@starci/hfs 4.9.2 → 4.9.4

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,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.9.4 - 2026-10-28
4
+
5
+ - Added: the bundled rule catalog carries R323 (`FE_PAGE_HEADER_HAND_ROLLED`, enforced by `@starci/eslint-grammar-core` `no-hand-rolled-page-header`) and its failure-code guidance.
6
+ - Changed: the bundled canon pins follow release F (Grammar family 0.15.3, hfs 4.9.4, canon-fe 9.4.3, canon-be 3.6.3, ai-router 0.3.0).
7
+
8
+ ## 4.9.3 - 2026-10-28
9
+
10
+ - Changed: the bundled canon pins no longer pin the command line. An app's `starci-harness` is judged and scaffolded at the version of the CLI that runs this package: `main(argv, { harness: { version } })` takes it, and `runtime/scripts/hfs/canon-pins.mjs` (`canonPins`, the one reader of `knowledge/hfs/canon-pins.yaml`) adds the `starci-harness` row from it for HFS_CANON_PIN_DRIFT, HFS_CI_MISSING_CANON and the scaffold's root devDependencies. The CLI depends on this package exactly, so a bundled pin of the CLI tied every CLI release to a new hfs and back. Run without a CLI, nothing judges the app's `starci-harness` and `scaffoldApp` refuses HFS_SCAFFOLD_PIN_MISSING before `git init`. Spec: `src/canon-pins.spec.mjs` (also: every `source:` pin at its folder's version, the five runtime copies byte-identical, `contexts/stack/pins.yaml` in step).
11
+ - Changed: the bundled canon pins follow release E (Grammar family 0.15.2, hfs 4.9.3, canon-fe 9.4.2, canon-be 3.6.2, ai-router 0.3.0).
12
+
3
13
  ## 4.9.2 - 2026-10-28
4
14
 
5
15
  - Fixed: `repositoryName` (runtime/scripts/hfs/repo-identity.mjs) names a repository by its origin remote, whatever folder holds the clone; the main checkout's folder names it only without an origin. HFS_README_TITLE_INVALID and the stack project name no longer change with the clone folder. Spec: `src/repo-identity.spec.mjs`.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @starci/hfs
2
2
 
3
- The HFS command line of a StarCi app: the one repository of a product, `<app>/` with its root (the `app.*` slots: the one `package.json`, lockfile and `node_modules`, the `hfs.json` declaration of kind `app`, CI, hooks, the `app.starciwork` work area) and its two sides `be/` and `fe/` (slot `app.sides`). It is installed from the npm registry at the exact version in `knowledge/hfs/canon-pins.yaml` (see [`packages/README.md`](../README.md)),
3
+ The HFS command line of a StarCi app: the one repository of a product, `<app>/` with its root (the `app.*` slots: the one `package.json`, lockfile and `node_modules`, the `hfs.json` declaration of kind `app`, CI, hooks, the `app.starciwork` work area) and its two sides `be/` and `fe/` (slot `app.sides`). It is installed from the npm registry as the exact dependency of the CLI that runs it in-process, `starci-harness` (see [`packages/README.md`](../README.md)), which passes `main` its own version as `harness`,
4
4
  and it is self-contained: `runtime/` carries the slot manifest, the canon pins, the Vietnamese why
5
5
  catalog slice, the loader and the architecture machine with every file it imports, so it runs where there is no runtime
6
6
  checkout. The machine loads `typescript` from the app it checks (never its own copy), so run `npm ci` first.
@@ -98,7 +98,7 @@ Its own checks (`scripts/hfs/check.mjs`, `scripts/hfs/rules/`; the rendered-file
98
98
  | `HFS_STARCIWORK_LITE_FORBIDDEN` | error | content outside the lite `app.starciwork` boundary: "lite .starciwork holds only workspace.yaml and brand/ (plus .gitignore)" (R212) |
99
99
  | `HFS_SLOT_REQUIRED_MISSING` | error | a file or directory a required slot, app or instance must contain |
100
100
  | `HFS_MIN_INSTANCES` | error | fewer instances of a slot than `minInstances` |
101
- | `HFS_CANON_PIN_DRIFT` | error | a dependency not at the exact version of `knowledge/hfs/canon-pins.yaml` |
101
+ | `HFS_CANON_PIN_DRIFT` | error | a dependency not at the exact version of `knowledge/hfs/canon-pins.yaml`, or `starci-harness` not at the version of the CLI that runs the check (`runtime/scripts/hfs/canon-pins.mjs`) |
102
102
  | `HFS_SIZE_SOFT_BACKLOG` | info | a source file over `ruleParams.fileLines.soft`; report only, never fails |
103
103
  | `HFS_MANAGED_FILE_DRIFT` | error | a managed file that exists but differs from its render (the `managedBy` slots: hooks, workflows, sonar, the back-end `be.tool-config` files, `app.format-config`, the `scripts` block of `app.package-manifest`, compared as parsed JSON) |
104
104
  | `HFS_RULE_OFF_WITHOUT_REPLACEMENT` | error | `eslint.config.mjs`, or a front end's `stylelint.config.mjs`, differs from its managed factory render, so a rule could be off, warned or redefined in it |
package/emit/db-types.mjs CHANGED
@@ -16,7 +16,7 @@ import { spawnSync } from 'node:child_process';
16
16
  import fs from 'node:fs';
17
17
  import path from 'node:path';
18
18
  import { parse as parseToml } from 'smol-toml';
19
- import { parseYaml } from '../runtime/engine/yaml.mjs';
19
+ import { bundledPins } from '../runtime/scripts/hfs/canon-pins.mjs';
20
20
 
21
21
  /** The repository-relative path of the generated types (the path of slot `app.supabase.types`). */
22
22
  export const dbTypesPath = 'supabase/types/database.types.ts';
@@ -56,7 +56,6 @@ class DbTypesError extends Error {
56
56
  }
57
57
 
58
58
  const spawn = (file, args, options = {}) => spawnSync(file, args, { encoding: 'utf8', windowsHide: true, maxBuffer: 64 * 1024 * 1024, ...options });
59
- const PINS_FILE = new URL('../runtime/knowledge/hfs/canon-pins.yaml', import.meta.url);
60
59
 
61
60
  /**
62
61
  * The one spawn: an argument array, a hidden window, a bound buffer, never a shell. Specs inject a fake of the same shape. The
@@ -65,7 +64,7 @@ const PINS_FILE = new URL('../runtime/knowledge/hfs/canon-pins.yaml', import.met
65
64
  const withNpxFallback = (run, platform) => (file, args, options = {}) => {
66
65
  const first = run(file, args, options);
67
66
  if (file !== SUPABASE_CLI || first.error?.code !== 'ENOENT') return first;
68
- const version = parseYaml(fs.readFileSync(PINS_FILE, 'utf8'))?.pins?.[SUPABASE_CLI]?.version;
67
+ const version = bundledPins()[SUPABASE_CLI]?.version;
69
68
  if (!version) return first;
70
69
  return platform === 'win32'
71
70
  ? run('cmd.exe', ['/d', '/s', '/c', 'npx', '--yes', `${SUPABASE_CLI}@${version}`, ...args], options)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starci/hfs",
3
- "version": "4.9.2",
3
+ "version": "4.9.4",
4
4
  "description": "The StarCi app command implementation for scaffolding, linting, checking and synchronizing one product repository.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,7 +1,8 @@
1
1
  schema: starci/canon-pins@1
2
2
  purpose: >-
3
3
  The exact version of every shared dependency in the StarCi family, including package-owned runtime dependencies. One version per dependency for the whole
4
- family. Raising a version is a change to this file first (with the package bump when it is a @starci package),
4
+ family. The command line itself is not pinned here: an app's starci-harness is the version of the CLI that runs @starci/hfs to
5
+ judge or scaffold it (scripts/hfs/canon-pins.mjs), since that CLI depends on this package exactly. Raising a version is a change to this file first (with the package bump when it is a @starci package),
5
6
  then the repositories' own upgrade lane (each sets the exact version and reinstalls; nothing else installs a @starci package). Read on 2026-09-29 from the packages under
6
7
  .claude/packages and from the installed node_modules of the six product repositories; where they disagreed the
7
8
  newest version installed in more than one repository won, and the runtime's own version won for @starci packages.
@@ -30,35 +31,35 @@ pins:
30
31
  source: packages/event-stream/package.json
31
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.'
32
33
  '@starci/grammar':
33
- version: 0.15.1
34
+ version: 0.15.3
34
35
  group: starci
35
36
  install: registry
36
37
  side: fe
37
38
  source: packages/grammar/package.json
38
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.'
39
40
  '@starci/eslint-canon-be':
40
- version: 3.6.1
41
+ version: 3.6.3
41
42
  group: starci
42
43
  install: registry
43
44
  side: be
44
45
  source: packages/eslint-canon-be/package.json
45
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.'
46
47
  '@starci/eslint-canon-fe':
47
- version: 9.4.1
48
+ version: 9.4.3
48
49
  group: starci
49
50
  install: registry
50
51
  side: fe
51
52
  source: packages/eslint-canon-fe/package.json
52
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.'
53
54
  '@starci/eslint-grammar-core':
54
- version: 0.15.1
55
+ version: 0.15.3
55
56
  group: starci
56
57
  install: registry
57
58
  side: fe
58
59
  source: packages/eslint-grammar-core/package.json
59
60
  why: 'the StarCi Grammar component API lint, versioned with @starci/grammar; front ends compose this family package with the universal FE canon.'
60
61
  '@starci/eslint-grammar-offset-pop':
61
- version: 0.15.1
62
+ version: 0.15.3
62
63
  group: starci
63
64
  install: registry
64
65
  side: fe
@@ -98,21 +99,14 @@ pins:
98
99
  source: packages/test-world/package.json
99
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. 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.'
100
101
  '@starci/ai-router':
101
- version: 0.2.1
102
+ version: 0.3.0
102
103
  group: starci
103
104
  install: registry
104
105
  side: be
105
106
  source: packages/ai-router/package.json
106
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.'
107
- 'starci-harness':
108
- version: 1.1.0-alpha.2
109
- group: starci
110
- install: registry
111
- side: both
112
- source: cli/package.json
113
- why: 'the one StarCi command line (bins starci and starci-harness): a product repository installs it for starci app, which it runs in-process through its exact @starci/hfs dependency, and its codegen loads starci-harness/input-bounds.'
114
108
  '@starci/hfs':
115
- version: 4.9.2
109
+ version: 4.9.4
116
110
  group: starci
117
111
  install: registry
118
112
  side: both
@@ -5687,3 +5687,26 @@ rules:
5687
5687
  source: owner
5688
5688
  quote: "A chip label starts uppercase."
5689
5689
  ruling: "The label of a status chip starts with an uppercase letter (owner ruling chip-label-uppercase); StatusDot capitalises a text label it renders, and the lint refuses a lowercase literal label at its source."
5690
+ - id: R323
5691
+ code: "FE_PAGE_HEADER_HAND_ROLLED"
5692
+ law: "A page is headed by Grammar's PageHeader; a Breadcrumbs trail followed by a SectionHeader in one parent is a page head composed by hand, with no band, no top inset and no separator."
5693
+ kinds: [lint]
5694
+ gates: [pre-commit, settle, land, ci]
5695
+ failureCodes: ["FE_PAGE_HEADER_HAND_ROLLED"]
5696
+ enforcers:
5697
+ - {kind: eslint-grammar-core, id: no-hand-rolled-page-header}
5698
+ editions: [lite, full]
5699
+ docs: [knowledge/patterns/fe/index.yaml#FE-RULE-OWNERSHIP]
5700
+ templateNa: "A template states no page head; the pages are the app's."
5701
+ proves:
5702
+ - packages/eslint-grammar-core/src/page-header.spec.ts
5703
+ exemplifiedBy:
5704
+ - {path: packages/eslint-grammar-core/src/page-header.spec.ts, editions: [lite, full], codes: [FE_PAGE_HEADER_HAND_ROLLED]}
5705
+ witness:
5706
+ - {path: packages/eslint-grammar-core/src/page-header.spec.ts, editions: [lite, full], codes: [FE_PAGE_HEADER_HAND_ROLLED]}
5707
+ cases:
5708
+ - id: case-1
5709
+ date: "2026-10-28"
5710
+ source: owner
5711
+ quote: "Publish Grammar 0.15.3 with PageHeader now, then move all 18 pages to it before deploying. (translated from Vietnamese)"
5712
+ ruling: "A page is headed by Grammar's PageHeader (owner ruling page-header-band): a surface band whose content shares the page content's edges and which ends in its own full-width separator, page tabs standing on it; the lint refuses a page head composed by hand from Breadcrumbs and SectionHeader."
@@ -2714,6 +2714,16 @@ FE_PACKAGE_SHAPE:
2714
2714
  owner: op-retry
2715
2715
  kind: check-finding
2716
2716
 
2717
+ FE_PAGE_HEADER_HAND_ROLLED:
2718
+ title: "A page head is composed by hand instead of PageHeader"
2719
+ title_vi: "Phần đầu trang được ghép tay thay vì dùng PageHeader"
2720
+ meaning_vi: "Tại <file>, một Breadcrumbs theo sau là một SectionHeader trong cùng một phần tử cha: phần đầu trang ghép tay không có dải nền, không có khoảng đệm trên và không có đường phân cách, nên mỗi trang tự quyết khoảng cách riêng."
2721
+ causes_vi:
2722
+ - "Đặt Breadcrumbs rồi SectionHeader level={1} ở đầu trang hoặc layout."
2723
+ nextStep_vi: "Dùng PageHeader của Grammar với breadcrumbs, title và các khe cần thiết (description, meta, status, mark, actions, tabs) (quy định page-header-band)."
2724
+ owner: op-retry
2725
+ kind: check-finding
2726
+
2717
2727
  FE_PAGE_METADATA_MISSING:
2718
2728
  title: "Every page exports metadata"
2719
2729
  title_vi: "Trang không khai báo metadata"
@@ -0,0 +1,27 @@
1
+ // canon-pins.mjs - the pins an app is judged and scaffolded against. knowledge/hfs/canon-pins.yaml pins every shared dependency
2
+ // but the command line itself: the `starci-harness` row is the version of the CLI that runs this @starci/hfs (it passes its own
3
+ // version to main as `harness`), never a bundled row, since the CLI depends on this package exactly and a bundled pin of the CLI
4
+ // would make each release of one need a release of the other. Run without a CLI (the package's own specs and diagnostics), there
5
+ // is no such row: nothing judges the app's starci-harness, and the scaffold, which needs it, refuses.
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { skillRoot } from '../../engine/runtime-root.mjs';
9
+ import { parseYaml } from '../../engine/yaml.mjs';
10
+
11
+ /** The runtime-relative file of the bundled pins. */
12
+ export const CANON_PINS_FILE = 'knowledge/hfs/canon-pins.yaml';
13
+ /** The npm name of the command line that runs @starci/hfs, pinned at its own version. */
14
+ export const HARNESS_PACKAGE = 'starci-harness';
15
+
16
+ /** The pin map of knowledge/hfs/canon-pins.yaml under `root` (the bundled runtime by default). */
17
+ export const bundledPins = (root = skillRoot) => parseYaml(fs.readFileSync(path.join(root, CANON_PINS_FILE), 'utf8'))?.pins ?? {};
18
+
19
+ /**
20
+ * The pin map an app answers to: the bundled pins plus the `starci-harness` row at `harness.version`, the version of the CLI that
21
+ * runs this package; without `harness` the bundled pins alone.
22
+ */
23
+ export function canonPins({ root = skillRoot, harness } = {}) {
24
+ const pins = bundledPins(root);
25
+ if (harness?.version === undefined) return pins;
26
+ return { ...pins, [HARNESS_PACKAGE]: { version: harness.version, group: 'starci', install: 'registry', side: 'both' } };
27
+ }
@@ -1,6 +1,7 @@
1
1
  // hfs-check.mjs - the HFS app check, behind `starci app check | explain` (packages/hfs) and reusable by any
2
2
  // runtime check. It reads three things and nothing else: the app's hfs.json, the slot manifest
3
- // (knowledge/hfs/slots.yaml through scripts/hfs/slots.mjs) and the pins (knowledge/hfs/canon-pins.yaml). It never
3
+ // (knowledge/hfs/slots.yaml through scripts/hfs/slots.mjs) and the pins (canon-pins.mjs: the bundled knowledge/hfs/canon-pins.yaml
4
+ // and the version of the starci-harness that runs the check). It never
4
5
  // writes to the repository it inspects.
5
6
  //
6
7
  // checkRepo() answers, for the tracked paths of one app (git ls-files), per scope (the app root and each side folder):
@@ -64,7 +65,7 @@ import { appWiring, BOOT_PROOF_CODE } from './app-wiring.mjs';
64
65
  import { coverageFrom as coverageFromReport } from './coverage-from.mjs';
65
66
  import { skillRoot } from '../../engine/runtime-root.mjs';
66
67
  import { ARCHITECTURE_RULE_IDS, checkArchitecture } from './architecture/index.mjs';
67
- import { parseYaml } from '../../engine/yaml.mjs';
68
+ import { canonPins } from './canon-pins.mjs';
68
69
  import { readCatalogOnce } from './catalog-once.mjs';
69
70
  import { APP_SCOPE, HFS_DECLARATION_FILE, appRelativeMessages, HfsSlotsError, SIDES, createSlotResolver, loadSlotManifest, readRepoDeclaration, resolveRepoDeclaration } from './slots.mjs';
70
71
  import { judgedFindings, summarize, withWhy } from './check-results.mjs';
@@ -101,7 +102,6 @@ export { REFUSAL_CODES } from './refusal-codes.mjs';
101
102
  // The database rules (R213-R216) read SQL through a WASM parser, so they are async: `starci app check` calls them beside the other emitters and passes the findings in as `extraFindings`.
102
103
  export { checkDatabase } from './rules/database.mjs';
103
104
 
104
- const CANON_PINS_FILE = 'knowledge/hfs/canon-pins.yaml';
105
105
  export const FAILURE_CODES_FILE = 'modules/kernel/failure-codes.yaml';
106
106
  /** The codes this module emits that are not the slot loader's own: the why bundle of packages/hfs ships exactly these plus the loader's. */
107
107
  export const CHECK_CODES = Object.freeze([
@@ -151,9 +151,6 @@ export function trackedFiles(repoRoot) {
151
151
 
152
152
  const fill = (text, bindings) => String(text).replace(VAR, (whole, name) => bindings[name] ?? whole);
153
153
 
154
- /** The pin map of knowledge/hfs/canon-pins.yaml under `root`. */
155
- const readPins = (root) => parseYaml(fs.readFileSync(path.join(root, CANON_PINS_FILE), 'utf8'))?.pins ?? {};
156
-
157
154
  /** Instances (slot, root, bindings) present in the tracked tree, for every slot that names required files or a minimum. */
158
155
  function instancesOf(resolver, files) {
159
156
  const found = new Map();
@@ -252,7 +249,7 @@ function sizeSoftFindings({ repoRoot, resolver, files, inScope }) {
252
249
  * relative to it, exactly as the standalone repository root was). `files` are the scope's tracked paths; `all` (root only) every
253
250
  * tracked path of the app, for the rules that read across it (dependency skew, proof commands).
254
251
  */
255
- function scopeFindings({ repoRoot, root, repo, resolver, files, all = files, scoped, manifest, appSide = false, editionDeclaration = true, phase }) {
252
+ function scopeFindings({ repoRoot, root, harness, repo, resolver, files, all = files, scoped, manifest, appSide = false, editionDeclaration = true, phase }) {
256
253
  const inScope = (file) => !scoped || scoped.has(file);
257
254
  // A required directory of the root (be/, fe/) is present through the files below it, which are the sides' own.
258
255
  const trackedSet = new Set(all);
@@ -265,14 +262,15 @@ function scopeFindings({ repoRoot, root, repo, resolver, files, all = files, sco
265
262
  // The runtime repository (kind runtime) is judged here by its tree law only; its own rules run in scripts/hfs/runtime-check.mjs.
266
263
  if (repo.kind === RUNTIME_KIND) return findings;
267
264
 
268
- const pins = readPins(root);
265
+ const pins = canonPins({ root, harness });
269
266
  findings.push(...contentRuleFindings({ repoRoot, repo, resolver, files, all, scoped, pins, inScope, isRoot, appSide, editionDeclaration, manifest, phase }));
270
267
  if (!isRoot) findings.push(...sizeSoftFindings({ repoRoot, resolver, files, inScope }));
271
268
  return findings;
272
269
  }
273
270
 
274
271
  /**
275
- * The check of one app. `repoRoot` is the app root (a side folder is checked as that side alone). `declaration` overrides hfs.json
272
+ * The check of one app. `repoRoot` is the app root (a side folder is checked as that side alone). `harness` ({ version }) is the CLI
273
+ * that runs the check, whose version is the app's starci-harness pin (canon-pins.mjs). `declaration` overrides hfs.json
276
274
  * (a dry run over an app that has none); `files` overrides git ls-files (specs), app-relative. `only` (a list of app-relative paths)
277
275
  * limits the per-path checks (slot, pin, size) to those paths; the checks of the tree as a whole (required files, minimum instances,
278
276
  * empty directories, untracked entries) are not per-path. `tree: false` skips the file-system checks (empty directories, ghosts,
@@ -284,7 +282,7 @@ function scopeFindings({ repoRoot, root, repo, resolver, files, all = files, sco
284
282
  * `phase: 'source'` is the runtime's no-install example-render pass: tracked obligations still run, while ignored generated
285
283
  * artifact presence/freshness belongs to the default `workspace` phase after codegen. Ordinary app check/lint always use it.
286
284
  */
287
- export function checkRepo({ repoRoot, root = skillRoot, declaration, files, only, extraFindings = [], tree = files === undefined, manifest = loadSlotManifest({ root }), surface = 'all', phase = 'workspace' }) {
285
+ export function checkRepo({ repoRoot, root = skillRoot, harness, declaration, files, only, extraFindings = [], tree = files === undefined, manifest = loadSlotManifest({ root }), surface = 'all', phase = 'workspace' }) {
288
286
  if (!['source', 'workspace'].includes(phase)) throw new Error(`unknown HFS inspection phase ${phase}`);
289
287
  const why = readWhy(root);
290
288
  let repo;
@@ -303,18 +301,18 @@ export function checkRepo({ repoRoot, root = skillRoot, declaration, files, only
303
301
  const keep = (list, scopeRoot) => (surface === 'check' ? list.filter((finding) => !(finding.origin === 'repo' && onLintSurface(scopeRoot, finding))) : list);
304
302
  if (repo.profile === APP_SCOPE) {
305
303
  const own = tracked.filter((file) => resolver.sideOf(file) === null);
306
- findings.push(...keep(scopeFindings({ repoRoot, root, repo, resolver, files: own, all: tracked, scoped, manifest, phase }), repoRoot));
304
+ findings.push(...keep(scopeFindings({ repoRoot, root, harness, repo, resolver, files: own, all: tracked, scoped, manifest, phase }), repoRoot));
307
305
  for (const side of SIDES) {
308
306
  const prefix = `${side}/`;
309
307
  const sideRoot = path.join(repoRoot, side);
310
308
  const sideFiles = tracked.filter((file) => file.startsWith(prefix)).map((file) => file.slice(prefix.length));
311
309
  const sideScoped = scoped ? new Set([...scoped].filter((file) => file.startsWith(prefix)).map((file) => file.slice(prefix.length))) : null;
312
- const sideFindings = scopeFindings({ repoRoot: sideRoot, root, repo: repo.sides[side], resolver: resolver.sides[side], files: sideFiles, scoped: sideScoped, manifest, appSide: true, editionDeclaration: false, phase });
310
+ const sideFindings = scopeFindings({ repoRoot: sideRoot, root, harness, repo: repo.sides[side], resolver: resolver.sides[side], files: sideFiles, scoped: sideScoped, manifest, appSide: true, editionDeclaration: false, phase });
313
311
  const message = appRelativeMessages(side, sideRoot);
314
312
  findings.push(...keep(sideFindings, sideRoot).map((finding) => ({ ...finding, side, path: onSide(side, finding.path), message: message(finding.message) })));
315
313
  }
316
314
  } else {
317
- findings.push(...keep(scopeFindings({ repoRoot, root, repo, resolver, files: tracked, scoped, manifest, phase }), repoRoot));
315
+ findings.push(...keep(scopeFindings({ repoRoot, root, harness, repo, resolver, files: tracked, scoped, manifest, phase }), repoRoot));
318
316
  }
319
317
  findings.push(...extraFindings);
320
318
  const untrackedSecrets = untrackedSecretFindings({ repoRoot, files: tracked, repo });
@@ -367,9 +365,9 @@ function machineFindings(report) {
367
365
  * templates or run the formatter (packages/hfs/sync), judged with checkRepo's own. `machine` is injectable for specs. A missing
368
366
  * merge-base under `fast` is an Error, never a silent full pass. `repoRoot` may also be a side folder: that side alone is checked.
369
367
  */
370
- export function checkRepository({ repoRoot, root = skillRoot, fast = false, base, extraFindings = [], manifest = loadSlotManifest({ root }), machine = checkArchitecture, wiring = false, coverageFrom }) {
368
+ export function checkRepository({ repoRoot, root = skillRoot, harness, fast = false, base, extraFindings = [], manifest = loadSlotManifest({ root }), machine = checkArchitecture, wiring = false, coverageFrom }) {
371
369
  const changed = fast ? changedSince(repoRoot, base) : null;
372
- const slotResult = checkRepo({ repoRoot, root, manifest, extraFindings, surface: 'check', ...(changed ? { only: changed.files, tree: false } : {}) });
370
+ const slotResult = checkRepo({ repoRoot, root, harness, manifest, extraFindings, surface: 'check', ...(changed ? { only: changed.files, tree: false } : {}) });
373
371
  if (slotResult.profile === null) return { ...slotResult, machine: { status: 'skipped', reason: 'hfs.json is not valid' } };
374
372
  const repo = readRepoDeclaration(manifest, repoRoot);
375
373
  const scopes = repo.profile === APP_SCOPE
@@ -1,8 +1,8 @@
1
1
  // pipeline.mjs - HFS_CI_MISSING_CANON (R13): CI runs the pinned `starci app lint`.
2
2
  // .github/workflows/ci.yml a `run:` step that runs the one lint entry: `starci app lint`,
3
3
  // `npx starci-harness[@<version>] app lint`, or `npm run <script>` whose command runs `starci app lint`
4
- // (`npm run lint`, arguments after `--` allowed); a version named in the step is the starci-harness pin of
5
- // canon-pins.yaml (an installed one is pinned by the pin check, R15)
4
+ // (`npm run lint`, arguments after `--` allowed); a version named in the step is the starci-harness pin,
5
+ // the version of the CLI that runs the check (canon-pins.mjs; an installed one is pinned by the pin check, R15)
6
6
  // (the push hook is no lint or typecheck step: it checks the release gate, scripts/hfs/runtime-rules/hook-shape.mjs keeps its shape)
7
7
  // A missing file is the slot manifest's finding (HFS_SLOT_REQUIRED_MISSING); a file that is present and lacks the step is this rule's.
8
8
  // The redirect of `core.hooksPath` away from husky is the architecture machine's (HFS_HOOKS_PATH_REDIRECTED).
@@ -77,7 +77,7 @@ function expanded(command, scripts) {
77
77
  /** Only the bundled example runner with its fixed current-app arguments delegates to the following argv. */
78
78
  const exampleCommandOf = (command) => command.startsWith(EXAMPLE_RUN_PREFIX) ? command.slice(EXAMPLE_RUN_PREFIX.length) : command;
79
79
 
80
- /** The findings of R13 over the repository at `repoRoot`; `pins` is the parsed canon-pins.yaml pin map. */
80
+ /** The findings of R13 over the repository at `repoRoot`; `pins` is the canon pin map (canon-pins.mjs). */
81
81
  export function pipelineFindings({ repoRoot, files, pins }) {
82
82
  const findings = [];
83
83
  const pinned = pins?.[hfsPackage]?.version;
@@ -7,7 +7,8 @@ import {
7
7
  } from "../runtime/scripts/hfs/slots.mjs";
8
8
  import { imageFiles, render, TEMPLATES_DIR } from "../sync/index.mjs";
9
9
  import { ScaffoldError, pascalOf } from "./service.mjs";
10
- import { PARTIAL_VARIABLES, bundledPins, callbackSourceFiles, coreFeApps, hasAuthCallback, jsonText, packageJsonText } from "./app.mjs";
10
+ import { bundledPins } from "../runtime/scripts/hfs/canon-pins.mjs";
11
+ import { PARTIAL_VARIABLES, callbackSourceFiles, coreFeApps, hasAuthCallback, jsonText, packageJsonText } from "./app.mjs";
11
12
 
12
13
  /** The shared fe app core (modules/api, auth, toast, redux, the providers, the hooks): the base tree, then the edition overlay. */
13
14
  const CORE_DIRS = Object.freeze({
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { resolveRepoDeclaration } from "../runtime/scripts/hfs/slots.mjs";
4
- import { parseYaml } from "../runtime/engine/yaml.mjs";
4
+ import { bundledPins } from "../runtime/scripts/hfs/canon-pins.mjs";
5
5
  import { appScripts, appSource, imageFiles, TEMPLATES_DIR } from "../sync/index.mjs";
6
6
  import { ScaffoldError } from "./service.mjs";
7
7
  import { jsonText, packageJsonText } from "./app.mjs";
@@ -12,7 +12,6 @@ const read = (relative) =>
12
12
  fs
13
13
  .readFileSync(path.join(TEMPLATES_DIR, ...relative.split("/")), "utf8")
14
14
  .replace(/\r\n/g, "\n");
15
- const PINS_FILE = path.join(import.meta.dirname, "..", "runtime", "knowledge", "hfs", "canon-pins.yaml");
16
15
 
17
16
 
18
17
  const managedScripts = (repo, source) => {
@@ -274,7 +273,7 @@ export function ensureLiteCli({ root, manifest, repo }) {
274
273
  if (!fs.existsSync(packageFile))
275
274
  throw new ScaffoldError("HFS_ADD_NOT_AN_APP", "add cli needs the app root package.json");
276
275
  const packageManifest = JSON.parse(fs.readFileSync(packageFile, "utf8"));
277
- const pin = parseYaml(fs.readFileSync(PINS_FILE, "utf8")).pins?.["nest-commander"]?.version;
276
+ const pin = bundledPins()["nest-commander"]?.version;
278
277
  if (typeof pin !== "string" || pin === "")
279
278
  throw new ScaffoldError("HFS_ADD_CANON_PIN_MISSING", "add cli needs the nest-commander canon pin");
280
279
  packageManifest.dependencies = { ...packageManifest.dependencies, "nest-commander": pin };
@@ -13,7 +13,8 @@ import { materializeDeclaredPlaceholders } from '../runtime/scripts/hfs/sealed-m
13
13
  import { REDIS_IMAGE, TEMPLATES_DIR, redisDevScripts, render } from '../sync/index.mjs';
14
14
  import { byCodeUnit } from '../report/order.mjs';
15
15
  import { withSqlIdentifierError } from './add-database-lite.mjs';
16
- import { bundledPins, jsonText, packageJsonText } from './app.mjs';
16
+ import { jsonText, packageJsonText } from './app.mjs';
17
+ import { bundledPins } from '../runtime/scripts/hfs/canon-pins.mjs';
17
18
  import { registerLiteExports } from './lite-exports.mjs';
18
19
  import { ScaffoldError } from './service.mjs';
19
20
  import { YamlBlockEdit } from './yaml-block-edit.mjs';
package/scaffold/add.mjs CHANGED
@@ -25,7 +25,8 @@ import { addLiteWebhookInbox } from './add-inbox-lite.mjs';
25
25
  import { planLiteQueue } from './add-queue-lite.mjs';
26
26
  import { addBuiltinLiteCli, fillKnownForms, formsOf, planKind, readAddContext } from './add-plan.mjs';
27
27
  export { liteApiTemplates } from './add-plan.mjs';
28
- import { bundledPins, jsonText, packageJsonText, peerDependencies } from './app.mjs';
28
+ import { jsonText, packageJsonText, peerDependencies } from './app.mjs';
29
+ import { bundledPins } from '../runtime/scripts/hfs/canon-pins.mjs';
29
30
  import { byCodeUnit } from '../report/order.mjs';
30
31
  import { registerLiteDomainService, registerLiteExports } from './lite-exports.mjs';
31
32
 
package/scaffold/app.mjs CHANGED
@@ -17,6 +17,7 @@ import path from 'node:path';
17
17
  import { spawnSync } from 'node:child_process';
18
18
  import { loadSlotManifest, resolveRepoDeclaration } from '../runtime/scripts/hfs/slots.mjs';
19
19
  import { parseYaml } from '../runtime/engine/yaml.mjs';
20
+ import { HARNESS_PACKAGE, canonPins } from '../runtime/scripts/hfs/canon-pins.mjs';
20
21
  import { dbTypesPath, generateDbTypes } from '../emit/db-types.mjs';
21
22
  import { TEMPLATES_DIR, appSource, imageFiles, render, renderTargets, writeTargets } from '../sync/index.mjs';
22
23
  import { ScaffoldError } from './service.mjs';
@@ -40,7 +41,6 @@ const APP_DIR = '__app__';
40
41
  /** The path variable of the project name: `packages/__project__-ui` is written as `packages/<project>-ui`. */
41
42
  const PROJECT_DIR = '__project__';
42
43
  const TIMESTAMP = '__timestamp__';
43
- const PINS_FILE = path.join(import.meta.dirname, '..', 'runtime', 'knowledge', 'hfs', 'canon-pins.yaml');
44
44
  /**
45
45
  * The front-end stack catalog (knowledge/hfs/fe-stack.yaml): the bundled runtime copy, else the runtime checkout's own file (the
46
46
  * package inside the StarCi repository before the runtime copy carries it).
@@ -53,8 +53,6 @@ const SONAR_GATE_FILE = path.join(import.meta.dirname, '..', 'runtime', 'knowled
53
53
  /** The runtime-peer catalog (knowledge/hfs/peer-integrations.yaml, R111) the package carries. */
54
54
  const PEERS_FILE = path.join(import.meta.dirname, '..', 'runtime', 'knowledge', 'hfs', 'peer-integrations.yaml');
55
55
  const pascal = name => name.split('-').map(part => part[0].toUpperCase() + part.slice(1)).join('');
56
- /** The canon pins (knowledge/hfs/canon-pins.yaml) the package carries. */
57
- export const bundledPins = () => parseYaml(fs.readFileSync(PINS_FILE, 'utf8')).pins;
58
56
 
59
57
  /** The parsed front-end stack catalog; a package without it cannot know what every front end installs. */
60
58
  function bundledFeStack() {
@@ -637,7 +635,8 @@ function emitScaffoldDatabaseTypes(root, name, writes, emitTypes) {
637
635
  * `starci app scaffold <name>`: writes the new app under `into`, resolves its lockfile with npm (`lock`, npmLock), and returns
638
636
  * `{ root, files }` (app-relative paths, sorted). A new path is first made an empty repository with unborn main (`initGit`), so the
639
637
  * app always lives in its own Git repository. `presets` are the jest preset's Sonar exclusions; the CLI passes the copy @starci/hfs
640
- * ships (bundledPresets). Failure removes only scaffold-owned entries, retaining the .git.
638
+ * ships (bundledPresets). `harness` ({ version }) is the CLI that runs the scaffold: the root devDependencies name starci-harness at
639
+ * its version (canon-pins.mjs). Failure removes only scaffold-owned entries, retaining the .git.
641
640
  */
642
641
  /** Seal declared starter members to a fresh app recipient through the scaffold rollback owner. */
643
642
  function initializeScaffoldSecrets(root, writes) {
@@ -659,13 +658,14 @@ function bornRepository(root, initGit) {
659
658
  return fs.lstatSync(root);
660
659
  }
661
660
 
662
- export function scaffoldApp({ name, into, presets, edition = 'full', authCallback = false, registryVisibility = 'public', manifest = loadSlotManifest(), pins = bundledPins(), lock = npmLock, emitTypes = generateDbTypes, initGit = gitInit, now = () => new Date() }) {
661
+ export function scaffoldApp({ name, into, presets, harness, edition = 'full', authCallback = false, registryVisibility = 'public', manifest = loadSlotManifest(), pins = canonPins({ harness }), lock = npmLock, emitTypes = generateDbTypes, initGit = gitInit, now = () => new Date() }) {
663
662
  if (!NAME.test(String(name))) throw new ScaffoldError('HFS_SCAFFOLD_NAME_INVALID', `the app name ${name} must be kebab-case (a project name: ${NAME})`);
664
663
  const root = path.resolve(into, name);
665
664
  const existing = fs.lstatSync(root, { throwIfNoEntry: false });
666
665
  if (existing && !emptyUnbornGit(root, existing)) throw new ScaffoldError('HFS_SCAFFOLD_EXISTS', `${root} already exists; only a plain directory containing solely .git with unborn main and an empty index can be scaffolded`);
667
666
  if (!['public', 'private'].includes(registryVisibility)) throw new ScaffoldError('HFS_SCAFFOLD_REGISTRY_VISIBILITY', `--registry-visibility accepts public or private (got ${registryVisibility})`);
668
667
  if (typeof authCallback !== 'boolean') throw new ScaffoldError('HFS_SCAFFOLD_NAME_INVALID', 'authCallback must be a boolean');
668
+ if (!pins[HARNESS_PACKAGE]) throw new ScaffoldError('HFS_SCAFFOLD_PIN_MISSING', `${HARNESS_PACKAGE} has no pin: an app pins the CLI that scaffolds it, so run the scaffold through it (starci app scaffold)`);
669
669
  const original = existing ?? bornRepository(root, initGit);
670
670
  const declaration = starterDeclaration(name, manifest, edition);
671
671
  if (authCallback) enableAuthCallback(declaration);
package/src/main.mjs CHANGED
@@ -242,7 +242,7 @@ function printExplain(e, out) {
242
242
  * The app pass: slots, managed files, formatter, contract snapshots and the architecture machine's check surface, root and sides.
243
243
  * `only` limits the formatter to those (app-relative) files.
244
244
  */
245
- async function runCheck({ repoRoot, fast = false, base, only, presets, prettier, dbTypes = false }) {
245
+ async function runCheck({ repoRoot, harness, fast = false, base, only, presets, prettier, dbTypes = false }) {
246
246
  const tracked = trackedFiles(repoRoot);
247
247
  // Managed files of the root and both sides, the .gitignore block and sonar against their render (R04, R05, R11, ...), and prettier
248
248
  // through the app's own install (R19, never under --fast).
@@ -260,7 +260,7 @@ async function runCheck({ repoRoot, fast = false, base, only, presets, prettier,
260
260
  contracts = { status: 'checked', apps: emitted.apps };
261
261
  }
262
262
  }
263
- const result = checkRepository({ repoRoot, fast, base, extraFindings });
263
+ const result = checkRepository({ repoRoot, harness, fast, base, extraFindings });
264
264
  if (contracts) result.contracts = contracts;
265
265
  return result;
266
266
  }
@@ -274,7 +274,7 @@ export function appRootAbove(dir) {
274
274
  }
275
275
 
276
276
  /** `starci app lint`: ESLint per side, the app checks and stylelint over the fe side as one report; see lint/run.mjs. */
277
- async function lintMain(argv, { cwd, stdout, presets, prettier, runLinter, checkApp = runCheck }) {
277
+ async function lintMain(argv, { cwd, harness, stdout, presets, prettier, runLinter, checkApp = runCheck }) {
278
278
  const opts = parseLintArgs(argv);
279
279
  // --workspace names a folder from the working directory (`starci app lint --workspace .` is an fe workspace script); the app
280
280
  // root is then the nearest folder above it that holds hfs.json, and the workspace is passed on app-relative.
@@ -283,7 +283,7 @@ async function lintMain(argv, { cwd, stdout, presets, prettier, runLinter, check
283
283
  if (workspaceDir) opts.workspace = path.relative(repoRoot, workspaceDir).split(path.sep).join('/');
284
284
  const { report, sonar, exit } = await lintRepository({
285
285
  repoRoot, opts, trackedFiles, runLinter,
286
- hfsCheck: (root) => checkApp({ repoRoot: root, only: opts.changed ?? undefined, presets, prettier }),
286
+ hfsCheck: (root) => checkApp({ repoRoot: root, harness, only: opts.changed ?? undefined, presets, prettier }),
287
287
  });
288
288
  if (opts.sonar !== undefined) writeReport(path.resolve(cwd, opts.sonar), sonar);
289
289
  if (opts.format === 'json') stdout(`${JSON.stringify(report, null, 2)}
@@ -346,10 +346,10 @@ function printCheckProofs(result, out) {
346
346
  if (result.coverage) out(formatCoverageReport(result.coverage));
347
347
  }
348
348
 
349
- async function checkMain(opts, { cwd: repoRoot, edition, stdout: out, presets, prettier, checkApp = runCheck, runLinter }) {
349
+ async function checkMain(opts, { cwd: repoRoot, harness, edition, stdout: out, presets, prettier, checkApp = runCheck, runLinter }) {
350
350
  if (opts.positional.length) throw new Error('starci app check takes no path');
351
351
  if (opts.base !== undefined && opts.fast !== true) throw new Error('--base names the ref --fast compares with; it needs --fast');
352
- const checked = await checkEdition({ repoRoot, edition, normal: () => checkApp({ repoRoot, fast: opts.fast === true, base: opts.base, presets, prettier, dbTypes: opts['db-types'] === true }), presets, prettier });
352
+ const checked = await checkEdition({ repoRoot, edition, normal: () => checkApp({ repoRoot, harness, fast: opts.fast === true, base: opts.base, presets, prettier, dbTypes: opts['db-types'] === true }), presets, prettier });
353
353
  const proof = withCheckProofs(checked, { repoRoot, edition, wiring: opts.wiring === true, coverageFrom: opts['coverage-from'] });
354
354
  const changed = opts.fast === true ? changedSince(repoRoot, proof.fast?.base ?? opts.base).files : null;
355
355
  const existing = changed ? changed.filter(file => fs.existsSync(path.join(repoRoot, file))) : [];
@@ -397,10 +397,10 @@ function addMain(opts, { cwd: repoRoot, stdout: out }) {
397
397
  return 0;
398
398
  }
399
399
 
400
- async function scaffoldMain(opts, { cwd, edition = 'full', presets, lock, stdout: out }) {
400
+ async function scaffoldMain(opts, { cwd, harness, edition = 'full', presets, lock, stdout: out }) {
401
401
  const [name, ...extra] = opts.positional;
402
402
  if (!name || extra.length) throw new Error('starci app scaffold takes `<name> [--into <dir>] [--edition full|lite] [--registry-visibility public|private]`');
403
- const { root: created, files } = scaffoldApp({ name, into: path.resolve(cwd, opts.into ?? '.'), edition, presets: presets ?? scaffoldPresets(), ...(opts['registry-visibility'] === undefined ? {} : { registryVisibility: opts['registry-visibility'] }), ...(lock ? { lock } : {}) });
403
+ const { root: created, files } = scaffoldApp({ name, into: path.resolve(cwd, opts.into ?? '.'), harness, edition, presets: presets ?? scaffoldPresets(), ...(opts['registry-visibility'] === undefined ? {} : { registryVisibility: opts['registry-visibility'] }), ...(lock ? { lock } : {}) });
404
404
  out(`starci app scaffold: created ${created} (${files.length} files); next: starci npm ci (the lockfile, then the git hooks through npm run prepare; after a raw npm ci --ignore-scripts run npm run prepare), then starci app lint\n`);
405
405
  return 0;
406
406
  }
@@ -449,9 +449,13 @@ function errorMessage(error) {
449
449
  return `starci app: ${error.message}\n${USAGE}`;
450
450
  }
451
451
 
452
- /** `run` injects the Supabase runner, `runLinter` the linter transport, `checkApp` the HFS check and `stackIo` Swarm tools; production defaults execute every check. */
452
+ /**
453
+ * `harness` ({ version }) is the CLI that runs this package in-process: its version is the starci-harness an app is judged and
454
+ * scaffolded at (runtime/scripts/hfs/canon-pins.mjs). `run` injects the Supabase runner, `runLinter` the linter transport, `checkApp`
455
+ * the HFS check and `stackIo` Swarm tools; production defaults execute every check.
456
+ */
453
457
  export async function main(argv, {
454
- cwd: baseCwd = process.cwd(), stdout = (s) => process.stdout.write(s), stderr = (s) => process.stderr.write(s), presets, prettier, lock, run, stackMain,
458
+ cwd: baseCwd = process.cwd(), harness, stdout = (s) => process.stdout.write(s), stderr = (s) => process.stderr.write(s), presets, prettier, lock, run, stackMain,
455
459
  env = process.env, stdin, sops, tools, platform, stackIo, runLinter, checkApp,
456
460
  } = {}) {
457
461
  try {
@@ -460,7 +464,7 @@ export async function main(argv, {
460
464
  const out = global.quiet ? () => {} : stdout;
461
465
  if (global.help) { out(USAGE); return 0; }
462
466
  if (!VERBS.includes(verb)) { stderr(USAGE); return 2; }
463
- return await dispatchMain(verb, rest, { cwd: global.cwd, edition: global.edition, stdout: out, stderr, presets, prettier, lock, run, stackMain, env, stdin, sops, tools, platform, stackIo, runLinter, checkApp });
467
+ return await dispatchMain(verb, rest, { cwd: global.cwd, harness, edition: global.edition, stdout: out, stderr, presets, prettier, lock, run, stackMain, env, stdin, sops, tools, platform, stackIo, runLinter, checkApp });
464
468
  } catch (error) {
465
469
  stderr(errorMessage(error));
466
470
  return 2;
package/upgrade/index.mjs CHANGED
@@ -7,20 +7,12 @@
7
7
  import fs from "node:fs";
8
8
  import path from "node:path";
9
9
  import { remember, ensureParent, writeFile, restore } from "./file-state.mjs";
10
- import { parseYaml } from "../runtime/engine/yaml.mjs";
10
+ import { bundledPins } from "../runtime/scripts/hfs/canon-pins.mjs";
11
11
  import { loadSlotManifest } from "../runtime/scripts/hfs/slots.mjs";
12
12
  import { LOCK_STEP, npmLock } from "../scaffold/app.mjs";
13
13
  import { WORLD_MARKER, testWorldFiles } from "../scaffold/test-world.mjs";
14
14
  import { loadHfs } from "../sync/index.mjs";
15
15
 
16
- const PACKAGE_ROOT = path.join(import.meta.dirname, "..");
17
- const PINS_FILE = path.join(
18
- PACKAGE_ROOT,
19
- "runtime",
20
- "knowledge",
21
- "hfs",
22
- "canon-pins.yaml",
23
- );
24
16
  const FULL_DEV_DEPENDENCIES = Object.freeze([
25
17
  "@nestjs/testing",
26
18
  "@starci/jest-preset",
@@ -111,7 +103,7 @@ function fullCompletion(root, declaration) {
111
103
  (name) => current.devDependencies?.[name] === undefined && current.dependencies?.[name] === undefined,
112
104
  );
113
105
  if (missing.length) {
114
- const pins = parseYaml(fs.readFileSync(PINS_FILE, "utf8")).pins;
106
+ const pins = bundledPins();
115
107
  const added = pinnedPackage(root, pins).devDependencies;
116
108
  const devDependencies = Object.fromEntries(
117
109
  Object.entries({ ...current.devDependencies, ...Object.fromEntries(missing.map((name) => [name, added[name]])) })