@starci/hfs 4.0.6 → 4.0.8
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 +17 -0
- package/README.md +10 -9
- package/bin/hfs.mjs +4 -4
- package/lint/run.mjs +1 -1
- package/package.json +2 -2
- package/runtime/engine/runtime-root.mjs +5 -0
- package/runtime/engine/yaml.mjs +3 -3
- package/runtime/knowledge/hfs/canon-pins.yaml +10 -10
- package/runtime/knowledge/hfs/slots.yaml +5 -5
- package/runtime/knowledge/patterns/fe/folder.yaml +2 -2
- package/runtime/knowledge/sonar-gate.yaml +2 -2
- package/runtime/modules/kernel/failure-codes.yaml +12 -2
- package/runtime/scripts/api/fs/lib.mjs +6 -0
- package/runtime/scripts/api/fs/rmdir-link.mjs +1 -1
- package/runtime/scripts/{lib → api/fs}/safe-remove.mjs +31 -82
- package/runtime/scripts/api/git/lib.mjs +1 -1
- package/runtime/scripts/api/sops/decrypt.mjs +16 -0
- package/runtime/scripts/{lib/hfs-allows.mjs → hfs/allows.mjs} +3 -3
- package/runtime/scripts/{checks → hfs}/architecture/backend.mjs +1 -1
- package/runtime/scripts/{checks → hfs}/architecture/config.mjs +2 -2
- package/runtime/scripts/{checks → hfs}/architecture/connection-map.mjs +1 -1
- package/runtime/scripts/{checks → hfs}/architecture/contract-fixture-guard.mjs +1 -1
- package/runtime/scripts/{checks → hfs}/architecture/fe-slot-allows.mjs +3 -3
- package/runtime/scripts/{checks → hfs}/architecture/feature-shape.mjs +1 -1
- package/runtime/scripts/{checks → hfs}/architecture/hfs-graph.mjs +1 -1
- package/runtime/scripts/{checks → hfs}/architecture/hfs.mjs +3 -3
- package/runtime/scripts/hfs/architecture/next-data-contract.mjs +96 -0
- package/runtime/scripts/{checks → hfs}/architecture/next-data.mjs +18 -105
- package/runtime/scripts/{checks → hfs}/architecture/required-files.mjs +1 -1
- package/runtime/scripts/{checks → hfs}/architecture/surface.mjs +1 -1
- package/runtime/scripts/{checks → hfs}/architecture/test-world-files.mjs +2 -2
- package/runtime/scripts/{checks → hfs}/architecture/typescript.mjs +1 -1
- package/runtime/scripts/{checks → hfs}/architecture.mjs +2 -2
- package/runtime/scripts/{lib/hfs-check.mjs → hfs/check.mjs} +33 -25
- package/runtime/scripts/hfs/manifest-shape.mjs +170 -0
- package/runtime/scripts/{lib/hfs-path-findings.mjs → hfs/path-findings.mjs} +6 -6
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/contract.mjs +3 -2
- package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +66 -0
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/integration-specs.mjs +1 -1
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/secrets.mjs +2 -2
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/stacks.mjs +2 -2
- package/runtime/scripts/{lib/hfs-slots.mjs → hfs/slots.mjs} +52 -58
- package/runtime/scripts/{lib/hfs-tree.mjs → hfs/tree.mjs} +1 -1
- package/runtime/scripts/{lib/hfs-view.mjs → hfs/view.mjs} +3 -3
- package/runtime/scripts/lib/graphql-contract.mjs +429 -0
- package/runtime/scripts/lib/is-main.mjs +13 -0
- package/runtime/scripts/lib/language.mjs +2 -2
- package/runtime/scripts/lib/path-key.mjs +2 -0
- package/runtime/scripts/lib/stack-declaration.mjs +2 -2
- package/runtime/scripts/lib/test-secrets.mjs +2 -6
- package/runtime/scripts/{checks/common.mjs → lib/walk.mjs} +1 -12
- package/scaffold/app.mjs +1 -1
- package/scaffold/service.mjs +2 -2
- package/sync/hygiene.mjs +8 -8
- package/sync/index.mjs +48 -11
- package/templates/app/quality-config/codecov.yml +1 -1
- package/templates/app/quality-config/sonar-project.properties +1 -1
- package/templates/be/skeleton/src/modules/platform/http-security/origin.guard.ts +2 -2
- package/templates/fe/skeleton/apps/__app__/next.config.ts +10 -2
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/error.tsx +2 -2
- package/templates/fe/skeleton/apps/__app__/src/features/pages/ErrorPage/component.tsx +0 -2
- package/templates/fe/skeleton/apps/__app__/src/features/pages/ErrorPage/index.tsx +1 -7
- package/templates/fe/skeleton/apps/__app__/src/features/pages/GlobalErrorPage/component.tsx +0 -2
- package/templates/fe/skeleton/apps/__app__/src/features/pages/GlobalErrorPage/index.tsx +0 -1
- package/templates/fe/skeleton/apps/__app__/src/features/pages/HomePage/component.tsx +0 -4
- package/templates/fe/skeleton/apps/__app__/src/features/pages/HomePage/index.tsx +1 -1
- package/templates/fe/skeleton/apps/__app__/src/features/pages/LoadingPage/component.tsx +0 -4
- package/templates/fe/skeleton/apps/__app__/src/features/pages/LoadingPage/index.tsx +1 -1
- package/templates/fe/skeleton/apps/__app__/src/features/pages/NotFoundPage/component.tsx +0 -4
- package/templates/fe/skeleton/apps/__app__/src/features/pages/NotFoundPage/index.tsx +1 -1
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/request.ts +8 -3
- package/templates/fe/skeleton/apps/__app__/src/proxy.ts +1 -1
- package/runtime/engine/admission.mjs +0 -284
- package/runtime/engine/digest.mjs +0 -10
- package/runtime/engine/ledger-db.mjs +0 -1245
- package/runtime/engine/machine-db.mjs +0 -1565
- package/runtime/engine/migrations/machine/0001-init.sql +0 -887
- package/runtime/engine/migrations/machine/0002-worktrees-no-workflow-kind.sql +0 -13
- package/runtime/engine/migrations/machine/0003-worktrees-workflow-orca.sql +0 -21
- package/runtime/engine/migrations/runtime/0001-init.sql +0 -1072
- package/runtime/engine/migrations/runtime/0003-usage-unavailable.sql +0 -13
- package/runtime/engine/migrations/runtime/0004-attempt-why.sql +0 -43
- package/runtime/engine/migrations/runtime/0005-ended-workflow-views.sql +0 -93
- package/runtime/scripts/lib/artifact-hold.mjs +0 -89
- package/runtime/scripts/lib/artifact-store.mjs +0 -103
- package/runtime/scripts/lib/redact.mjs +0 -148
- /package/runtime/scripts/{checks → hfs}/architecture/background-unowned.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/client-reaches-server.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/clones.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/config-unread.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/constructor-deps.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/contracts.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/cross-app-duplicate.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/dead-exports.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/default-deny.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/doc-language.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/entrypoint.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/error-codes.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/error-masked.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/framework-pinned.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/frontend.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/hooks-are-hooks.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/i18n-keys.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/index.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/injection-token-exported.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/machine-ast.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/module-per-transport.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/owners.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/package-shape.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/reachability.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/register-once.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/registration.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/route-files-thin.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/schema-owner.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/source-names.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/sql-owner.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/sql-tokens.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/symbols.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/tiers.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/transport-owner.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/unit-spec-providers.mjs +0 -0
- /package/runtime/scripts/{lib → hfs}/repo-identity.mjs +0 -0
- /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/deps.mjs +0 -0
- /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/fe-no-tests.mjs +0 -0
- /package/runtime/scripts/{lib/hfs-rules/frontend.mjs → hfs/rules/frontend-tree.mjs} +0 -0
- /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/lint-suppression.mjs +0 -0
- /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/peer-integrations.mjs +0 -0
- /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/pipeline.mjs +0 -0
- /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/proof-commands.mjs +0 -0
- /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/read.mjs +0 -0
- /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/repo-local-checks.mjs +0 -0
- /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/spec-placement.mjs +0 -0
- /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/test-topology.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/typescript-programs.mjs +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
- Changed: the managed fe scripts (`dev:fe`, `start:<app>`, `build:fe`) run `next` from the app directory (`cd fe/apps/<app> && next ...`) so next-intl finds its request config; the fe skeleton reads the request locale through `next/root-params` (`experimental.rootParams`), pins the workspace root three levels above the app and uses the `.*[.].*` proxy matcher; the bundled runtime carries the `&&`/`||`-aware FE_SWR_KEY_IDENTITY check.
|
|
6
|
+
## Unreleased (C0 batch)
|
|
7
|
+
|
|
8
|
+
- Fixed (contract change `sonar-coverage-exclusions`): SonarQube has no `sonar.coverage.inclusions` property, so Sonar ignored it and counted every executable be/ file at 0% (overall coverage 65.8% and 45.2% on the examples, although every service was at 100). The managed `sonar-project.properties` now renders `sonar.coverage.exclusions` as the complement of the services. `coverageExclusions(presets, manifest)` in `sync/index.mjs` derives it from the preset's `COVERAGE_SOURCES` and the slot manifest, and lists:
|
|
9
|
+
- every be role of the R89 suffix vocabulary other than `service`;
|
|
10
|
+
- the be file names a slot declares outside it (`main.ts`, `index.ts`, `connection.ts`, the migrations, the test world's files);
|
|
11
|
+
- `fe/**`.
|
|
12
|
+
|
|
13
|
+
A coverage source whose complement Sonar's globs cannot write is refused (`HFS_SYNC_COVERAGE_SCOPE`). `sonar.coverage.inclusions` is deleted.
|
|
14
|
+
- Fixed: the fe skeleton's `[locale]/error.tsx` exports `ErrorBoundary` (Sonar S2137: `Error` shadowed the global).
|
|
15
|
+
|
|
16
|
+
## 4.0.7 - 2026-10-01
|
|
17
|
+
|
|
18
|
+
- Changed: the runtime layer check (scripts/api/<system>, a pure scripts/lib) in its bundled runtime.
|
|
19
|
+
|
|
3
20
|
## 4.0.6 - 2026-10-01
|
|
4
21
|
|
|
5
22
|
- Changed: R112 integration specs, the runtime api layer, the examples-root CI scope and the new pins.
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ and it is self-contained: `runtime/` carries the slot manifest, the canon pins,
|
|
|
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.
|
|
7
7
|
|
|
8
|
-
Every command runs at the app root. A side is judged with its folder as the root it was when products were split in two repositories (the "side view" of `scripts/
|
|
8
|
+
Every command runs at the app root. A side is judged with its folder as the root it was when products were split in two repositories (the "side view" of `scripts/hfs/slots.mjs`): the root slots (`app.*`) are judged once, the `be.*` slots under `be/` and the `fe.*` slots under `fe/`, and nothing crosses sides except `sides.fe.reads` (`be/contracts/`, the input of the front end's codegen). Every finding path is app-relative (`be/src/...`, `fe/apps/...`).
|
|
9
9
|
|
|
10
10
|
```sh
|
|
11
11
|
npx hfs lint [--repo <dir>] [--changed <file>...] [--fix] [--format text|json] [--sonar <file>] [--stylelint <glob>] # THE lint entry (`npm run lint`): ESLint over be/ with the BE canon and over fe/ with the FE canon, stylelint over fe/, the hfs checks; exit 0 clean, 1 findings, 2 a tool could not run
|
|
@@ -32,7 +32,7 @@ The spec skeleton is `Test.createTestingModule({ providers: [Service, { provide:
|
|
|
32
32
|
whole architecture machine over each side folder. Every finding carries a why code and its Vietnamese text
|
|
33
33
|
(`modules/kernel/failure-codes.yaml`).
|
|
34
34
|
|
|
35
|
-
Its own checks (`scripts/
|
|
35
|
+
Its own checks (`scripts/hfs/check.mjs`, `scripts/hfs/rules/`; the rendered-file checks (`sync/managed.mjs`) and the prettier check (`sync/format.mjs`) live in `sync/` because they read the templates and the repository's own install, and reach `hfs check` as `extraFindings`; the jest / vitest preset the coverage exclusions come from and prettier are read from the repository's `node_modules`, so run `npm ci` first):
|
|
36
36
|
|
|
37
37
|
| Code | Level | Meaning |
|
|
38
38
|
|---|---|---|
|
|
@@ -63,13 +63,14 @@ Its own checks (`scripts/lib/hfs-check.mjs`, `scripts/lib/hfs-rules/`; the rende
|
|
|
63
63
|
| `HFS_REPO_LOCAL_CHECK` | error | a `check-*` file in `scripts/` or `tools/`, an `eslint-local-rules*` file or local eslint plugin, or a script that runs a local check (R103) |
|
|
64
64
|
| `HFS_LINT_SUPPRESSION_FILE` | error | an `eslint.suppressions*` file, a `lint:suppressions` script, an eslint suppress flag or a suppressions config (R104) |
|
|
65
65
|
| `HFS_PROOF_COMMAND_FILE_MISSING` | error | a `.starciwork` `requiresProof.<kind>.command` that runs a file the repository does not hold (R105) |
|
|
66
|
+
| `FE_GRAPHQL_CONTRACT` | error | a front-end `.graphql` document that the back end's contract snapshot (`be/contracts/<service>/schema.graphql`) does not serve: an unknown field, argument or input field, a missing required argument, a variable of another type, or a selection that does not fit (R113) |
|
|
66
67
|
| `BE_INTEGRATION_SPEC_MISSING` | error | an integration (`src/modules/integrations/<provider>/` with `<provider>.config.ts`) without a `src/tests/integration/<provider>/*.integration-spec.ts` that registers its module through `useTestWorld({ modules })`, references its ErrorCode enum and drives an outage through the world (R112) |
|
|
67
68
|
| `HFS_PEER_INTEGRATION_MISSING` | error | the app root `package.json` depends on a driver integration (a pair of `knowledge/hfs/peer-integrations.yaml`) without its runtime peer, e.g. `@nestjs/apollo` on `@nestjs/platform-express` 11 without `@as-integrations/express5` (R111) |
|
|
68
69
|
| `FE_WIRE_GENERATED` | error | a contract copy with no `codegen` script wired before `build` and `typecheck`, or generated types older than the copy (R52) |
|
|
69
70
|
| `FE_I18N_PLACEMENT` | error | no `next-intl`, no `src/proxy.ts`, a `middleware.ts`, a route file outside `[locale]`, no `vi.json` catalog (R59) |
|
|
70
71
|
| `FE_I18N_CATALOG` | error | a locale catalog lacking a key another locale has (R60) |
|
|
71
72
|
| `FE_I18N_KEYS` | error | a literal key read through `next-intl` that a locale lacks, or a catalog key no source reads (R106; the architecture machine) |
|
|
72
|
-
| `FE_NO_TESTS` | error | a front end holds a `*.spec.*`, `*.test.*` or `*-spec.*` file, an `e2e/`, `__tests__/`, `__mocks__/` or `test-support/` directory, a vitest, Playwright, jest or Cypress file, a test script, or a test dependency in a `package.json`; no exception (R97; `scripts/
|
|
73
|
+
| `FE_NO_TESTS` | error | a front end holds a `*.spec.*`, `*.test.*` or `*-spec.*` file, an `e2e/`, `__tests__/`, `__mocks__/` or `test-support/` directory, a vitest, Playwright, jest or Cypress file, a test script, or a test dependency in a `package.json`; no exception (R97; `scripts/hfs/rules/fe-no-tests.mjs`) |
|
|
73
74
|
| `HFS_GITIGNORE_BLOCK_DRIFT` | error | the managed `.gitignore` block differs from its render (R04; `sync/managed.mjs`) |
|
|
74
75
|
| `HFS_SONAR_CONFIG` | error | `sonar-project.properties` differs from its render: no `sonar.host.url`, the `sonar.exclusions` of the installed jest preset, the be lcov import with the services as the only coverage scope (R11; `sync/managed.mjs`) |
|
|
75
76
|
| `HFS_FORMAT` | error | a tracked file the repository's own prettier would change (R19; `sync/format.mjs`, not under `--fast`) |
|
|
@@ -81,7 +82,7 @@ Each finding is reported once: the eslint and stylelint one-liners under R17, `t
|
|
|
81
82
|
The root and both sides are rendered by the one mechanism. The root: `package.json` `scripts` (`dev:be`, `dev:fe`, `build:be`, `build:fe`, `start:<app>`, `lint`, `lint:fix`, `test`, `test:integration`, `test:e2e`, `test:contract`, `test:stack`, `codegen`, `contract:emit`, `migrate`, `typecheck`, ...; a be script runs from `be/`, where its tsconfig and jest configuration are), `.prettierrc`, `.prettierignore`, the `.husky` hooks, the CI workflows, the Sonar file and the `.gitignore` block. A side: its `tsconfig.json` (resolving `@starci/tsconfig` from the root `node_modules`), its `eslint.config.mjs` one-liner, and for be `tsconfig.build.json`, `src/tests/tsconfig.json` and `jest.config.js`, for fe `stylelint.config.mjs`. `lint` is the one lint gate, `hfs lint`: ESLint over each side with its canon, the app check and stylelint over fe; `lint:fix` is the same with `--fix`. The front end has no test script, no test configuration and no e2e or coverage file: it has no tests, and `FE_NO_TESTS` (R97) refuses any spec, e2e file, test tool or test script under `fe/`. `turbo.json` stays the app's own
|
|
82
83
|
(`fe.tool-config-repo`): it carries the task graph, which no preset can render.
|
|
83
84
|
|
|
84
|
-
The architecture machine (`scripts/
|
|
85
|
+
The architecture machine (`scripts/hfs/architecture.mjs` of the runtime, the same code bundled here): tiers and import
|
|
85
86
|
direction, owner public API, cycles, module registration and composition, clones, dead exports, required files,
|
|
86
87
|
the source-shape, contract-form and front-end rules, and the repository-tree rules. Each violation and each error is one
|
|
87
88
|
finding under the machine's own rule id (`BE_TIER_DIRECTION`, `HFS_UNUSED_EXPORT`, `ARCH_OWNER_EXPORT_BYPASS`, ...). A
|
|
@@ -120,18 +121,18 @@ every hotspot reviewed, duplicated lines density, cognitive complexity through t
|
|
|
120
121
|
open issue, imported or native; that is stricter than the three imports alone and is intended. `hfs check` reports `HFS_SONAR_CONFIG` (R11) when the
|
|
121
122
|
properties file is not its render or the stack declaration names another gate. R20 and R21 have their Sonar enforcers as conditions of that file.
|
|
122
123
|
|
|
123
|
-
Coverage is the services' alone, from one scope. The managed `test` script is `jest --selectProjects unit --coverage`: it fails below the per-file 100 threshold on `src/**/*.service.ts` and writes `be/coverage/lcov.info` (the jest preset's lcov reporter). The managed `sonar-project.properties` imports that report (`sonar.javascript.lcov.reportPaths=be/coverage/lcov.info`) with `sonar.coverage.inclusions
|
|
124
|
+
Coverage is the services' alone, from one scope. The managed `test` script is `jest --selectProjects unit --coverage`: it fails below the per-file 100 threshold on `src/**/*.service.ts` and writes `be/coverage/lcov.info` (the jest preset's lcov reporter). The managed `sonar-project.properties` imports that report (`sonar.javascript.lcov.reportPaths=be/coverage/lcov.info`) with `sonar.coverage.exclusions` set to the complement of the services (SonarQube has no coverage inclusions: `coverageExclusions` in `sync/index.mjs` lists every be source role of the slot manifest's suffix vocabulary other than `service`, the be file names a slot declares outside it such as `main.ts`, `index.ts` and the migrations, and `fe/**`) and no other coverage key, so a handler, resolver, controller, module, config file or test is not a coverage target and `fe/` is outside coverage. The managed `codecov.yml` holds the same paths at 100 on the project and the patch and ignores `fe/**`; the managed CI workflow uploads the lcov with `codecov/codecov-action` after the unit run. `hfs sync` renders the scope into both files from the installed jest preset's `COVERAGE_SOURCES` (`coverageScope` in `sync/index.mjs`), so they can never drift. The runtime judges the coverage per file: `sonar-local.mjs scan` holds every service a slice touched at 100, and `sonar-local.mjs dashboard` fails a project unless every service is at 100.
|
|
124
125
|
|
|
125
126
|
The upload needs the repository secret `CODECOV_TOKEN` (each product monorepo gets its own; the runtime repository's root `.github/workflows/examples.yml` uses the runtime repository's one for the example apps; the owner adds it once per repository: Codecov, the repository's settings, then GitHub Settings > Secrets and variables > Actions > New repository secret `CODECOV_TOKEN`). Without it the step is skipped like the Sonar steps without `SONAR_TOKEN`.
|
|
126
127
|
|
|
127
128
|
## Maintaining the bundle
|
|
128
129
|
|
|
129
|
-
`runtime/` is a byte copy of the slot loader files, the pins, and the import closure of `scripts/
|
|
130
|
-
`scripts/
|
|
130
|
+
`runtime/` is a byte copy of the slot loader files, the pins, and the import closure of `scripts/hfs/check.mjs` and
|
|
131
|
+
`scripts/hfs/architecture.mjs` (computed by `scripts/sync-runtime.mjs`, so a new import of the machine is bundled without
|
|
131
132
|
editing a list), plus the catalog slice of every code `hfs check` can emit: its own and the machine's (`ARCHITECTURE_RULE_IDS`,
|
|
132
133
|
derived from the machine's rule id lists). After changing any of those files, `knowledge/hfs/slots.yaml`,
|
|
133
|
-
`knowledge/hfs/canon-pins.yaml`, `knowledge/patterns/fe/folder.yaml` or the catalog entries of those codes, run `node
|
|
134
|
-
`tests/hfs-cli.spec.mjs` fails on a stale copy. Bump `version` here and in the pin when the behaviour changes.
|
|
134
|
+
`knowledge/hfs/canon-pins.yaml`, `knowledge/patterns/fe/folder.yaml` or the catalog entries of those codes, run `node scripts/hfs/sync-runtime.mjs`;
|
|
135
|
+
`tests/packages-hfs/hfs-cli.spec.mjs` fails on a stale copy. Bump `version` here and in the pin when the behaviour changes.
|
|
135
136
|
|
|
136
137
|
The examples gate `node scripts/checks/check-example-architecture.mjs` runs `hfs lint` of this CLI at the root of every `examples/*`
|
|
137
138
|
app with an `hfs.json` (`examples/todo-app`, `examples/ecommerce-app`) and fails on any finding or any tool that could not run;
|
package/bin/hfs.mjs
CHANGED
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
// 0 clean, 1 error findings, 2 a refusal or bad usage.
|
|
39
39
|
import path from 'node:path';
|
|
40
40
|
import { fileURLToPath } from 'node:url';
|
|
41
|
-
import { isMain } from '../runtime/scripts/
|
|
42
|
-
import { checkRepository, explainPath, trackedFiles } from '../runtime/scripts/
|
|
43
|
-
import { HfsSlotsError, loadSlotManifest, readRepoDeclaration } from '../runtime/scripts/
|
|
41
|
+
import { isMain } from '../runtime/scripts/lib/is-main.mjs';
|
|
42
|
+
import { checkRepository, explainPath, trackedFiles } from '../runtime/scripts/hfs/check.mjs';
|
|
43
|
+
import { HfsSlotsError, loadSlotManifest, readRepoDeclaration } from '../runtime/scripts/hfs/slots.mjs';
|
|
44
44
|
import { formatFindings } from '../sync/format.mjs';
|
|
45
45
|
import { main as syncMain } from '../sync/cli.mjs';
|
|
46
46
|
import { SyncError, loadPresets } from '../sync/index.mjs';
|
|
@@ -48,7 +48,7 @@ import { managedFindings } from '../sync/managed.mjs';
|
|
|
48
48
|
import { emitContracts } from '../emit/contracts.mjs';
|
|
49
49
|
import { ScaffoldError, newService, newSpec } from '../scaffold/service.mjs';
|
|
50
50
|
import { scaffoldApp } from '../scaffold/app.mjs';
|
|
51
|
-
import { contractEmitFindings } from '../runtime/scripts/
|
|
51
|
+
import { contractEmitFindings } from '../runtime/scripts/hfs/rules/contract.mjs';
|
|
52
52
|
import { lintRepository, parseLintArgs, printLintText } from '../lint/run.mjs';
|
|
53
53
|
import { writeReport } from '../report/sonar.mjs';
|
|
54
54
|
|
package/lint/run.mjs
CHANGED
|
@@ -17,7 +17,7 @@ import path from 'node:path';
|
|
|
17
17
|
import { createRequire } from 'node:module';
|
|
18
18
|
import { spawnSync } from 'node:child_process';
|
|
19
19
|
import { linterReport, mergeReports, sonarReport, sourceRootsOf } from '../report/sonar.mjs';
|
|
20
|
-
import { SIDES, appRelativeMessages, loadSlotManifest, readRepoDeclaration } from '../runtime/scripts/
|
|
20
|
+
import { SIDES, appRelativeMessages, loadSlotManifest, readRepoDeclaration } from '../runtime/scripts/hfs/slots.mjs';
|
|
21
21
|
import { STYLE_GLOB } from '../sync/index.mjs';
|
|
22
22
|
|
|
23
23
|
export const LINT_SCHEMA = 'starci/lint@1';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@starci/hfs",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.8",
|
|
4
4
|
"description": "The HFS command line of a StarCi app (one repository: the root, be/ and fe/): hfs lint, check, scaffold app, explain, sync and work-hygiene. Self-contained: it carries the runtime files it reads.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -33,6 +33,6 @@
|
|
|
33
33
|
"scripts": {
|
|
34
34
|
"sync": "node scripts/sync-runtime.mjs",
|
|
35
35
|
"sync:check": "node scripts/sync-runtime.mjs --check",
|
|
36
|
-
"test": "node --test \"bin/*.
|
|
36
|
+
"test": "node --test \"bin/*.spec.mjs\""
|
|
37
37
|
}
|
|
38
38
|
}
|
|
@@ -16,6 +16,11 @@ const moduleRoot = path.dirname(fileURLToPath(new URL('../package.json', import.
|
|
|
16
16
|
|
|
17
17
|
/** The runtime root: this source tree (or an immutable sealed payload of it). */
|
|
18
18
|
export const skillRoot = moduleRoot;
|
|
19
|
+
/**
|
|
20
|
+
* The StarCi Source root (the host containing .claude and .workspaces): STARCI_SOURCE_ROOT overrides it, else the directory
|
|
21
|
+
* holding this runtime checkout. Parameterized over `env`, so a spec can inject a fixture Source.
|
|
22
|
+
*/
|
|
23
|
+
export const starciSourceRoot = (env = process.env) => (env.STARCI_SOURCE_ROOT ? path.resolve(env.STARCI_SOURCE_ROOT) : path.dirname(skillRoot));
|
|
19
24
|
|
|
20
25
|
/**
|
|
21
26
|
* Read a runtime contract document. `parts` are path segments under `skillRoot`
|
package/runtime/engine/yaml.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// engine/yaml.mjs — GENERATED, frozen. This is the yaml@2.9.0 package bundled by esbuild, with the
|
|
2
2
|
// runtime's parseYaml/stringifyYaml wrapper as the entry module (source shown below). The installed
|
|
3
|
-
// runtime carries zero npm dependencies (tests/npm-package.spec.mjs), so it vendors this parser
|
|
3
|
+
// runtime carries zero npm dependencies (tests/repo/npm-package.spec.mjs), so it vendors this parser
|
|
4
4
|
// rather than importing node_modules; yaml is a devDependency and a direct import breaks that
|
|
5
|
-
// install. tests/yaml-vendored-parity.spec.mjs proves the vendored parseYaml/stringifyYaml behave
|
|
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
7
|
// deep-equal, stringify output byte-identical). License: engine/yaml-license.json.
|
|
8
8
|
// Rebuild only when deliberately upgrading yaml. The entry source is:
|
|
@@ -133,7 +133,7 @@ ${t.comment}`:t.comment}this.doc.range[2]=t.offset;break}default:this.errors.pus
|
|
|
133
133
|
`),i=e.substring(0,t),n=e.substring(t+1)+`
|
|
134
134
|
`;if(s.type==="block-scalar"){let r=s.props[0];if(r.type!=="block-scalar-header")throw new Error("Invalid block scalar header");r.source=i,s.source=n}else{let{offset:r}=s,a="indent"in s?s.indent:-1,l=[{type:"block-scalar-header",offset:r,indent:a,source:i}];Xr(l,"end"in s?s.end:void 0)||l.push({type:"newline",offset:-1,indent:a,source:`
|
|
135
135
|
`});for(let c of Object.keys(s))c!=="type"&&c!=="offset"&&delete s[c];Object.assign(s,{type:"block-scalar",indent:a,props:l,source:n})}}o(Af,"setBlockScalarValue");function Xr(s,e){if(e)for(let t of e)switch(t.type){case"space":case"comment":s.push(t);break;case"newline":return s.push(t),!0}return!1}o(Xr,"addEndtoBlockProps");function pi(s,e,t){switch(s.type){case"scalar":case"double-quoted-scalar":case"single-quoted-scalar":s.type=t,s.source=e;break;case"block-scalar":{let i=s.props.slice(1),n=e.length;s.props[0].type==="block-scalar-header"&&(n-=s.props[0].source.length);for(let r of i)r.offset+=n;delete s.props,Object.assign(s,{type:t,source:e,end:i});break}case"block-map":case"block-seq":{let n={type:"newline",offset:s.offset+e.length,indent:s.indent,source:`
|
|
136
|
-
`};delete s.items,Object.assign(s,{type:t,source:e,end:[n]});break}default:{let i="indent"in s?s.indent:-1,n="end"in s&&Array.isArray(s.end)?s.end.filter(r=>r.type==="space"||r.type==="comment"||r.type==="newline"):[];for(let r of Object.keys(s))r!=="type"&&r!=="offset"&&delete s[r];Object.assign(s,{type:t,indent:i,source:e,end:n})}}}o(pi,"setFlowScalarValue");jt.createScalarToken=kf;jt.resolveAsScalar=vf;jt.setScalarValue=Nf});var ea=y(Zr=>{"use strict";var qf=o(s=>"type"in s?Rt(s):Ft(s),"stringify");function Rt(s){switch(s.type){case"block-scalar":{let e="";for(let t of s.props)e+=Rt(t);return e+s.source}case"block-map":case"block-seq":{let e="";for(let t of s.items)e+=Ft(t);return e}case"flow-collection":{let e=s.start.source;for(let t of s.items)e+=Ft(t);for(let t of s.end)e+=t.source;return e}case"document":{let e=Ft(s);if(s.end)for(let t of s.end)e+=t.source;return e}default:{let e=s.source;if("end"in s&&s.end)for(let t of s.end)e+=t.source;return e}}}o(Rt,"stringifyToken");function Ft({start:s,key:e,sep:t,value:i}){let n="";for(let r of s)n+=r.source;if(e&&(n+=Rt(e)),t)for(let r of t)n+=r.source;return i&&(n+=Rt(i)),n}o(Ft,"stringifyItem");Zr.stringify=qf});var na=y(ia=>{"use strict";var mi=Symbol("break visit"),Of=Symbol("skip children"),ta=Symbol("remove item");function re(s,e){"type"in s&&s.type==="document"&&(s={start:s.start,value:s.value}),sa(Object.freeze([]),s,e)}o(re,"visit");re.BREAK=mi;re.SKIP=Of;re.REMOVE=ta;re.itemAtPath=(s,e)=>{let t=s;for(let[i,n]of e){let r=t?.[i];if(r&&"items"in r)t=r.items[n];else return}return t};re.parentCollection=(s,e)=>{let t=re.itemAtPath(s,e.slice(0,-1)),i=e[e.length-1][0],n=t?.[i];if(n&&"items"in n)return n;throw new Error("Parent collection not found")};function sa(s,e,t){let i=t(e,s);if(typeof i=="symbol")return i;for(let n of["key","value"]){let r=e[n];if(r&&"items"in r){for(let a=0;a<r.items.length;++a){let l=sa(Object.freeze(s.concat([[n,a]])),r.items[a],t);if(typeof l=="number")a=l-1;else{if(l===mi)return mi;l===ta&&(r.items.splice(a,1),a-=1)}}typeof i=="function"&&n==="key"&&(i=i(e,s))}}return typeof i=="function"?i(e,s):i}o(sa,"_visit");ia.visit=re});var Yt=y($=>{"use strict";var gi=zr(),Lf=ea(),Ef=na(),yi="\uFEFF",bi="
|
|
136
|
+
`};delete s.items,Object.assign(s,{type:t,source:e,end:[n]});break}default:{let i="indent"in s?s.indent:-1,n="end"in s&&Array.isArray(s.end)?s.end.filter(r=>r.type==="space"||r.type==="comment"||r.type==="newline"):[];for(let r of Object.keys(s))r!=="type"&&r!=="offset"&&delete s[r];Object.assign(s,{type:t,indent:i,source:e,end:n})}}}o(pi,"setFlowScalarValue");jt.createScalarToken=kf;jt.resolveAsScalar=vf;jt.setScalarValue=Nf});var ea=y(Zr=>{"use strict";var qf=o(s=>"type"in s?Rt(s):Ft(s),"stringify");function Rt(s){switch(s.type){case"block-scalar":{let e="";for(let t of s.props)e+=Rt(t);return e+s.source}case"block-map":case"block-seq":{let e="";for(let t of s.items)e+=Ft(t);return e}case"flow-collection":{let e=s.start.source;for(let t of s.items)e+=Ft(t);for(let t of s.end)e+=t.source;return e}case"document":{let e=Ft(s);if(s.end)for(let t of s.end)e+=t.source;return e}default:{let e=s.source;if("end"in s&&s.end)for(let t of s.end)e+=t.source;return e}}}o(Rt,"stringifyToken");function Ft({start:s,key:e,sep:t,value:i}){let n="";for(let r of s)n+=r.source;if(e&&(n+=Rt(e)),t)for(let r of t)n+=r.source;return i&&(n+=Rt(i)),n}o(Ft,"stringifyItem");Zr.stringify=qf});var na=y(ia=>{"use strict";var mi=Symbol("break visit"),Of=Symbol("skip children"),ta=Symbol("remove item");function re(s,e){"type"in s&&s.type==="document"&&(s={start:s.start,value:s.value}),sa(Object.freeze([]),s,e)}o(re,"visit");re.BREAK=mi;re.SKIP=Of;re.REMOVE=ta;re.itemAtPath=(s,e)=>{let t=s;for(let[i,n]of e){let r=t?.[i];if(r&&"items"in r)t=r.items[n];else return}return t};re.parentCollection=(s,e)=>{let t=re.itemAtPath(s,e.slice(0,-1)),i=e[e.length-1][0],n=t?.[i];if(n&&"items"in n)return n;throw new Error("Parent collection not found")};function sa(s,e,t){let i=t(e,s);if(typeof i=="symbol")return i;for(let n of["key","value"]){let r=e[n];if(r&&"items"in r){for(let a=0;a<r.items.length;++a){let l=sa(Object.freeze(s.concat([[n,a]])),r.items[a],t);if(typeof l=="number")a=l-1;else{if(l===mi)return mi;l===ta&&(r.items.splice(a,1),a-=1)}}typeof i=="function"&&n==="key"&&(i=i(e,s))}}return typeof i=="function"?i(e,s):i}o(sa,"_visit");ia.visit=re});var Yt=y($=>{"use strict";var gi=zr(),Lf=ea(),Ef=na(),yi="\uFEFF",bi="\u0002",Si="\u0018",wi="\u001f",Tf=o(s=>!!s&&"items"in s,"isCollection"),Cf=o(s=>!!s&&(s.type==="scalar"||s.type==="single-quoted-scalar"||s.type==="double-quoted-scalar"||s.type==="block-scalar"),"isScalar");function If(s){switch(s){case yi:return"<BOM>";case bi:return"<DOC>";case Si:return"<FLOW_END>";case wi:return"<SCALAR>";default:return JSON.stringify(s)}}o(If,"prettyToken");function Mf(s){switch(s){case yi:return"byte-order-mark";case bi:return"doc-mode";case Si:return"flow-error-end";case wi:return"scalar";case"---":return"doc-start";case"...":return"doc-end";case"":case`
|
|
137
137
|
`:case`\r
|
|
138
138
|
`:return"newline";case"-":return"seq-item-ind";case"?":return"explicit-key-ind";case":":return"map-value-ind";case"{":return"flow-map-start";case"}":return"flow-map-end";case"[":return"flow-seq-start";case"]":return"flow-seq-end";case",":return"comma"}switch(s[0]){case" ":case" ":return"space";case"#":return"comment";case"%":return"directive-line";case"*":return"alias";case"&":return"anchor";case"!":return"tag";case"'":return"single-quoted-scalar";case'"':return"double-quoted-scalar";case"|":case">":return"block-scalar-header"}return null}o(Mf,"tokenType");$.createScalarToken=gi.createScalarToken;$.resolveAsScalar=gi.resolveAsScalar;$.setScalarValue=gi.setScalarValue;$.stringify=Lf.stringify;$.visit=Ef.visit;$.BOM=yi;$.DOCUMENT=bi;$.FLOW_END=Si;$.SCALAR=wi;$.isCollection=Tf;$.isScalar=Cf;$.prettyToken=If;$.tokenType=Mf});var Ni=y(aa=>{"use strict";var Je=Yt();function K(s){switch(s){case void 0:case" ":case`
|
|
139
139
|
`:case"\r":case" ":return!0;default:return!1}}o(K,"isEmpty");var ra=new Set("0123456789ABCDEFabcdef"),Pf=new Set("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-#;/?:@&=+$_.!~*'()"),Ut=new Set(",[]{}"),_f=new Set(` ,[]{}
|
|
@@ -17,68 +17,68 @@ pins:
|
|
|
17
17
|
# --- @starci packages. Every @starci package is a PUBLISHED npm package (install: registry): a product repository
|
|
18
18
|
# installs the exact pinned version from the npm registry in the root and every workspace package.json, never `file:`.
|
|
19
19
|
'@starci/heroicons':
|
|
20
|
-
version: 0.3.
|
|
20
|
+
version: 0.3.2
|
|
21
21
|
group: starci
|
|
22
22
|
install: registry
|
|
23
23
|
side: fe
|
|
24
24
|
source: packages/heroicons/package.json
|
|
25
25
|
why: 'StarCi custom Heroicons-compatible cuts; eslint-canon-fe (icon.mjs) admits @starci/heroicons/24/outline and /16/solid as icon sources, so an app that uses them gets this exact version.'
|
|
26
26
|
'@starci/grammar':
|
|
27
|
-
version: 0.8.
|
|
27
|
+
version: 0.8.2
|
|
28
28
|
group: starci
|
|
29
29
|
install: registry
|
|
30
30
|
side: fe
|
|
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.
|
|
34
|
+
version: 3.0.8
|
|
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.
|
|
41
|
+
version: 8.0.8
|
|
42
42
|
group: starci
|
|
43
43
|
install: registry
|
|
44
44
|
side: fe
|
|
45
45
|
source: packages/eslint/fe/package.json
|
|
46
46
|
why: '8.0.3: its bundled canon-pins copy pins stylelint-canon 2.0.2 and hfs 4.0.3; no rule changed. 8.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. 8.0.1: its bundled canon-pins copy pins hfs 4.0.1. 8.0.0: `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; the project graph is built per side. 7.0.0: the front end has no tests (FE_NO_TESTS R97 in hfs); no-vietnamese-in-source (R91).'
|
|
47
47
|
'@starci/stylelint-canon':
|
|
48
|
-
version: 2.0.
|
|
48
|
+
version: 2.0.3
|
|
49
49
|
group: starci
|
|
50
50
|
install: registry
|
|
51
51
|
side: fe
|
|
52
52
|
source: packages/stylelint/package.json
|
|
53
53
|
why: '2.0.2: no-class-selector refuses a compound class selector (`div.card`), which the 2.0.1 pattern let through. 2.0.1 accepts `--font-sans` and `--font-mono` in the brand layer (the vocabulary follows the grammar 0.8.0). 2.0.0 adds status-contrast (HeroUI soft pairs measured per theme), fixes brand-layer-shape on the shared :root,.light,.dark block and the info soft pair, and derives appTokens with loadAppTokens(import.meta.url) for the managed one-line stylelint.config.mjs.'
|
|
54
54
|
'@starci/tsconfig':
|
|
55
|
-
version: 2.0.
|
|
55
|
+
version: 2.0.2
|
|
56
56
|
group: starci
|
|
57
57
|
install: registry
|
|
58
58
|
side: both
|
|
59
59
|
source: packages/tsconfig/package.json
|
|
60
60
|
'@starci/prettier-config':
|
|
61
|
-
version: 1.0.
|
|
61
|
+
version: 1.0.1
|
|
62
62
|
group: starci
|
|
63
63
|
install: registry
|
|
64
64
|
side: both
|
|
65
65
|
source: packages/prettier-config/package.json
|
|
66
66
|
'@starci/jest-preset':
|
|
67
|
-
version: 2.2.
|
|
67
|
+
version: 2.2.3
|
|
68
68
|
group: starci
|
|
69
69
|
install: registry
|
|
70
70
|
side: be
|
|
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.
|
|
74
|
+
version: 1.0.6
|
|
75
75
|
group: starci
|
|
76
76
|
install: registry
|
|
77
77
|
side: be
|
|
78
78
|
source: packages/test-world/package.json
|
|
79
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.
|
|
81
|
+
version: 4.0.8
|
|
82
82
|
group: starci
|
|
83
83
|
install: registry
|
|
84
84
|
side: both
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# the CI, the hooks and .starciwork (slots of profile `app`); `be/` and `fe/` are its two sides, each laid out as the old
|
|
5
5
|
# standalone repository root (slots of profile be or fe, paths relative to the side folder). Every tracked path of every
|
|
6
6
|
# repository must match exactly one slot. Checks, lint factories, the architecture machine, templates and the why
|
|
7
|
-
# catalog READ this file through scripts/
|
|
7
|
+
# catalog READ this file through scripts/hfs/slots.mjs; none of them hardcodes a path.
|
|
8
8
|
# Shape: modules/schemas/hfs-slots.schema.yaml. The repository side is hfs.json: modules/schemas/hfs-repo.schema.yaml.
|
|
9
9
|
schema: starci/hfs-slots@2
|
|
10
10
|
version: 2.0.0
|
|
@@ -97,7 +97,7 @@ tiers:
|
|
|
97
97
|
crossOwner: every import across owners targets the owner's public entry (index.ts or index.tsx); never export *.
|
|
98
98
|
crossApp: apps never import each other; shared code is a packages/<pkg> slot.
|
|
99
99
|
|
|
100
|
-
# Parameters the lint factories and checks read per profile (through ruleParams(profile) in scripts/
|
|
100
|
+
# Parameters the lint factories and checks read per profile (through ruleParams(profile) in scripts/hfs/slots.mjs).
|
|
101
101
|
ruleParams:
|
|
102
102
|
be:
|
|
103
103
|
# HFS_SIZE_GROWTH: a file above soft may not grow against its parent commit; a new file stays within soft.
|
|
@@ -1166,9 +1166,9 @@ slots:
|
|
|
1166
1166
|
|
|
1167
1167
|
# Checks that read this manifest (all ship from .claude; none keeps its own path list)
|
|
1168
1168
|
consumers:
|
|
1169
|
-
- scripts/
|
|
1170
|
-
- scripts/
|
|
1171
|
-
- scripts/
|
|
1169
|
+
- scripts/hfs/slots.mjs # the loader every consumer below goes through
|
|
1170
|
+
- scripts/hfs/check.mjs # hfs check: HFS_SLOT_* / tracked / external, the side view per side
|
|
1171
|
+
- scripts/hfs/architecture.mjs # tiers, direction matrix, cycles, reachability
|
|
1172
1172
|
- packages/eslint/be starciBeConfig({hfs}) # file globs for rules come from slots
|
|
1173
1173
|
- packages/eslint/fe starciFeConfig({hfs})
|
|
1174
1174
|
- packages/stylelint # @starci/stylelint-canon: colour and brand allowances from fe.modules.brand
|
|
@@ -40,11 +40,11 @@ rules:
|
|
|
40
40
|
fe.source-root-pinned, even though the machine list names it as a file Next loads. Any other file at the source
|
|
41
41
|
root, including a helper folder beside a pinned file, has no owner and is refused.
|
|
42
42
|
frameworkPinnedRootFiles:
|
|
43
|
-
# Machine list read by scripts/
|
|
43
|
+
# Machine list read by scripts/hfs/architecture/framework-pinned.mjs (FE_SOURCE_LAYOUT_INVALID accepts exactly
|
|
44
44
|
# these basenames directly in a Next source root; FE_TIER_DIRECTION keeps them thin). Exact names,
|
|
45
45
|
# case-sensitive, no globs. It lists what the framework loads from the source root. Which of them a repository may
|
|
46
46
|
# keep is decided by the slot fe.source-root-pinned and FE_NEXT_CONVENTIONS, not by this list.
|
|
47
|
-
# Machine list read by scripts/
|
|
47
|
+
# Machine list read by scripts/hfs/architecture/framework-pinned.mjs (FE_SOURCE_LAYOUT_INVALID
|
|
48
48
|
# accepts exactly these basenames directly in a Next source root;
|
|
49
49
|
# their imports follow the FE_TIER_DIRECTION matrix). Exact names, case-sensitive, no globs. Supervisor rulings for nivo
|
|
50
50
|
# wf-nivo-fe-debt-mug06w7h inc-2e42a24b74e4 and inc-846867b9a34e (proxy is the Next 16 name of
|
|
@@ -4,7 +4,7 @@ title: The Sonar quality gate every product is held to
|
|
|
4
4
|
purpose: |
|
|
5
5
|
The one place the Sonar thresholds live. A product repository never restates them: its
|
|
6
6
|
.starcistacks/application-stacks.yaml `services.sonar.qualityGate` names the gate below, and
|
|
7
|
-
scripts/
|
|
7
|
+
scripts/gates/sonar-local.mjs (1) makes the local SonarQube's gate of that name carry exactly these
|
|
8
8
|
conditions and selects it for the project, and (2) judges an op's slice - the lines it changed - against
|
|
9
9
|
the same numbers. Code-writing ops cannot settle done while the slice is red
|
|
10
10
|
(scripts/kernel/sonar-settle.mjs). Changing a number here changes every repository on the next scan.
|
|
@@ -20,7 +20,7 @@ gate:
|
|
|
20
20
|
newCode:
|
|
21
21
|
# Coverage of the services only. The be unit run writes coverage/lcov.info (@starci/jest-preset coverageReporters lcov) and the
|
|
22
22
|
# managed sonar-project.properties imports it (sonar.javascript.lcov.reportPaths=be/coverage/lcov.info) with
|
|
23
|
-
# sonar.coverage.
|
|
23
|
+
# sonar.coverage.exclusions = the complement of be/src/**/*.service.ts (SonarQube has no coverage inclusions), rendered by hfs sync from the preset's COVERAGE_SOURCES and the slot manifest (codecov.yml
|
|
24
24
|
# is rendered from the same constant). A handler, resolver, controller, module, config file or test is not a coverage target and
|
|
25
25
|
# fe/ is outside coverage entirely, so coverage on these conditions is the services' coverage alone.
|
|
26
26
|
coverage:
|
|
@@ -86,7 +86,7 @@ ARCH_KNOWLEDGE_UNAVAILABLE:
|
|
|
86
86
|
meaning_vi: "Bộ kiểm kiến trúc cần một tệp tri thức đi kèm (danh sách tệp gốc do framework ghim) nhưng không đọc được hoặc sai khuôn, nên không phán xét bằng hợp đồng khác."
|
|
87
87
|
causes_vi:
|
|
88
88
|
- "Bản đóng gói của `hfs` thiếu hoặc hỏng tệp tri thức, hoặc tệp tri thức trong runtime bị sửa sai khuôn."
|
|
89
|
-
nextStep_vi: "Cài lại `@starci/hfs` đúng phiên bản đã ghim; nếu là runtime thì sửa tệp tri thức rồi chạy `node
|
|
89
|
+
nextStep_vi: "Cài lại `@starci/hfs` đúng phiên bản đã ghim; nếu là runtime thì sửa tệp tri thức rồi chạy `node scripts/hfs/sync-runtime.mjs`."
|
|
90
90
|
owner: runtime-core
|
|
91
91
|
kind: check-finding
|
|
92
92
|
|
|
@@ -667,6 +667,16 @@ FE_ERROR_BOUNDARY_MISSING:
|
|
|
667
667
|
owner: op-retry
|
|
668
668
|
kind: check-finding
|
|
669
669
|
|
|
670
|
+
FE_GRAPHQL_CONTRACT:
|
|
671
|
+
title: "A front-end GraphQL document is one its back end serves"
|
|
672
|
+
title_vi: "Tài liệu GraphQL của front end không khớp hợp đồng của back end"
|
|
673
|
+
meaning_vi: "Một thao tác trong tệp `.graphql` dưới `fe/` không khớp ảnh chụp hợp đồng `be/contracts/<service>/schema.graphql` của dịch vụ phục vụ trường gốc đầu tiên của nó: trường, đối số hoặc trường input không được khai báo, thiếu đối số bắt buộc, biến khác kiểu, chọn trường trên giá trị lá hoặc thiếu lựa chọn trên đối tượng, biến khai báo mà không dùng, hoặc không hợp đồng nào phục vụ trường gốc đó."
|
|
674
|
+
causes_vi:
|
|
675
|
+
- "Vi phạm luật R113: front end gửi một tài liệu viết theo hợp đồng cũ (ví dụ `(request: ...)` trong khi back end nhận `input`), nên lời gọi hỏng khi chạy mà type-check của front end không thấy."
|
|
676
|
+
nextStep_vi: "Op vừa tạo ra bản này sẽ được chạy lại để sửa theo lời nhắn của bộ kiểm tra: sửa tài liệu `.graphql` cho khớp hợp đồng (hoặc chạy `npm run contract:emit` nếu ảnh chụp đã cũ); không thêm ngoại lệ."
|
|
677
|
+
owner: op-retry
|
|
678
|
+
kind: check-finding
|
|
679
|
+
|
|
670
680
|
FE_HOOKS_ARE_HOOKS:
|
|
671
681
|
title: "`hooks/` hold hooks and one shared file per domain"
|
|
672
682
|
title_vi: "hooks/ chứa thứ không phải hook"
|
|
@@ -1496,7 +1506,7 @@ HFS_SONAR_CONFIG:
|
|
|
1496
1506
|
title_vi: "Cấu hình Sonar lệch"
|
|
1497
1507
|
meaning_vi: "Cấu hình Sonar lệch: `<detail>`. Sonar phải đọc đúng cấu hình sinh ra và nhập báo cáo độ phủ lcov của be, chỉ tính các service."
|
|
1498
1508
|
causes_vi:
|
|
1499
|
-
- "Vi phạm luật R11: `sonar-project.properties` do template sinh: không `sonar.host.url`, `sources`/`tests` không chồng, `sonar.exclusions` = đúng danh sách loại trừ của jest, nhập lcov của be (`sonar.javascript.lcov.reportPaths=be/coverage/lcov.info`) với `sonar.coverage.
|
|
1509
|
+
- "Vi phạm luật R11: `sonar-project.properties` do template sinh: không `sonar.host.url`, `sources`/`tests` không chồng, `sonar.exclusions` = đúng danh sách loại trừ của jest, nhập lcov của be (`sonar.javascript.lcov.reportPaths=be/coverage/lcov.info`) với `sonar.coverage.exclusions` loại mọi tệp không phải service (SonarQube không có coverage inclusions; hfs sync sinh phần bù từ COVERAGE_SOURCES và bộ hậu tố của slot manifest, gồm cả fe/**) và không khóa độ phủ nào khác, không nhắc kiểu spec đã bỏ, và nhận báo cáo ESLint cùng tệp nhập lỗi HFS (`sonar.eslint.reportPaths`, `sonar.externalIssuesReportPaths`) để mọi lỗi canon hiện trong Sonar."
|
|
1500
1510
|
- "Khối `services.sonar.qualityGate` của khai báo stack phải gọi đúng cổng chất lượng duy nhất của `knowledge/sonar-gate.yaml`; repo không tự ghi ngưỡng."
|
|
1501
1511
|
nextStep_vi: "Op vừa tạo ra bản này sẽ được chạy lại để sửa theo lời nhắn của bộ kiểm tra (nợ hàng loạt có codemod của HFS); không cần ai can thiệp thêm."
|
|
1502
1512
|
owner: op-retry
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// scripts/api/fs/lib.mjs — what the fs call files beside it share: the error codes Windows returns while another
|
|
2
|
+
// process holds a path open. The call files (safe-remove.mjs, rename-over.mjs, rmdir-link.mjs, zip-write.mjs) each
|
|
3
|
+
// name one filesystem use.
|
|
4
|
+
|
|
5
|
+
/** The rename and unlink errors Windows returns while another process holds the target open: retried, never final. */
|
|
6
|
+
export const FS_BUSY = Object.freeze(['EPERM', 'EBUSY', 'EACCES']);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// rmdir-link.mjs — the link-safe removal primitive: `cmd /d /c rmdir <link>` (no /s) removes a Windows junction or
|
|
2
|
-
// directory symlink as a link and never touches its target. scripts/
|
|
2
|
+
// directory symlink as a link and never touches its target. scripts/api/fs/safe-remove.mjs removeLink is its one caller and
|
|
3
3
|
// checks afterwards that the link is gone (unlinkOnly); this file only issues the call.
|
|
4
4
|
|
|
5
5
|
import { spawnSync } from 'node:child_process';
|
|
@@ -13,18 +13,19 @@
|
|
|
13
13
|
// its own path under its parent's real path (any name-surrogate reparse point). A link that cannot be
|
|
14
14
|
// unlinked stops the removal of everything above it; nothing is ever deleted through it.
|
|
15
15
|
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
16
|
+
// A worktree's removal composes this with git (scripts/machine/worktree-git.mjs safeRemoveWorktree, the Orca home in
|
|
17
|
+
// scripts/machine/worktree-orca.mjs): every link removed as a link first (removeLinksUnder), zero asserted, only then git
|
|
18
|
+
// or Orca. Whether a tree holds an indexed job artifact is the caller's decision (`hold`: scripts/machine/artifact-hold.mjs
|
|
19
|
+
// artifactHoldReason), taken before anything is deleted.
|
|
18
20
|
import fs from 'node:fs';
|
|
19
21
|
import os from 'node:os';
|
|
20
22
|
import path from 'node:path';
|
|
21
23
|
import { fileURLToPath } from 'node:url';
|
|
22
|
-
import { sleepSync } from '
|
|
23
|
-
import { samePath } from '
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import { realpathOr } from './fs-kind.mjs';
|
|
24
|
+
import { sleepSync } from '../../lib/sleep-sync.mjs';
|
|
25
|
+
import { samePath } from '../../lib/path-key.mjs';
|
|
26
|
+
import { rmdirLink } from './rmdir-link.mjs';
|
|
27
|
+
import { FS_BUSY } from './lib.mjs';
|
|
28
|
+
import { realpathOr } from '../../lib/fs-kind.mjs';
|
|
28
29
|
|
|
29
30
|
const WIN = process.platform === 'win32';
|
|
30
31
|
const same = samePath;
|
|
@@ -73,7 +74,7 @@ const retrying = (fn, retries) => {
|
|
|
73
74
|
for (let attempt = 0; ; attempt += 1) {
|
|
74
75
|
try { fn(); return null; } catch (error) {
|
|
75
76
|
if (error?.code === 'ENOENT') return null;
|
|
76
|
-
if (attempt >= retries || ![
|
|
77
|
+
if (attempt >= retries || ![...FS_BUSY, 'ENOTEMPTY'].includes(error?.code)) return error;
|
|
77
78
|
sleepSync(25 * (attempt + 1));
|
|
78
79
|
}
|
|
79
80
|
}
|
|
@@ -84,7 +85,7 @@ const removeFile = (p, retries) => retrying(() => {
|
|
|
84
85
|
}
|
|
85
86
|
}, retries);
|
|
86
87
|
|
|
87
|
-
const SKILL_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
88
|
+
const SKILL_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
|
|
88
89
|
/**
|
|
89
90
|
* True when `p` sits strictly below `root` by real path: no link on the way, not `root` itself. The one place a
|
|
90
91
|
* primary checkout may be removed is a disposable fixture a caller names by its temp root (hk-tmp).
|
|
@@ -105,17 +106,20 @@ export function strictlyInsideReal(p, root) {
|
|
|
105
106
|
* scratch trees the runtime removes are temp directories and linked worktrees, whose .git is a file).
|
|
106
107
|
* `checkoutsUnder` names a disposable root (hk-tmp's temp root): a checkout strictly inside it by real path
|
|
107
108
|
* is a spec fixture, not a live repository, and is not refused for its .git. Every other refusal stands.
|
|
108
|
-
*
|
|
109
|
-
* workflow's phase
|
|
109
|
+
* `hold(path)` is the caller's artifact-hold check (scripts/machine/artifact-hold.mjs artifactHoldReason: a tree holding
|
|
110
|
+
* an indexed job artifact, or inside an evidence directory holding one, whatever the workflow's phase), its refusal line
|
|
111
|
+
* or null. It is required: without one every path is refused (fail closed); a caller that removes only a scratch tree
|
|
112
|
+
* it made itself, outside every ledger's repository, says so with `hold: () => null`.
|
|
110
113
|
*/
|
|
111
|
-
export function forbiddenRoot(p, { checkoutsUnder = null } = {}) {
|
|
114
|
+
export function forbiddenRoot(p, { checkoutsUnder = null, hold } = {}) {
|
|
112
115
|
const resolved = path.resolve(p);
|
|
113
116
|
if (path.parse(resolved).root === resolved || same(path.dirname(resolved), resolved)) return 'a filesystem root';
|
|
114
117
|
for (const [name, dir] of [['the home directory', os.homedir()], ['the temp directory', os.tmpdir()], ['the runtime', SKILL_ROOT],
|
|
115
118
|
['the repository hosting the runtime', path.dirname(SKILL_ROOT)], ['the repositories root', path.dirname(path.dirname(SKILL_ROOT))]]) {
|
|
116
119
|
if (dir && same(path.resolve(dir), resolved)) return name;
|
|
117
120
|
}
|
|
118
|
-
|
|
121
|
+
if (typeof hold !== 'function') return 'a tree no artifact-hold check cleared (pass hold)';
|
|
122
|
+
const held = hold(resolved);
|
|
119
123
|
if (held) return held;
|
|
120
124
|
let checkout = false;
|
|
121
125
|
try { checkout = fs.lstatSync(path.join(resolved, '.git')).isDirectory(); } catch { /* no .git directory */ }
|
|
@@ -125,14 +129,14 @@ export function forbiddenRoot(p, { checkoutsUnder = null } = {}) {
|
|
|
125
129
|
|
|
126
130
|
/**
|
|
127
131
|
* Remove `root` and everything under it without ever following a link. Links are unlinked (the link only);
|
|
128
|
-
* plain files and directories are deleted bottom-up. `checkoutsUnder`
|
|
132
|
+
* plain files and directories are deleted bottom-up. `checkoutsUnder` and `hold` (required) are forbiddenRoot's. Returns {ok, root, removed: {files, dirs, links},
|
|
129
133
|
* errors: [{path, code, message}]}; ok is true only when `root` is gone. A missing root is ok.
|
|
130
134
|
*/
|
|
131
|
-
export function safeRemoveTree(root, { retries = 5, checkoutsUnder = null } = {}) {
|
|
135
|
+
export function safeRemoveTree(root, { retries = 5, checkoutsUnder = null, hold } = {}) {
|
|
132
136
|
const target = path.resolve(String(root ?? ''));
|
|
133
137
|
const out = { ok: false, root: target, removed: { files: 0, dirs: 0, links: 0 }, errors: [] };
|
|
134
138
|
const fail = (p, error) => { out.errors.push({ path: p, code: error?.code ?? 'ERROR', message: String(error?.message ?? error) }); };
|
|
135
|
-
const refused = root ? forbiddenRoot(target, { checkoutsUnder }) : 'no path';
|
|
139
|
+
const refused = root ? forbiddenRoot(target, { checkoutsUnder, hold }) : 'no path';
|
|
136
140
|
if (refused) { fail(target, { code: 'REFUSED', message: `refusing to remove ${refused}` }); return out; }
|
|
137
141
|
let st;
|
|
138
142
|
try { st = fs.lstatSync(target); } catch (error) {
|
|
@@ -191,29 +195,8 @@ export function removeLink(p) {
|
|
|
191
195
|
return unlinkOnly(p);
|
|
192
196
|
}
|
|
193
197
|
|
|
194
|
-
/** The main checkout's state a removal must never change: its tracked deletions and its node_modules entry counts. */
|
|
195
|
-
export function mainCheckoutGuard(mainRoot, { git = null } = {}) {
|
|
196
|
-
const run = git ?? ((args, opts) => gitSpawn('git', args, { cwd: opts.cwd, maxBuffer: 64 * 1024 * 1024 }));
|
|
197
|
-
const count = (rel) => { try { return fs.readdirSync(path.join(mainRoot, rel)).length; } catch { return null; } };
|
|
198
|
-
// porcelain v2 ("1 <XY> <sub> <mH> <mI> <mW> <hH> <hI> <path>"): no leading blank a runner's trim could eat.
|
|
199
|
-
const st = run(['status', '--porcelain=v2', '--untracked-files=no'], { cwd: mainRoot });
|
|
200
|
-
const text = String(st?.stdout ?? st?.out ?? '');
|
|
201
|
-
const ok = st?.ok ?? (!st?.error && st?.status === 0);
|
|
202
|
-
const deleted = text.split(/\r?\n/).map((l) => l.trim().split(' ')).filter((f) => f[0] === '1' && f.length >= 9 && f[1].includes('D')).map((f) => f.slice(8).join(' '));
|
|
203
|
-
return { ok: Boolean(ok), deleted: new Set(deleted),
|
|
204
|
-
nodeModules: count('node_modules'), packagesNodeModules: count(path.join('packages', 'node_modules')) };
|
|
205
|
-
}
|
|
206
|
-
/** What changed in the main checkout between two guards: [] when nothing. */
|
|
207
|
-
export function mainCheckoutDamage(before, after) {
|
|
208
|
-
const out = [];
|
|
209
|
-
if (before.ok && after.ok) for (const f of after.deleted) if (!before.deleted.has(f)) out.push(`tracked file deleted: ${f}`);
|
|
210
|
-
if (before.nodeModules !== after.nodeModules) out.push(`node_modules entries ${before.nodeModules} -> ${after.nodeModules}`);
|
|
211
|
-
if (before.packagesNodeModules !== after.packagesNodeModules) out.push(`packages/node_modules entries ${before.packagesNodeModules} -> ${after.packagesNodeModules}`);
|
|
212
|
-
return out;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
198
|
/**
|
|
216
|
-
* The link step of every worktree removal (git
|
|
199
|
+
* The link step of every worktree removal (scripts/machine/worktree-git.mjs safeRemoveWorktree, worktree-orca.mjs removeOrcaWorktree): every link
|
|
217
200
|
* under `target` found WITHOUT following one (linksUnder), each removed as a link (removeLink: `cmd /c rmdir <link>`, never
|
|
218
201
|
* /s), outermost first, then a re-scan that must find ZERO. {ok, links, errors: [{path, code, message}]}; ok false: a link
|
|
219
202
|
* is stuck and the caller removes nothing.
|
|
@@ -228,48 +211,14 @@ export function removeLinksUnder(target) {
|
|
|
228
211
|
}
|
|
229
212
|
|
|
230
213
|
/**
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
* 2. remove each as a link (removeLink: `cmd /c rmdir <link>`, never /s), outermost first;
|
|
234
|
-
* 3. re-scan the same way and refuse (link-stuck, nothing deleted) unless ZERO links remain;
|
|
235
|
-
* 4. only then `git worktree remove --force` (a link-free tree: git cannot walk out of it); a directory git does not know
|
|
236
|
-
* goes through safeRemoveTree (never follows a link); `git worktree prune`;
|
|
237
|
-
* 5. assert the main checkout is untouched: no new tracked deletion, node_modules and packages/node_modules entry counts
|
|
238
|
-
* unchanged - a violation is {ok:false, fatal:true, reason:'main-checkout-damaged'}: the caller (the GC) stops.
|
|
239
|
-
* Never robocopy, rm -rf or rmdir /s. `git(args, {cwd})` is the caller's git runner; `repo` any checkout of the repository.
|
|
240
|
-
* {ok, root, links, removed, errors, damage?}
|
|
214
|
+
* Unlink `<dir>/node_modules` when it is a link (one an older runtime made; no runtime code makes one, RT_NODE_MODULES_LINK).
|
|
215
|
+
* true when no link is left there; a real directory is left alone (true: it is the checkout's own).
|
|
241
216
|
*/
|
|
242
|
-
export function
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
if (mainRoot && same(mainRoot, target)) { out.errors.push({ path: target, code: 'REFUSED', message: 'refusing to remove the main checkout' }); return out; }
|
|
250
|
-
const refused = forbiddenRoot(target);
|
|
251
|
-
if (refused) { out.errors.push({ path: target, code: 'REFUSED', message: `refusing to remove ${refused}` }); return out; }
|
|
252
|
-
const before = mainRoot ? mainCheckoutGuard(mainRoot, { git: run }) : null;
|
|
253
|
-
if (fs.existsSync(target)) {
|
|
254
|
-
const unlinked = removeLinksUnder(target);
|
|
255
|
-
out.links = unlinked.links;
|
|
256
|
-
if (!unlinked.ok) { out.errors.push(...unlinked.errors); out.reason = 'link-stuck'; return out; }
|
|
257
|
-
out.removed.links = out.links;
|
|
258
|
-
const registered = trees.some((t) => same(t, target));
|
|
259
|
-
if (registered && repo) run(['worktree', 'remove', '--force', target], { cwd: repo });
|
|
260
|
-
if (fs.existsSync(target)) {
|
|
261
|
-
if (linksUnder(target).length) { out.errors.push({ path: target, code: 'LINK_STUCK', message: 'a link appeared during removal' }); out.reason = 'link-stuck'; return out; }
|
|
262
|
-
const rm = safeRemoveTree(target, { retries });
|
|
263
|
-
out.removed.files += rm.removed.files; out.removed.dirs += rm.removed.dirs;
|
|
264
|
-
out.errors.push(...rm.errors);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
if (repo) { try { run(['worktree', 'prune'], { cwd: repo }); } catch { /* the registration is pruned on the next prune */ } }
|
|
268
|
-
if (before) {
|
|
269
|
-
const damage = mainCheckoutDamage(before, mainCheckoutGuard(mainRoot, { git: run }));
|
|
270
|
-
if (damage.length) { out.damage = damage; out.fatal = true; out.reason = 'main-checkout-damaged'; out.errors.push({ path: mainRoot, code: 'main-checkout-damaged', message: damage.join('; ') }); return out; }
|
|
271
|
-
}
|
|
272
|
-
out.ok = !fs.existsSync(target) && (() => { try { fs.lstatSync(target); return false; } catch { return true; } })();
|
|
273
|
-
if (!out.ok && !out.reason) out.reason = 'remove-failed';
|
|
274
|
-
return out;
|
|
217
|
+
export function unlinkNodeModulesLink(dir) {
|
|
218
|
+
const nm = path.join(dir, 'node_modules');
|
|
219
|
+
let st;
|
|
220
|
+
try { st = fs.lstatSync(nm); } catch { return true; }
|
|
221
|
+
if (!st.isSymbolicLink()) return true;
|
|
222
|
+
try { fs.unlinkSync(nm); } catch { try { fs.rmdirSync(nm); } catch { /* checked below */ } }
|
|
223
|
+
try { fs.lstatSync(nm); return false; } catch { return true; }
|
|
275
224
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// scripts/api/git/lib.mjs — the one place the runtime spawns git (scripts/
|
|
1
|
+
// scripts/api/git/lib.mjs — the one place the runtime spawns git (RT_EXTERNAL_OWNER of scripts/hfs/runtime-rules/external-owner.mjs enforces it).
|
|
2
2
|
//
|
|
3
3
|
// Every caller spelt the same options by hand - encoding:'utf8', windowsHide:true, sometimes a timeout - with two shapes:
|
|
4
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).
|