@starci/hfs 3.0.0 → 4.0.1

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.
Files changed (199) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +25 -21
  3. package/bin/hfs.mjs +59 -37
  4. package/lint/run.mjs +70 -39
  5. package/package.json +2 -2
  6. package/runtime/engine/admission.mjs +3 -3
  7. package/runtime/engine/ledger-db.mjs +2 -2
  8. package/runtime/engine/machine-db.mjs +90 -9
  9. package/runtime/engine/migrations/machine/0002-worktrees-no-workflow-kind.sql +13 -0
  10. package/runtime/engine/migrations/runtime/0005-ended-workflow-views.sql +93 -0
  11. package/runtime/knowledge/hfs/canon-pins.yaml +28 -9
  12. package/runtime/knowledge/hfs/peer-integrations.yaml +18 -0
  13. package/runtime/knowledge/hfs/slots.yaml +193 -128
  14. package/runtime/knowledge/patterns/fe/folder.yaml +36 -36
  15. package/runtime/modules/kernel/failure-codes.yaml +23 -32
  16. package/runtime/scripts/checks/architecture/backend.mjs +1 -1
  17. package/runtime/scripts/checks/architecture/config.mjs +31 -11
  18. package/runtime/scripts/checks/architecture/contracts.mjs +4 -4
  19. package/runtime/scripts/checks/architecture/fe-slot-allows.mjs +7 -3
  20. package/runtime/scripts/checks/architecture/framework-pinned.mjs +5 -47
  21. package/runtime/scripts/checks/architecture/frontend.mjs +6 -4
  22. package/runtime/scripts/checks/architecture/hfs.mjs +104 -66
  23. package/runtime/scripts/checks/architecture/next-data.mjs +3 -2
  24. package/runtime/scripts/checks/architecture/registration.mjs +1 -1
  25. package/runtime/scripts/checks/architecture/symbols.mjs +13 -2
  26. package/runtime/scripts/checks/architecture/test-world-files.mjs +83 -45
  27. package/runtime/scripts/checks/architecture/typescript.mjs +45 -20
  28. package/runtime/scripts/checks/typescript-programs.mjs +2 -2
  29. package/runtime/scripts/lib/hfs-check.mjs +156 -141
  30. package/runtime/scripts/lib/hfs-path-findings.mjs +13 -2
  31. package/runtime/scripts/lib/hfs-rules/contract.mjs +15 -42
  32. package/runtime/scripts/lib/hfs-rules/deps.mjs +4 -2
  33. package/runtime/scripts/lib/hfs-rules/frontend.mjs +37 -36
  34. package/runtime/scripts/lib/hfs-rules/peer-integrations.mjs +44 -0
  35. package/runtime/scripts/lib/hfs-rules/proof-commands.mjs +11 -8
  36. package/runtime/scripts/lib/hfs-slots.mjs +244 -61
  37. package/runtime/scripts/lib/hfs-view.mjs +9 -7
  38. package/runtime/scripts/lib/language.mjs +11 -1
  39. package/runtime/scripts/lib/safe-remove.mjs +95 -10
  40. package/scaffold/app.mjs +205 -0
  41. package/scaffold/service.mjs +26 -16
  42. package/sync/cli.mjs +1 -1
  43. package/sync/hygiene.mjs +11 -8
  44. package/sync/index.mjs +109 -111
  45. package/sync/managed.mjs +9 -8
  46. package/sync/sonar-key.mjs +20 -22
  47. package/templates/{be → app}/ci-workflows/github/workflows/ci.yml +4 -2
  48. package/templates/app/gitignore +6 -0
  49. package/templates/app/hooks/husky/pre-commit +25 -0
  50. package/templates/app/hooks/husky/pre-push +7 -0
  51. package/templates/app/package-scripts/package.json +22 -0
  52. package/templates/{be → app}/quality-config/sonar-project.properties +3 -2
  53. package/templates/app/skeleton/.editorconfig +15 -0
  54. package/templates/app/skeleton/.gitattributes +2 -0
  55. package/templates/app/skeleton/.nvmrc +1 -0
  56. package/templates/app/skeleton/.starciwork/features/index.yaml +7 -0
  57. package/templates/app/skeleton/.starciwork/workspace.yaml +9 -0
  58. package/templates/app/skeleton/README.md +36 -0
  59. package/templates/app/skeleton/scripts/codegen.mjs +4 -0
  60. package/templates/{fe → app}/tool-config/prettierignore +4 -1
  61. package/templates/be/skeleton/.sops.yaml +2 -0
  62. package/templates/be/skeleton/.starcistacks/application-stacks.yaml +10 -0
  63. package/templates/be/skeleton/apps/__app__/src/__app__.options.ts +3 -0
  64. package/templates/be/skeleton/apps/__app__/src/app.module.ts +27 -6
  65. package/templates/be/skeleton/apps/__app__/src/main.ts +4 -1
  66. package/templates/be/skeleton/src/features/system-health/transport/http/live.controller.ts +2 -0
  67. package/templates/be/skeleton/src/modules/domain/identity/admission.policy.ts +11 -0
  68. package/templates/be/skeleton/src/modules/domain/identity/auth.guard.ts +25 -0
  69. package/templates/be/skeleton/src/modules/domain/identity/errors/identity.error.ts +16 -0
  70. package/templates/be/skeleton/src/modules/domain/identity/identity.contracts.ts +11 -0
  71. package/templates/be/skeleton/src/modules/domain/identity/identity.decorators.ts +8 -0
  72. package/templates/be/skeleton/src/modules/domain/identity/identity.module-definition.ts +7 -0
  73. package/templates/be/skeleton/src/modules/domain/identity/identity.module.ts +14 -0
  74. package/templates/be/skeleton/src/modules/domain/identity/identity.options.ts +2 -0
  75. package/templates/be/skeleton/src/modules/domain/identity/index.ts +6 -0
  76. package/templates/be/skeleton/src/modules/domain/identity/messages/identity.messages.ts +11 -0
  77. package/templates/be/skeleton/src/modules/platform/composition/composition.decorators.ts +8 -1
  78. package/templates/be/skeleton/src/modules/platform/composition/index.ts +1 -1
  79. package/templates/be/skeleton/src/modules/platform/config/env-source.config.ts +71 -23
  80. package/templates/be/skeleton/src/modules/platform/config/errors/config.error.ts +14 -16
  81. package/templates/be/skeleton/src/modules/platform/config/index.ts +1 -1
  82. package/templates/be/skeleton/src/modules/platform/config/server.config.ts +2 -12
  83. package/templates/be/skeleton/src/modules/platform/errors/domain.error.ts +16 -7
  84. package/templates/be/skeleton/src/modules/platform/errors/errors/errors.error.ts +16 -0
  85. package/templates/be/skeleton/src/modules/platform/errors/errors.contracts.ts +33 -0
  86. package/templates/be/skeleton/src/modules/platform/errors/errors.decorators.ts +16 -0
  87. package/templates/be/skeleton/src/modules/platform/errors/errors.filter.ts +32 -0
  88. package/templates/be/skeleton/src/modules/platform/errors/errors.log-events.ts +2 -2
  89. package/templates/be/skeleton/src/modules/platform/errors/errors.module-definition.ts +9 -0
  90. package/templates/be/skeleton/src/modules/platform/errors/errors.module.ts +19 -0
  91. package/templates/be/skeleton/src/modules/platform/errors/errors.options.ts +7 -0
  92. package/templates/be/skeleton/src/modules/platform/errors/errors.service.spec.ts +94 -0
  93. package/templates/be/skeleton/src/modules/platform/errors/errors.service.ts +47 -0
  94. package/templates/be/skeleton/src/modules/platform/errors/http-status.policy.ts +13 -0
  95. package/templates/be/skeleton/src/modules/platform/errors/index.ts +4 -1
  96. package/templates/be/skeleton/src/modules/platform/errors/messages/errors.messages.ts +11 -0
  97. package/templates/be/skeleton/src/modules/platform/http-security/errors/http-security.error.ts +19 -0
  98. package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.ts +5 -0
  99. package/templates/be/skeleton/src/modules/platform/http-security/http-security.config.ts +16 -0
  100. package/templates/be/skeleton/src/modules/platform/http-security/http-security.decorators.ts +10 -0
  101. package/templates/be/skeleton/src/modules/platform/http-security/http-security.module-definition.ts +9 -0
  102. package/templates/be/skeleton/src/modules/platform/http-security/http-security.module.ts +13 -0
  103. package/templates/be/skeleton/src/modules/platform/http-security/http-security.options.ts +17 -0
  104. package/templates/be/skeleton/src/modules/platform/http-security/index.ts +7 -0
  105. package/templates/be/skeleton/src/modules/platform/http-security/messages/http-security.messages.ts +13 -0
  106. package/templates/be/skeleton/src/modules/platform/http-security/origin.guard.ts +31 -0
  107. package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.ts +68 -0
  108. package/templates/be/skeleton/src/modules/platform/i18n/bundle-message-catalog.service.spec.ts +66 -0
  109. package/templates/be/skeleton/src/modules/platform/i18n/bundle-message-catalog.service.ts +30 -0
  110. package/templates/be/skeleton/src/modules/platform/i18n/i18n.contracts.ts +18 -0
  111. package/templates/be/skeleton/src/modules/platform/i18n/i18n.decorators.ts +23 -0
  112. package/templates/be/skeleton/src/modules/platform/i18n/i18n.module-definition.ts +9 -0
  113. package/templates/be/skeleton/src/modules/platform/i18n/i18n.module.ts +24 -0
  114. package/templates/be/skeleton/src/modules/platform/i18n/i18n.options.ts +7 -0
  115. package/templates/be/skeleton/src/modules/platform/i18n/i18n.port.ts +13 -0
  116. package/templates/be/skeleton/src/modules/platform/i18n/index.ts +4 -0
  117. package/templates/be/skeleton/src/modules/platform/i18n/request-locale.service.spec.ts +45 -0
  118. package/templates/be/skeleton/src/modules/platform/i18n/request-locale.service.ts +19 -0
  119. package/templates/be/skeleton/src/modules/platform/logging/index.ts +1 -1
  120. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +85 -68
  121. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +14 -10
  122. package/templates/be/skeleton/src/modules/platform/logging/logging.decorators.ts +13 -0
  123. package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +4 -0
  124. package/templates/be/skeleton/src/modules/platform/logging/logging.port.ts +0 -1
  125. package/templates/be/skeleton/src/modules/platform/primitives/index.ts +2 -0
  126. package/templates/be/skeleton/src/modules/platform/primitives/outcome.contracts.ts +25 -0
  127. package/templates/be/skeleton/src/modules/platform/primitives/outcome.mapper.ts +24 -0
  128. package/templates/fe/skeleton/apps/__app__/postcss.config.mjs +7 -0
  129. package/templates/fe/skeleton/apps/__app__/src/app/[locale]/error.tsx +5 -17
  130. package/templates/fe/skeleton/apps/__app__/src/app/[locale]/layout.tsx +11 -22
  131. package/templates/fe/skeleton/apps/__app__/src/app/[locale]/loading.tsx +6 -0
  132. package/templates/fe/skeleton/apps/__app__/src/app/[locale]/not-found.tsx +3 -12
  133. package/templates/fe/skeleton/apps/__app__/src/app/[locale]/page.tsx +6 -24
  134. package/templates/fe/skeleton/apps/__app__/src/app/global-error.tsx +4 -18
  135. package/templates/fe/skeleton/apps/__app__/src/app/globals.css +5 -0
  136. package/templates/fe/skeleton/apps/__app__/src/components/composites/FailureScreen/index.tsx +28 -0
  137. package/templates/fe/skeleton/apps/__app__/src/features/layouts/LocaleShell/index.tsx +35 -0
  138. package/templates/fe/skeleton/apps/__app__/src/features/pages/ErrorPage/component.tsx +21 -0
  139. package/templates/fe/skeleton/apps/__app__/src/features/pages/ErrorPage/index.tsx +17 -0
  140. package/templates/fe/skeleton/apps/__app__/src/features/pages/GlobalErrorPage/component.tsx +26 -0
  141. package/templates/fe/skeleton/apps/__app__/src/features/pages/GlobalErrorPage/index.tsx +15 -0
  142. package/templates/fe/skeleton/apps/__app__/src/features/pages/HomePage/component.tsx +25 -0
  143. package/templates/fe/skeleton/apps/__app__/src/features/pages/HomePage/index.tsx +15 -0
  144. package/templates/fe/skeleton/apps/__app__/src/features/pages/LoadingPage/component.tsx +27 -0
  145. package/templates/fe/skeleton/apps/__app__/src/features/pages/LoadingPage/index.tsx +8 -0
  146. package/templates/fe/skeleton/apps/__app__/src/features/pages/NotFoundPage/component.tsx +32 -0
  147. package/templates/fe/skeleton/apps/__app__/src/features/pages/NotFoundPage/index.tsx +8 -0
  148. package/templates/fe/skeleton/apps/__app__/src/modules/config/index.ts +12 -0
  149. package/templates/fe/skeleton/apps/__app__/src/modules/i18n/index.ts +2 -0
  150. package/templates/fe/skeleton/apps/__app__/src/modules/i18n/messages/vi.json +6 -0
  151. package/templates/fe/{skeleton-app → skeleton}/apps/__app__/src/modules/i18n/request.ts +1 -0
  152. package/templates/fe/skeleton/apps/__app__/src/modules/routes/index.ts +4 -0
  153. package/templates/fe/{skeleton-app → skeleton}/apps/__app__/src/proxy.ts +1 -1
  154. package/sync/skeleton.mjs +0 -76
  155. package/templates/be/gitignore +0 -2
  156. package/templates/be/hooks/husky/pre-commit +0 -13
  157. package/templates/be/hooks/husky/pre-push +0 -6
  158. package/templates/be/package-scripts/package.json +0 -19
  159. package/templates/be/skeleton/scripts/.gitkeep +0 -0
  160. package/templates/be/skeleton/src/modules/platform/config/config.decorators.ts +0 -9
  161. package/templates/be/skeleton/src/modules/platform/errors/error.filter.ts +0 -20
  162. package/templates/be/tool-config/prettierignore +0 -8
  163. package/templates/fe/ci-workflows/github/workflows/ci.yml +0 -40
  164. package/templates/fe/gitignore +0 -3
  165. package/templates/fe/hooks/husky/pre-commit +0 -16
  166. package/templates/fe/hooks/husky/pre-push +0 -5
  167. package/templates/fe/package-scripts/package.json +0 -13
  168. package/templates/fe/parts/api-client.ts +0 -44
  169. package/templates/fe/parts/api-outcome.ts +0 -7
  170. package/templates/fe/quality-config/sonar-project.properties +0 -8
  171. package/templates/fe/skeleton/scripts/.gitkeep +0 -0
  172. package/templates/fe/skeleton-app/apps/__app__/src/modules/api/client.ts +0 -1
  173. package/templates/fe/skeleton-app/apps/__app__/src/modules/api/index.ts +0 -3
  174. package/templates/fe/skeleton-app/apps/__app__/src/modules/api/outcome.ts +0 -1
  175. package/templates/fe/skeleton-app/apps/__app__/src/modules/i18n/index.ts +0 -4
  176. package/templates/fe/skeleton-app/apps/__app__/src/modules/i18n/navigation.ts +0 -5
  177. package/templates/fe/skeleton-shared/apps/__app__/next.config.ts +0 -12
  178. package/templates/fe/skeleton-shared/apps/__app__/src/modules/api/index.ts +0 -2
  179. package/templates/fe/skeleton-shared/apps/__app__/src/modules/i18n/index.ts +0 -9
  180. package/templates/fe/skeleton-shared/apps/__app__/src/modules/i18n/request.ts +0 -5
  181. package/templates/fe/skeleton-shared/apps/__app__/src/proxy.ts +0 -5
  182. package/templates/fe/skeleton-shared/packages/__family__-api/package.json +0 -12
  183. package/templates/fe/skeleton-shared/packages/__family__-api/src/client.ts +0 -1
  184. package/templates/fe/skeleton-shared/packages/__family__-api/src/index.ts +0 -3
  185. package/templates/fe/skeleton-shared/packages/__family__-api/src/outcome.ts +0 -1
  186. package/templates/fe/skeleton-shared/packages/__family__-api/tsconfig.json +0 -5
  187. package/templates/fe/skeleton-shared/packages/__family__-i18n/package.json +0 -18
  188. package/templates/fe/skeleton-shared/packages/__family__-i18n/src/app.ts +0 -19
  189. package/templates/fe/skeleton-shared/packages/__family__-i18n/src/index.ts +0 -2
  190. package/templates/fe/skeleton-shared/packages/__family__-i18n/src/proxy.ts +0 -12
  191. package/templates/fe/skeleton-shared/packages/__family__-i18n/src/request.ts +0 -15
  192. package/templates/fe/skeleton-shared/packages/__family__-i18n/tsconfig.json +0 -5
  193. package/templates/fe/tool-config/prettierrc +0 -1
  194. /package/templates/{be → app}/ci-workflows/github/workflows/e2e.yml +0 -0
  195. /package/templates/{be → app}/starciwork.gitignore +0 -0
  196. /package/templates/{be → app}/tool-config/prettierrc +0 -0
  197. /package/templates/fe/{skeleton-app → skeleton}/apps/__app__/next.config.ts +0 -0
  198. /package/templates/fe/{skeleton-app → skeleton}/apps/__app__/src/modules/i18n/config.ts +0 -0
  199. /package/templates/fe/{skeleton-app → skeleton}/apps/__app__/src/modules/i18n/routing.ts +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.0.1 - 2026-10-01
4
+
5
+ - Fixed: `hfs scaffold app` no longer writes a hand-made lockfile. The 4.0.0 stub held only the root entry, so `npm ci` in a new app failed with EUSAGE ("package.json and package-lock.json are not in sync"). Once the files are written, the scaffold runs `npm install --package-lock-only --ignore-scripts --no-audit --no-fund` in the new app root (registry or npm cache; no node_modules, no scripts), so the lockfile resolves every dependency of the root and its workspaces and `npm ci` accepts it. If npm cannot resolve it, the scaffold exits 2 with `HFS_SCAFFOLD_LOCK_FAILED`, names the step and removes the app it began: no stub lock and no app without a lock is left. There is no switch to skip the step.
6
+ - Fixed (false positive): R14 `HFS_DEP_VERSION_SKEW` does not report a bundled copy in the lockfile (`inBundle`, a package's bundleDependencies, e.g. the tslib inside `@tailwindcss/oxide-wasm32-wasi`): it ships inside its parent's tarball and nothing in the app can move it. A fresh scaffold's real lock has one.
7
+
8
+ ## 4.0.0 - 2026-10-01
9
+
10
+ - Added: the scaffolded root `package.json` (and its lockfile root entry) pins `@starci/test-world` from `knowledge/hfs/canon-pins.yaml` (1.0.0), the package whose `starci-test-stack` bin the managed `test:stack` script runs.
11
+ - Changed: R47 `test-world-files` reads `test-world.config.ts` in the shape `@starci/test-world` defines and in its one named form `export const { useTestWorld, useSandbox } = defineTestWorld({ ... })` (the form R89 allows): `stack` names the environment, a stack service is faked only by its own `stacks` entry `{ fakedBy, reason }`, and a `fakes` entry of the declaration that fakes a stack service is refused like a `fakes/<provider>/` folder. A config with no readable named declaration (a default export included) is refused. The top-level `fakedBy` and the `stacks` list of environment names are no longer read.
12
+ - New (lane SHAPE): `HFS_PEER_INTEGRATION_MISSING` (R111). The app root package.json that depends on every package of a pair of `knowledge/hfs/peer-integrations.yaml` (at the named major) declares the pair's peer in its dependencies; the first pair is `@nestjs/apollo` on `@nestjs/platform-express` 11, which requires `@as-integrations/express5`.
13
+ - Changed (lane SHAPE): `HFS_PROOF_COMMAND_FILE_MISSING` (R105) judges the records naming a side (`repository: be` or `fe`) from the app root, where their proof commands run; a record naming another repository is still not judged.
14
+ - Changed (lane SHAPE): the managed `typecheck` script of an app with fe packages runs `npm run build --workspaces --if-present` before it type-checks the fe apps, which import the packages from `dist/`.
15
+ - Changed (lane SHAPE): `hfs scaffold app` writes `.starciwork/workspace.yaml` with the repositories `[{role: be, name: be}, {role: fe, name: fe}]`, the form of the examples.
16
+ - Fixed (lane SHAPE): slot be.tests.world allows `fakes/<provider>/server.ts`, so `BE_SOURCE_FORM` reads the fake server of a provider as its role instead of refusing the path `test-world-files` accepts.
17
+ - Breaking: the app monorepo standard. A product is ONE app repository `<app>/`: the root holds the one `package.json` (every dependency of both sides and every script; npm workspaces only for `fe/packages/*`), the one lockfile and `node_modules`, `hfs.json` of kind `app`, README, CI, `.husky`, `scripts/` and `.starciwork`; `be/` and `fe/` hold what the old back-end and front-end repository roots held except `package.json` and the lockfile. `hfs.json` is `{ "hfs": 2, "kind": "app", "project", "sides": { "be": { "apps", "optionalSlots", "connections" }, "fe": { "apps", "optionalSlots", "reads": ["be/contracts/"] } } }` (manifest major 2). The standalone `be` and `fe` repository kinds (`profile`, `stacks`) are deleted, with no alias.
18
+ - Breaking: `hfs init` is deleted; `hfs scaffold app <name> [--into <dir>]` writes the root, the `be/` and `fe/` skeletons and the app hfs.json, and refuses an existing directory. The fresh app lints clean (0 findings): the be skeleton is default-deny (throttler, CSRF origin guard, AuthGuard of `domain/identity`), carries the required platform capabilities (config, logging, errors, primitives, clock, i18n, cqrs, http-security) and the health feature; the fe skeleton mounts one pages feature per route slot, drawn with `@starci/grammar`.
19
+ - Breaking: `hfs lint`, `hfs check` and `hfs sync` run at the app root. The lint runs ESLint once per side (be/ with the BE canon, fe/ with the FE canon, each from its side folder with its one-line config), stylelint over fe/, the root checks once and the side checks and the architecture machine per side ("side view": each side judged with its folder as the root), and writes one `starci/lint@1` report and one Sonar import with app-relative paths. `--changed` keeps working with app-relative files. `hfs sync` renders the root files (package.json scripts `dev:be`, `dev:fe`, `build:be`, `build:fe`, `start:<app>`, `migrate`, `lint`, `test*`, `codegen`, `contract:emit`, `typecheck`, the hooks, CI, Sonar, prettier, the .gitignore block) and each side's managed files.
20
+ - Breaking: the front end reads `be/contracts/` in place for its codegen (`sides.fe.reads`); the front-end contract copy and its `HFS_CONTRACT_SNAPSHOT_DRIFT` hash comparison are gone.
21
+ - Changed: one version per dependency in the one package.json: a conflict between the sides takes its canon pin, otherwise the higher version, pinned in `knowledge/hfs/canon-pins.yaml` with a why (`graphql` 16.14.2).
22
+ - Changed: a side finding names every path from the app root, in its message as in its path (`fe/apps/web/...`, `be/src/...`), in `hfs check` and in the ESLint and stylelint findings of `hfs lint`.
23
+ - Fixed: the architecture machine gives each file to the deepest tsconfig project that holds it, so a route file importing its feature through the app alias `@/` mounts its owner (`FE_ROUTE_FILES_THIN` reported "0 feature owners"); the skeleton route slots import through the alias. A route file binding a declaration to a Next.js segment export (`generateMetadata`, ...) is not `HFS_ALIAS_REEXPORT`.
24
+
3
25
  ## 3.0.0 - 2026-10-01
4
26
 
5
27
  - Breaking: one entry, `hfs lint`. It runs the repository's eslint, the `hfs check` repository pass and, for a front end, stylelint, and writes one `starci/lint@1` report; `--sonar <file>` writes the single Sonar import (engines starci-hfs, eslint and stylelint in one document); `--changed <files...> --format json` is the land-gate shape. `hfs report`, `hfs check --sonar` and the managed scripts `lint:check`, `lint:report`, `hfs:check` and `hfs:report` are deleted; the managed `lint` script is `hfs lint`, and CI, hooks and the Sonar template read `reports/lint.sonar.json`.
package/README.md CHANGED
@@ -1,15 +1,17 @@
1
1
  # @starci/hfs
2
2
 
3
- The HFS command line of a StarCi product repository. 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 one `package.json`, lockfile and `node_modules`, `hfs.json` of kind `app`, CI, hooks, `.starciwork`) and its two sides `be/` and `fe/`. It is installed from the npm registry at the exact version in `knowledge/hfs/canon-pins.yaml` (see [`packages/README.md`](../README.md)),
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
- checkout. The machine loads `typescript` from the repository it checks (never its own copy), so run `npm ci` first.
6
+ checkout. The machine loads `typescript` from the app it checks (never its own copy), so run `npm ci` first.
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/lib/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/...`).
7
9
 
8
10
  ```sh
9
- npx hfs lint [--repo <dir>] [--changed <file>...] [--fix] [--format text|json] [--sonar <file>] [--stylelint <glob>] # THE lint entry (`npm run lint`): exit 0 clean, 1 findings, 2 a tool could not run
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
10
12
  npx hfs check [--repo <dir>] [--json] [--fast] [--base <ref>] # the repository pass alone (what `hfs lint` runs for the findings that have no TypeScript file); exit 1 on any error-level finding
11
- npx hfs init [--repo <dir>] [--stdout] # write a starter hfs.json (never overwrites); --stdout only prints
12
- npx hfs emit-contracts [--repo <dir>] # write contracts/<app>/schema.graphql of every api app that serves GraphQL and contracts/<app>/openapi.json of every api app with a typed operation table (the managed script contract:emit)
13
+ npx hfs scaffold app <name> [--into <dir>] # a new app <dir>/<name>/: the root, be/ and fe/ skeletons and the app hfs.json; refuses an existing directory
14
+ npx hfs emit-contracts [--repo <dir>] # write be/contracts/<app>/schema.graphql of every api app that serves GraphQL and be/contracts/<app>/openapi.json of every api app with a typed operation table (the managed script contract:emit)
13
15
  npx hfs explain <path> [--repo <dir>] [--json]
14
16
  npx hfs sync (--check | --write) [--root <dir>] # generated files: the managedBy slots of slots.yaml, the .gitignore block (sync/, templates/)
15
17
  npx hfs work-hygiene # pre-commit guard: staged .starciwork / .starcistacks paths, and the secrets guard over every staged file (read from the index)
@@ -26,8 +28,8 @@ Only `*.service.ts` files are unit-tested (unit test standard), each with exactl
26
28
 
27
29
  The spec skeleton is `Test.createTestingModule({ providers: [Service, { provide: TOKEN, useValue: double }, ...] }).compile()` and `moduleRef.get(Service)`, with one provider per constructor dependency and nothing else, every double imported from `@starci/jest-preset` (the root), and one placeholder `it` per public method. The double of a token comes from `ruleParams.be.specDoubles` of the slot manifest, the table the lint law `spec-infra-double-from-kit` holds a spec to (`*_ENTITY_MANAGER` -> `mockEntityManager()`, `CLOCK` -> `new FakeClock(...)`, `OUTBOX` -> `recordingOutbox()`, `CACHE` -> `fakeCache(clock)`, lock, lease, fence and hold -> `fakeLock(clock)`, ids -> `fakeIds()`, `*_OPTIONS` -> a literal to fill in, everything else and every class -> `mock<T>()`), so the skeleton satisfies the law by construction: no cast, no `new` of the service, no ambient clock. A back end only (a front end has no services); the files are written for prettier (print width 120).
28
30
 
29
- `hfs check` reads the repository's `hfs.json` and the tracked paths (`git ls-files`), checks the work tree, and then runs the
30
- whole architecture machine over the repository. Every finding carries a why code and its Vietnamese text
31
+ `hfs check` reads the app's `hfs.json` and the tracked paths (`git ls-files`), checks the work tree (the root rules once, the side rules per side), and then runs the
32
+ whole architecture machine over each side folder. Every finding carries a why code and its Vietnamese text
31
33
  (`modules/kernel/failure-codes.yaml`).
32
34
 
33
35
  Its own checks (`scripts/lib/hfs-check.mjs`, `scripts/lib/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):
@@ -55,12 +57,13 @@ Its own checks (`scripts/lib/hfs-check.mjs`, `scripts/lib/hfs-rules/`; the rende
55
57
  | `HFS_STACKS_SHAPE` | error | a `.starcistacks` path outside the standard shape (a sealed file outside `<env>/secrets/`, `runtime/files/`, root `DESIGN.md` or `k8s/`), a local Sonar not owned by the host, a service still rooted at `.stacks` (R10) |
56
58
  | `HFS_CI_MISSING_CANON` | error | `ci.yml` without a `run:` step of `hfs lint` (`npm run lint`, or `npx hfs lint` at the pinned version), or `.husky/pre-push` without `npm run typecheck` and `npm run lint` (R13) |
57
59
  | `HFS_DEP_VERSION_SKEW` | error | a dependency at two specs across the root and workspace `package.json` files, a dependency declared at another version than the root `overrides` pin, or a nested copy of a declared dependency in `package-lock.json` (R14) |
58
- | `HFS_CONTRACT_SNAPSHOT_DRIFT` | error / info | a back end serving GraphQL without `contracts/<app>/schema.graphql`, a front-end contract copy that differs by hash from the sibling back end named by `hfs.json` `stacks` (info when the sibling is not checked out) (R23) |
60
+ | `HFS_CONTRACT_SNAPSHOT_DRIFT` | error | a back-end api app serving GraphQL without `be/contracts/<app>/schema.graphql` (R23); the front end reads that snapshot in place, so there is no copy to drift |
59
61
  | `BE_TEST_TOPOLOGY` | error | a `*.test.*` file, a `testing/` folder, a second jest configuration or a `jest` key in `package.json` (R47; `int-spec`, `harness-spec` and the retired test folders are the machine's `HFS_TEST_KIND_RETIRED`) |
60
62
  | `BE_SPEC_PLACEMENT` | error | a `*.spec.*`, `*.test.*` or `*-spec.*` file outside the four test layers, `scripts/` and `tools/` included (R102) |
61
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) |
62
64
  | `HFS_LINT_SUPPRESSION_FILE` | error | an `eslint.suppressions*` file, a `lint:suppressions` script, an eslint suppress flag or a suppressions config (R104) |
63
65
  | `HFS_PROOF_COMMAND_FILE_MISSING` | error | a `.starciwork` `requiresProof.<kind>.command` that runs a file the repository does not hold (R105) |
66
+ | `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) |
64
67
  | `FE_WIRE_GENERATED` | error | a contract copy with no `codegen` script wired before `build` and `typecheck`, or generated types older than the copy (R52) |
65
68
  | `FE_I18N_PLACEMENT` | error | no `next-intl`, no `src/proxy.ts`, a `middleware.ts`, a route file outside `[locale]`, no `vi.json` catalog (R59) |
66
69
  | `FE_I18N_CATALOG` | error | a locale catalog lacking a key another locale has (R60) |
@@ -74,9 +77,8 @@ Its own checks (`scripts/lib/hfs-check.mjs`, `scripts/lib/hfs-rules/`; the rende
74
77
  The managed files are judged by `sync/managed.mjs` and `sync/ts-strict.mjs` (the package renders the templates; the runtime copy does not). The list of managed
75
78
  files is the `managedBy` slots of `slots.yaml`; `hfs sync --write` renders them and `hfs check` compares them, so a hand edit and a forgotten `sync` are the same finding.
76
79
  Each finding is reported once: the eslint and stylelint one-liners under R17, `tsconfig.json` under R22 when it names a flag, a workflow or hook that lost a canon step under R13 (or R19 for the format step), everything else under R05.
77
- A front end is rendered by the same mechanism as a back end: `tsconfig.json`, `eslint.config.mjs`, `stylelint.config.mjs`, `.prettierrc`, `.prettierignore`, the hooks, the workflow, the Sonar file
78
- and the `scripts` block (`lint` is the one lint gate, `hfs lint`: ESLint over the repository, the repository check and, for a front end, stylelint; `lint:fix` is the same with `--fix`; a 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, test script or test dependency, `scripts/` included). `turbo.json` stays the repository's own
79
- (`fe.tool-config-repo`): it carries the repository's task graph, which no preset can render.
80
+ 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
81
+ (`fe.tool-config-repo`): it carries the task graph, which no preset can render.
80
82
 
81
83
  The architecture machine (`scripts/checks/architecture.mjs` of the runtime, the same code bundled here): tiers and import
82
84
  direction, owner public API, cycles, module registration and composition, clones, dead exports, required files,
@@ -90,26 +92,26 @@ of the changed source files without clones and dead exports, and no file-system
90
92
  checks still cover the whole tree. With no merge-base `--fast` is a refusal (exit 2) that names the fix, never a silent full
91
93
  pass.
92
94
 
93
- Exit codes: 0 clean, 1 an error finding, 2 a refusal (not a Git work tree, bad flag, `--fast` with no merge-base). The command never writes to the repository
94
- except `hfs init`, which writes `hfs.json` only when none exists.
95
+ Exit codes: 0 clean, 1 an error finding, 2 a refusal (not a Git work tree, bad flag, `--fast` with no merge-base). `hfs check` and `hfs lint` never write to the app
96
+ (`hfs lint --fix` lets ESLint and stylelint fix in place).
95
97
 
96
98
  ## Sonar
97
99
 
98
- One mechanism, the same for a back end and a front end: every finding of the canon is imported into Sonar, and the quality gate
99
- fails while any is open. Nothing is configured per repository; the pieces are managed files (`hfs sync`) and this package.
100
+ One mechanism for both sides: every finding of the canon is imported into Sonar, and the quality gate
101
+ fails while any is open. Nothing is configured per app; the pieces are managed files (`hfs sync`) and this package.
100
102
 
101
- One entry, one report, one file: `npm run lint` is `hfs lint`; `npm run lint -- --sonar reports/lint.sonar.json` writes the ONE Sonar file, read through `sonar.externalIssuesReportPaths`. It carries three engines: `starci-hfs` (the repository findings of `hfs check`, rule id = the finding code), `eslint` (the BE and FE canon plugins alike, rule id = the ESLint rule) and, for a front end, `stylelint` (rule id = the stylelint rule). `hfs lint --format json` prints the same findings as the one report `starci/lint@1` (`{ schema, ok, changed, counts.error, engines, errors[], findings[{ engine, rule, code, severity, path, line, column, message }] }`); `--changed <files...>` restricts ESLint and stylelint to those files and keeps of the repository findings the ones on a listed file or on no file. The exit code is 0 clean, 1 findings, 2 a tool could not run (a missing ESLint install is never a pass).
103
+ One entry, one report, one file: `npm run lint` is `hfs lint`; `npm run lint -- --sonar reports/lint.sonar.json` writes the ONE Sonar file, read through `sonar.externalIssuesReportPaths`. It carries three engines: `starci-hfs` (the repository findings of `hfs check`, rule id = the finding code), `eslint` (the BE and FE canon plugins alike, rule id = the ESLint rule) and `stylelint` over fe (rule id = the stylelint rule). `hfs lint --format json` prints the same findings as the one report `starci/lint@1` (`{ schema, ok, changed, counts.error, engines, errors[], findings[{ engine, rule, code, severity, path, line, column, message }] }`); `--changed <files...>` restricts ESLint and stylelint to those files and keeps of the repository findings the ones on a listed file or on no file. The exit code is 0 clean, 1 findings, 2 a tool could not run (a missing ESLint install is never a pass).
102
104
 
103
105
  `--sonar` writes the findings as a Generic Issue Import document (SonarQube 10.3+ format: `{ rules, issues }`) before the verdict, so a
104
106
  failing lint still leaves its report. A rule's name and description are the catalog's English title and Vietnamese title, meaning and next step;
105
107
  impacts are HIGH (maintainability). `info` findings (the soft-size backlog) are report-only and not imported. The output is sorted, so two runs
106
108
  over one tree are byte-identical. Sonar drops an issue on a file it does not index (a tracked source file or stylesheet under `sonar.sources`), so the ONE placement rule of the three engines files a finding on
107
109
  any other path (hfs.json, a workflow, a config file, `e2e/`, a package the sources do not list) and a finding with no path on the first source file, its message starting with the real path.
108
- Sonar's own ESLint import (`sonar.eslint.reportPaths`) is not used: it drops those issues silently. A front end's `sonar.sources` and `sonar.tests` are `apps`, plus `packages` when hfs.json opts into `repo.packages` or an `fe.package.*` slot.
110
+ Sonar's own ESLint import (`sonar.eslint.reportPaths`) is not used: it drops those issues silently. `sonar.sources` lists `be/apps`, `be/src` and `fe/apps` (plus `fe/packages` when the front end opts into an `fe.package.*` slot); `sonar.tests` is the back end's.
109
111
 
110
112
  The managed `sonar-project.properties` carries `sonar.externalIssuesReportPaths` (`reports/lint.sonar.json` only) and no `sonar.host.url` (the host is
111
113
  `SONAR_HOST_URL`); the managed CI workflow produces the report and runs the scan and the gate action with `!cancelled()`, so a failed lint step still
112
- reaches Sonar while the job stays failed. There is no `continue-on-error`. Both profiles run `npm run lint -- --sonar reports/lint.sonar.json` once, before the scan. The duplicate-block threshold (`ruleParams.<profile>.duplicateBlock`) has no Sonar property for TypeScript (SonarJS detects
114
+ reaches Sonar while the job stays failed. There is no `continue-on-error`. CI runs `npm run lint -- --sonar reports/lint.sonar.json` once at the app root, before the scan. The duplicate-block threshold (`ruleParams.<side>.duplicateBlock`) has no Sonar property for TypeScript (SonarJS detects
113
115
  clones with its own token rule), so the machine enforces it (R21) and its findings are imported like every other.
114
116
 
115
117
  The gate is `knowledge/sonar-gate.yaml`, the one declaration: the new-code conditions (duplication, blocker and critical issues, hotspots; no coverage condition) and an `overall` part (0 open issues on the whole code,
@@ -117,7 +119,7 @@ duplicated lines density, cognitive complexity through the S3776 rule). A SonarQ
117
119
  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
118
120
  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.
119
121
 
120
- Sonar does not depend on coverage: the managed properties file has no `sonar.*.lcov.reportPaths` and no `sonar.coverage.*`, the gate has no coverage condition, and the managed CI workflow uploads no coverage anywhere (no Codecov). A back end's unit coverage is the runner's: the managed `test` script is `jest --selectProjects unit --coverage`, which fails below the per-file 100 threshold on `src/**/*.service.ts`.
122
+ Sonar does not depend on coverage: the managed properties file has no `sonar.*.lcov.reportPaths` and no `sonar.coverage.*`, the gate has no coverage condition, and the managed CI workflow uploads no coverage anywhere (no Codecov). The back end's unit coverage is the runner's: the managed `test` script is `jest --selectProjects unit --coverage`, which fails below the per-file 100 threshold on `src/**/*.service.ts`.
121
123
 
122
124
  ## Maintaining the bundle
123
125
 
@@ -128,8 +130,10 @@ derived from the machine's rule id lists). After changing any of those files, `k
128
130
  `knowledge/hfs/canon-pins.yaml`, `knowledge/patterns/fe/folder.yaml` or the catalog entries of those codes, run `node packages/hfs/scripts/sync-runtime.mjs`;
129
131
  `tests/hfs-cli.spec.mjs` fails on a stale copy. Bump `version` here and in the pin when the behaviour changes.
130
132
 
131
- The examples gate `node scripts/checks/check-example-architecture.mjs` runs this CLI (full check) on every `examples/*` directory
132
- with an `hfs.json` and fails on any error-level finding (it is heavy: run it once, by hand).
133
+ The examples gate `node scripts/checks/check-example-architecture.mjs` runs `hfs lint` of this CLI at the root of every `examples/*`
134
+ app with an `hfs.json` (`examples/todo-app`, `examples/ecommerce-app`) and fails on any finding or any tool that could not run;
135
+ `hfs check` alone would miss the machine's source rules, which the canons judge. It is heavy: run it once, by hand, after `npm ci`
136
+ in each app.
133
137
 
134
138
  ## Serving knowledge to other packages
135
139
 
package/bin/hfs.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
- // hfs - the HFS command line of a StarCi product repository.
2
+ // hfs - the HFS command line of a StarCi app: one repository, `<app>/`, with one package.json, lockfile and node_modules at its root
3
+ // and two sides, be/ and fe/, declared by the one hfs.json of kind app. Every verb runs at the app root.
3
4
  // hfs check [--repo <dir>] [--json] [--fast] [--base <ref>]
4
5
  // every tracked path has a slot; required files exist; nothing forbidden or
5
6
  // tracked-that-must-be-ignored; pins match; every managed file equals its render
@@ -13,43 +14,47 @@
13
14
  // --fast: only what changed since the merge-base with origin/main (else main; --base
14
15
  // overrides it); the machine
15
16
  // runs on those owners without clones and dead exports. No merge-base is a refusal
16
- // (exit 2), never a silent full pass. Exit 1 on any error-level finding.
17
- // hfs init [--repo <dir>] [--stdout] write a starter hfs.json by detecting the profile and the apps.
18
- // hfs lint [--repo <dir>] [--changed <file>...] [--fix] [--format text|json] [--sonar <file>] [--stylelint <glob>]
19
- // the ONE lint entry (npm run lint): ESLint (canon per-file rules and the project-graph rules)
20
- // plus this check's repository findings plus stylelint, as one starci/lint@1 report; --sonar writes
21
- // the one Sonar import file. Exit 0 clean, 1 findings, 2 a tool could not run (lint/run.mjs).
17
+ // (exit 2), never a silent full pass. Exit 1 on any error-level finding. The root checks run once and
18
+ // the side checks and the machine once per side, the side folder as their root; every path is app-relative.
19
+ // hfs lint [--repo <dir>] [--changed <file>...] [--fix] [--format text|json] [--sonar <file>]
20
+ // the ONE lint entry (npm run lint): ESLint per side with that side's canon config (per-file rules and
21
+ // the project-graph rules), this check's findings, and stylelint over the fe side, as one starci/lint@1
22
+ // report; --sonar writes the one Sonar import file. Exit 0 clean, 1 findings, 2 a tool could not run (lint/run.mjs).
23
+ // hfs scaffold app <name> [--into <dir>] a new app <dir>/<name>/: the root (hfs.json, package.json, managed files, .starciwork) and the
24
+ // be/ and fe/ skeletons (scaffold/app.mjs). Refuses an existing directory.
22
25
  // hfs explain <path> [--repo <dir>] [--json] which slot owns the path, its tier, allowed imports, required tests.
23
- // hfs emit-contracts [--repo <dir>] write contracts/<app>/schema.graphql of every api app that serves GraphQL (emit/contracts.mjs):
26
+ // hfs emit-contracts [--repo <dir>] write be/contracts/<app>/schema.graphql of every api app that serves GraphQL (emit/contracts.mjs):
24
27
  // printSchema(lexicographicSortSchema) of the resolvers the app root composes; no env, no database, no network.
25
- // hfs sync (--check | --write) [--root <dir>] the generated files (husky, CI, .gitignore block, sonar); sync/cli.mjs
28
+ // hfs sync (--check | --write) [--root <dir>] the managed files of the root (scripts, husky, CI, .gitignore block, sonar, prettier) and
29
+ // of each side (tsconfig, eslint, jest, stylelint); sync/cli.mjs
26
30
  // hfs work-hygiene the pre-commit guard: staged .starciwork and .starcistacks paths, and the secrets guard over every staged file (read from the index); sync/cli.mjs
27
31
  // hfs new service <dir> <name> [--inject <Decorator>=<module>:<Type> | <Class>=<module>]... [--repo <dir>]
28
- // a back-end `<name>.service.ts` and its `<name>.service.spec.ts` skeleton (scaffold/service.mjs): the spec is built
32
+ // a back-end `<name>.service.ts` and its `<name>.service.spec.ts` skeleton (scaffold/service.mjs; <dir> is
33
+ // relative to be/): the spec is built
29
34
  // with Test.createTestingModule, one provider per constructor dependency (kit doubles from @starci/jest-preset),
30
35
  // one placeholder it per public method. Never overwrites a file.
31
36
  // hfs new spec <file>.service.ts [--repo <dir>] the spec skeleton of an existing service, read from its constructor with the repository's TypeScript
32
- // Every finding names a why code and carries its Vietnamese text. The command reads the repository, never writes to it
33
- // (init writes hfs.json only, and only when none exists). Exit codes: 0 clean, 1 error findings, 2 a refusal or bad usage.
34
- import fs from 'node:fs';
37
+ // Every finding names a why code and carries its Vietnamese text. check, lint and explain read the app, never write to it. Exit codes:
38
+ // 0 clean, 1 error findings, 2 a refusal or bad usage.
35
39
  import path from 'node:path';
36
40
  import { fileURLToPath } from 'node:url';
37
41
  import { isMain } from '../runtime/scripts/checks/common.mjs';
38
- import { checkRepository, explainPath, initRepo, trackedFiles } from '../runtime/scripts/lib/hfs-check.mjs';
39
- import { HfsSlotsError } from '../runtime/scripts/lib/hfs-slots.mjs';
42
+ import { checkRepository, explainPath, trackedFiles } from '../runtime/scripts/lib/hfs-check.mjs';
43
+ import { HfsSlotsError, loadSlotManifest, readRepoDeclaration } from '../runtime/scripts/lib/hfs-slots.mjs';
40
44
  import { formatFindings } from '../sync/format.mjs';
41
45
  import { main as syncMain } from '../sync/cli.mjs';
42
- import { SyncError } from '../sync/index.mjs';
46
+ import { SyncError, loadPresets } from '../sync/index.mjs';
43
47
  import { managedFindings } from '../sync/managed.mjs';
44
48
  import { emitContracts } from '../emit/contracts.mjs';
45
49
  import { ScaffoldError, newService, newSpec } from '../scaffold/service.mjs';
50
+ import { scaffoldApp } from '../scaffold/app.mjs';
46
51
  import { contractEmitFindings } from '../runtime/scripts/lib/hfs-rules/contract.mjs';
47
52
  import { lintRepository, parseLintArgs, printLintText } from '../lint/run.mjs';
48
53
  import { writeReport } from '../report/sonar.mjs';
49
54
 
50
55
  const USAGE = `hfs check [--repo <dir>] [--json] [--fast] [--base <ref>]
51
- hfs lint [--repo <dir>] [--changed <file>...] [--fix] [--format text|json] [--sonar <file>] [--stylelint <glob>]
52
- hfs init [--repo <dir>] [--stdout]
56
+ hfs lint [--repo <dir>] [--changed <file>...] [--fix] [--format text|json] [--sonar <file>]
57
+ hfs scaffold app <name> [--into <dir>]
53
58
  hfs emit-contracts [--repo <dir>]
54
59
  hfs explain <path> [--repo <dir>] [--json]
55
60
  hfs sync (--check | --write) [--root <dir>]
@@ -58,10 +63,10 @@ hfs new service <dir> <name> [--inject <Decorator>=<module>:<Type> | <Class>=<mo
58
63
  hfs new spec <file>.service.ts [--repo <dir>]
59
64
  `;
60
65
  const PER_CODE_LIMIT = 25;
61
- const VALUE_FLAGS = new Set(['--repo', '--base', '--inject']);
66
+ const VALUE_FLAGS = new Set(['--repo', '--base', '--inject', '--into']);
62
67
  /** Flags that may repeat: their values are collected in order. */
63
68
  const LIST_FLAGS = new Set(['--inject']);
64
- const BOOL_FLAGS = new Set(['--json', '--stdout', '--fast']);
69
+ const BOOL_FLAGS = new Set(['--json', '--fast']);
65
70
 
66
71
  function parse(argv) {
67
72
  const opts = { positional: [] };
@@ -114,20 +119,25 @@ function printExplain(e, out) {
114
119
  if (e.code) out(` ${e.code}: ${e.titleVi}\n ${e.whyVi}\n`);
115
120
  }
116
121
 
117
- /** The repository pass: slots, managed files, formatter, contract snapshots and the architecture machine's check surface. `only` limits the formatter to those files. */
122
+ /**
123
+ * The app pass: slots, managed files, formatter, contract snapshots and the architecture machine's check surface, root and sides.
124
+ * `only` limits the formatter to those (app-relative) files.
125
+ */
118
126
  async function runCheck({ repoRoot, fast = false, base, only, presets, prettier }) {
119
127
  const tracked = trackedFiles(repoRoot);
120
- // Managed files, the .gitignore block and sonar against their render (R04, R05, R11, ...), and prettier through the repository's own install (R19, never under --fast).
128
+ // Managed files of the root and both sides, the .gitignore block and sonar against their render (R04, R05, R11, ...), and prettier
129
+ // through the app's own install (R19, never under --fast).
121
130
  const extraFindings = [...await managedFindings({ repoRoot, tracked, presets }), ...(fast ? [] : await formatFindings({ repoRoot, files: only ?? tracked, prettier }))];
122
- // R23 against the app itself (full pass only): the committed snapshots equal what `emit-contracts` writes now.
131
+ // R23 against the be side itself (full pass only): the committed snapshots equal what `emit-contracts` writes now.
123
132
  let contracts = null;
124
- let declared = null;
125
- try { declared = JSON.parse(fs.readFileSync(path.join(repoRoot, 'hfs.json'), 'utf8')); } catch { /* checkRepository reports the unreadable declaration */ }
126
- if (declared?.profile === 'be') {
133
+ let be = null;
134
+ try { be = readRepoDeclaration(loadSlotManifest(), repoRoot).sides?.be ?? null; } catch { /* checkRepository reports the unreadable declaration */ }
135
+ if (be) {
127
136
  if (fast) contracts = { status: 'skipped', reason: '--fast does not emit the apps' };
128
137
  else {
129
- const emitted = contractEmitFindings({ repoRoot, files: tracked, repo: declared, emit: emitContracts });
130
- extraFindings.push(...emitted.findings);
138
+ const files = tracked.filter((file) => file.startsWith('be/')).map((file) => file.slice('be/'.length));
139
+ const emitted = contractEmitFindings({ repoRoot: path.join(repoRoot, 'be'), files, repo: be, emit: emitContracts });
140
+ extraFindings.push(...emitted.findings.map((finding) => ({ ...finding, side: 'be', path: `be/${finding.path}` })));
131
141
  contracts = { status: 'checked', apps: emitted.apps };
132
142
  }
133
143
  }
@@ -136,7 +146,7 @@ async function runCheck({ repoRoot, fast = false, base, only, presets, prettier
136
146
  return result;
137
147
  }
138
148
 
139
- /** `hfs lint`: ESLint, the repository checks and (front end) stylelint as one report; see lint/run.mjs. */
149
+ /** `hfs lint`: ESLint per side, the app checks and stylelint over the fe side as one report; see lint/run.mjs. */
140
150
  async function lintMain(argv, { stdout, presets, prettier }) {
141
151
  const opts = parseLintArgs(argv);
142
152
  const repoRoot = path.resolve(opts.repo ?? process.cwd());
@@ -150,10 +160,20 @@ async function lintMain(argv, { stdout, presets, prettier }) {
150
160
  return exit;
151
161
  }
152
162
 
163
+ /**
164
+ * The jest preset a new app renders its Sonar exclusions from: the one installed beside this @starci/hfs (`npx -p @starci/hfs -p
165
+ * @starci/jest-preset hfs scaffold app <name>`, or an install that holds both). None is a refusal, never a render without it.
166
+ */
167
+ async function scaffoldPresets() {
168
+ try { return await loadPresets(path.dirname(fileURLToPath(import.meta.url))); } catch (error) {
169
+ throw new SyncError('HFS_SYNC_PRESET_MISSING', `${error.message.replace(/^[A-Z_]+: /, '')}; run hfs scaffold with @starci/jest-preset installed beside @starci/hfs (npx -p @starci/hfs -p @starci/jest-preset hfs scaffold app <name>)`);
170
+ }
171
+ }
172
+
153
173
  /** `presets` and `prettier` are test seams: the Sonar exclusions sync would load from the repository's installed preset, and the repository's own prettier. */
154
174
  export async function main(argv, { stdout = (s) => process.stdout.write(s), stderr = (s) => process.stderr.write(s), presets, prettier } = {}) {
155
175
  const [verb, ...rest] = argv;
156
- if (!['check', 'lint', 'init', 'explain', 'sync', 'work-hygiene', 'emit-contracts', 'new'].includes(verb)) { stderr(USAGE); return 2; }
176
+ if (!['check', 'lint', 'scaffold', 'explain', 'sync', 'work-hygiene', 'emit-contracts', 'new'].includes(verb)) { stderr(USAGE); return 2; }
157
177
  try {
158
178
  if (verb === 'sync' || verb === 'work-hygiene') return await syncMain(argv);
159
179
  if (verb === 'lint') return await lintMain(rest, { stdout, presets, prettier });
@@ -168,9 +188,10 @@ export async function main(argv, { stdout = (s) => process.stdout.write(s), stde
168
188
  }
169
189
  if (verb === 'emit-contracts') {
170
190
  if (opts.positional.length) throw new Error('hfs emit-contracts takes no path');
171
- const declaration = JSON.parse(fs.readFileSync(path.join(repoRoot, 'hfs.json'), 'utf8'));
172
- const { written, skipped, standIns } = emitContracts({ repoRoot, declaration });
173
- for (const file of written) stdout(`wrote ${file}
191
+ const be = readRepoDeclaration(loadSlotManifest(), repoRoot).sides?.be;
192
+ if (!be) throw new Error('hfs emit-contracts runs at the app root (the folder of hfs.json)');
193
+ const { written, skipped, standIns } = emitContracts({ repoRoot: path.join(repoRoot, 'be'), declaration: be });
194
+ for (const file of written) stdout(`wrote be/${file}
174
195
  `);
175
196
  stdout(`hfs emit-contracts: ${written.length} written${skipped.length ? `, ${skipped.join(', ')} serve no GraphQL` : ''}
176
197
  `);
@@ -190,10 +211,11 @@ export async function main(argv, { stdout = (s) => process.stdout.write(s), stde
190
211
  `);
191
212
  return 0;
192
213
  }
193
- if (verb === 'init') {
194
- if (opts.positional.length) throw new Error('hfs init takes no path');
195
- const result = initRepo({ repoRoot, write: !opts.stdout });
196
- if (opts.stdout) stdout(result.text); else stdout(`hfs init: wrote ${result.file} (${result.declaration.profile}, ${result.declaration.apps.map((a) => `${a.name}:${a.kind}`).join(', ')})\n`);
214
+ if (verb === 'scaffold') {
215
+ const [kind, name, ...extra] = opts.positional;
216
+ if (kind !== 'app' || !name || extra.length || opts.repo !== undefined) throw new Error('hfs scaffold takes `app <name> [--into <dir>]`');
217
+ const { root: created, files } = scaffoldApp({ name, into: path.resolve(opts.into ?? process.cwd()), presets: presets ?? await scaffoldPresets() });
218
+ stdout(`hfs scaffold app: created ${created} (${files.length} files); next: npm ci, then npm run lint\n`);
197
219
  return 0;
198
220
  }
199
221
  if (opts.positional.length !== 1) throw new Error('hfs explain takes exactly one path');
package/lint/run.mjs CHANGED
@@ -1,36 +1,41 @@
1
- // hfs lint - the ONE lint entry and the ONE report of a StarCi repository.
1
+ // hfs lint - the ONE lint entry and the ONE report of a StarCi app.
2
2
  //
3
- // hfs lint [--repo <dir>] [--changed <file>...] [--fix] [--format text|json] [--sonar <file>] [--stylelint <glob>]
3
+ // hfs lint [--repo <dir>] [--changed <file>...] [--fix] [--format text|json] [--sonar <file>]
4
4
  //
5
- // It runs, over the repository (or over the files named by --changed):
6
- // 1. ESLint through the repository's own install and eslint.config.mjs (the canon plugin: the per-file rules and the project-graph
7
- // rules of the architecture machine, findings on the line of the offending TypeScript file);
8
- // 2. `hfs check` for what has no TypeScript file to sit on (the tree, managed files, pins, CI, contracts, docs, .starciwork);
9
- // 3. stylelint when --stylelint names the style glob (front ends).
10
- // Their findings become one list of one shape (`starci/lint@1`). `--format json` prints that report on stdout; `--sonar <file>` writes the
11
- // same findings as THE Sonar Generic Issue Import file (engine ids starci-hfs, eslint, stylelint in one document); the exit code is the
12
- // land-gate input: 0 no finding, 1 at least one, 2 a tool could not run (never a pass).
13
- // --changed restricts ESLint and stylelint to the listed files and keeps of the repository findings only those on a listed file or on no file.
5
+ // It runs at the app root (the folder of hfs.json), over the app (or over the app-relative files named by --changed):
6
+ // 1. ESLint once per side, from the side folder, through the app's own install and that side's eslint.config.mjs (be/ the be canon,
7
+ // fe/ the fe canon: the per-file rules and the project-graph rules of the architecture machine, findings on the line of the
8
+ // offending TypeScript file); each side lints only its own files, so the be rules never see fe/ and the fe rules never see be/;
9
+ // 2. `hfs check` for what has no TypeScript file to sit on (the tree, managed files, pins, CI, contracts, docs, .starciwork), root and sides;
10
+ // 3. stylelint over the fe side's stylesheets (sync STYLE_GLOB), from fe/ with its stylelint.config.mjs.
11
+ // Their findings become one list of one shape (`starci/lint@1`), every path app-relative. `--format json` prints that report on stdout;
12
+ // `--sonar <file>` writes the same findings as THE Sonar Generic Issue Import file (engine ids starci-hfs, eslint, stylelint in one
13
+ // document); the exit code is the land-gate input: 0 no finding, 1 at least one, 2 a tool could not run (never a pass).
14
+ // --changed restricts ESLint and stylelint to the listed files and keeps of the app findings only those on a listed file or on no file.
14
15
  import fs from 'node:fs';
15
16
  import path from 'node:path';
16
17
  import { createRequire } from 'node:module';
17
18
  import { spawnSync } from 'node:child_process';
18
19
  import { linterReport, mergeReports, sonarReport, sourceRootsOf } from '../report/sonar.mjs';
20
+ import { SIDES, appRelativeMessages, loadSlotManifest, readRepoDeclaration } from '../runtime/scripts/lib/hfs-slots.mjs';
21
+ import { STYLE_GLOB } from '../sync/index.mjs';
19
22
 
20
23
  export const LINT_SCHEMA = 'starci/lint@1';
24
+ /** The side whose stylesheets stylelint judges. */
25
+ const STYLE_SIDE = 'fe';
21
26
  const CODE_PREFIX = /^\[([A-Z][A-Z0-9_]+)\] /;
22
27
  const posix = (file) => String(file).replace(/\\/g, '/').replace(/^\.\//, '');
23
28
 
24
29
  /** The flags of `hfs lint`; `--changed` takes every argument up to the next flag. */
25
30
  export function parseLintArgs(argv) {
26
- const opts = { changed: null, fix: false, format: 'text', repo: undefined, sonar: undefined, stylelint: undefined };
31
+ const opts = { changed: null, fix: false, format: 'text', repo: undefined, sonar: undefined };
27
32
  for (let i = 0; i < argv.length; i += 1) {
28
33
  const arg = argv[i];
29
34
  if (arg === '--changed') {
30
35
  opts.changed = [];
31
36
  while (i + 1 < argv.length && !argv[i + 1].startsWith('--')) opts.changed.push(posix(argv[++i]));
32
37
  } else if (arg === '--fix') opts.fix = true;
33
- else if (['--format', '--repo', '--sonar', '--stylelint'].includes(arg)) {
38
+ else if (['--format', '--repo', '--sonar'].includes(arg)) {
34
39
  if (argv[i + 1] === undefined) throw new Error(`${arg} needs a value`);
35
40
  opts[arg.slice(2)] = argv[++i];
36
41
  } else throw new Error(`unknown argument ${arg}`);
@@ -39,39 +44,43 @@ export function parseLintArgs(argv) {
39
44
  return opts;
40
45
  }
41
46
 
42
- /** A linter's json results through the repository's own install: `{ results }` or `{ error }`. */
43
- function runLinter({ repoRoot, pkg, bin, args }) {
47
+ /** A linter's json results through the app's own install, run from `cwd` (a side folder): `{ results }` or `{ error }`. */
48
+ function runLinter({ cwd, pkg, bin, args }) {
44
49
  let entry;
45
50
  try {
46
- const manifest = createRequire(path.join(repoRoot, 'package.json')).resolve(`${pkg}/package.json`);
51
+ const manifest = createRequire(path.join(cwd, 'package.json')).resolve(`${pkg}/package.json`);
47
52
  const declared = JSON.parse(fs.readFileSync(manifest, 'utf8')).bin;
48
53
  entry = path.join(path.dirname(manifest), typeof declared === 'string' ? declared : declared[bin]);
49
54
  } catch {
50
- return { error: `${pkg} is not installed under ${repoRoot}` };
55
+ return { error: `${pkg} is not installed for ${cwd}` };
51
56
  }
52
- const run = spawnSync(process.execPath, [entry, ...args], { cwd: repoRoot, encoding: 'utf8', maxBuffer: 512 * 1024 * 1024 });
53
- // The linters exit 1 when they found something; no json at all means they could not run.
57
+ const run = spawnSync(process.execPath, [entry, ...args], { cwd, encoding: 'utf8', maxBuffer: 512 * 1024 * 1024 });
58
+ // The linters exit 1 when they found something; no json at all means they could not run. ESLint prints its report on stdout,
59
+ // stylelint (16 and later) its formatter output on stderr.
54
60
  let results;
55
- try { results = JSON.parse(run.stdout); } catch { return { error: `${pkg} produced no json report (exit ${run.status}): ${String(run.stderr || run.stdout).trim().split('\n')[0]}` }; }
61
+ try { results = JSON.parse(run.stdout.trim() || run.stderr); } catch { return { error: `${pkg} produced no json report in ${path.basename(cwd)}/ (exit ${run.status}): ${String(run.stderr || run.stdout).trim().split('\n')[0]}` }; }
56
62
  if (!Array.isArray(results)) return { error: `${pkg} produced a report that is not a result list` };
57
63
  return { results };
58
64
  }
59
65
 
60
- const eslintFindings = (results, repoRoot) => results.flatMap((result) => (result.messages ?? []).map((message) => ({
66
+ const eslintFindings = (results, appRoot) => results.flatMap((result) => (result.messages ?? []).map((message) => ({
61
67
  engine: 'eslint', rule: message.ruleId ?? 'eslint-error', code: CODE_PREFIX.exec(message.message ?? '')?.[1] ?? null, severity: 'error',
62
- path: posix(path.relative(repoRoot, result.filePath)), line: message.line ?? null, column: message.column ?? null, message: message.message,
68
+ path: posix(path.relative(appRoot, result.filePath)), line: message.line ?? null, column: message.column ?? null, message: message.message,
63
69
  })));
64
70
 
65
- const stylelintFindings = (results, repoRoot) => results.flatMap((result) => (result.warnings ?? []).map((warning) => ({
71
+ const stylelintFindings = (results, appRoot) => results.flatMap((result) => (result.warnings ?? []).map((warning) => ({
66
72
  engine: 'stylelint', rule: warning.rule || 'stylelint-error', code: null, severity: 'error',
67
- path: posix(path.relative(repoRoot, result.source)), line: warning.line ?? null, column: warning.column ?? null, message: warning.text,
73
+ path: posix(path.relative(appRoot, result.source)), line: warning.line ?? null, column: warning.column ?? null, message: warning.text,
68
74
  })));
69
75
 
70
76
  const byLocation = (a, b) => `${a.path ?? ''}:${String(a.line ?? 0).padStart(7, '0')}:${a.rule}`.localeCompare(`${b.path ?? ''}:${String(b.line ?? 0).padStart(7, '0')}:${b.rule}`);
71
77
 
78
+ /** The files of `changed` (app-relative) below `side`/, relative to the side folder. */
79
+ const onSide = (changed, side) => changed.filter((file) => file.startsWith(`${side}/`)).map((file) => file.slice(side.length + 1));
80
+
72
81
  /**
73
- * Run the whole lint of a repository. `hfsCheck(repoRoot)` returns the `hfs check` result (`{ findings, tracked }`); the CLI injects it.
74
- * Returns `{ report, sonar, exit }`; `sonar` is the merged Generic Issue Import document.
82
+ * Run the whole lint of the app at `repoRoot`. `hfsCheck(repoRoot)` returns the `hfs check` result (`{ findings, tracked }`); the CLI
83
+ * injects it. Returns `{ report, sonar, exit }`; `sonar` is the merged Generic Issue Import document.
75
84
  */
76
85
  export async function lintRepository({ repoRoot, opts, hfsCheck, trackedFiles = () => [] }) {
77
86
  const changed = opts.changed === null ? null : new Set(opts.changed);
@@ -81,26 +90,48 @@ export async function lintRepository({ repoRoot, opts, hfsCheck, trackedFiles =
81
90
  const raw = { eslint: [], stylelint: [] };
82
91
  const engines = {};
83
92
 
84
- const sources = existing.filter((file) => /\.(?:[cm]?[jt]sx?)$/.test(file));
85
- if (changed === null || sources.length) {
86
- const linted = runLinter({ repoRoot, pkg: 'eslint', bin: 'eslint', args: ['--format', 'json', ...(opts.fix ? ['--fix'] : []), ...(changed ? sources : ['.'])] });
93
+ let app = null;
94
+ try { app = readRepoDeclaration(loadSlotManifest(), repoRoot); } catch (error) { errors.push(`hfs lint runs at the app root: ${String(error?.message ?? error)}`); }
95
+ if (app && !app.sides) { errors.push(`${repoRoot} is the ${app.side} side of an app; hfs lint runs at the app root, the folder of hfs.json`); app = null; }
96
+
97
+ // 1. ESLint, once per side, from the side folder with that side's config.
98
+ engines.eslint = { sides: {} };
99
+ for (const side of app ? SIDES : []) {
100
+ const sources = onSide(existing, side).filter((file) => /\.(?:[cm]?[jt]sx?)$/.test(file));
101
+ if (changed !== null && !sources.length) { engines.eslint.sides[side] = { files: 0, skipped: 'no changed source file' }; continue; }
102
+ const linted = runLinter({ cwd: path.join(repoRoot, side), pkg: 'eslint', bin: 'eslint', args: ['--format', 'json', ...(opts.fix ? ['--fix'] : []), ...(changed ? sources : ['.'])] });
87
103
  if (linted.error) errors.push(linted.error);
88
- else { raw.eslint = linted.results; findings.push(...eslintFindings(linted.results, repoRoot)); }
89
- engines.eslint = { files: changed ? sources.length : null };
90
- } else engines.eslint = { files: 0, skipped: 'no changed source file' };
104
+ else {
105
+ // The side canon names side-relative paths in its messages; the report names every path from the app root.
106
+ const appRelative = appRelativeMessages(side, path.join(repoRoot, side));
107
+ for (const result of linted.results) for (const message of result.messages ?? []) message.message = appRelative(message.message);
108
+ raw.eslint.push(...linted.results);
109
+ findings.push(...eslintFindings(linted.results, repoRoot));
110
+ }
111
+ engines.eslint.sides[side] = { files: linted.results?.length ?? 0 };
112
+ }
91
113
 
92
- if (opts.stylelint !== undefined) {
93
- const styles = existing.filter((file) => file.endsWith('.css'));
114
+ // 3. stylelint over the fe side's stylesheets.
115
+ if (app) {
116
+ const styles = onSide(existing, STYLE_SIDE).filter((file) => file.endsWith('.css'));
94
117
  if (changed === null || styles.length) {
95
- const linted = runLinter({ repoRoot, pkg: 'stylelint', bin: 'stylelint', args: [...(changed ? styles : [opts.stylelint]), '--formatter', 'json', ...(opts.fix ? ['--fix'] : [])] });
118
+ const linted = runLinter({ cwd: path.join(repoRoot, STYLE_SIDE), pkg: 'stylelint', bin: 'stylelint', args: [...(changed ? styles : [STYLE_GLOB]), '--formatter', 'json', ...(opts.fix ? ['--fix'] : [])] });
96
119
  if (linted.error) errors.push(linted.error);
97
- else { raw.stylelint = linted.results; findings.push(...stylelintFindings(linted.results, repoRoot)); }
120
+ else {
121
+ const appRelative = appRelativeMessages(STYLE_SIDE, path.join(repoRoot, STYLE_SIDE));
122
+ for (const result of linted.results) for (const warning of result.warnings ?? []) warning.text = appRelative(warning.text);
123
+ raw.stylelint = linted.results;
124
+ findings.push(...stylelintFindings(linted.results, repoRoot));
125
+ }
98
126
  }
99
- engines.stylelint = { files: changed ? styles.length : null };
127
+ engines.stylelint = { side: STYLE_SIDE, files: raw.stylelint.length };
100
128
  }
101
129
 
130
+ // 2. The app check: root and sides.
102
131
  let checked = { findings: [] };
103
- try { checked = await hfsCheck(repoRoot); } catch (error) { errors.push(`hfs check could not run: ${String(error?.message ?? error)}`); }
132
+ if (app) {
133
+ try { checked = await hfsCheck(repoRoot); } catch (error) { errors.push(`hfs check could not run: ${String(error?.message ?? error)}`); }
134
+ }
104
135
  const repoErrors = checked.findings.filter((finding) => finding.level === 'error');
105
136
  const kept = changed === null ? repoErrors : repoErrors.filter((finding) => !finding.path || changed.has(posix(finding.path)));
106
137
  for (const finding of kept) {
@@ -116,7 +147,7 @@ export async function lintRepository({ repoRoot, opts, hfsCheck, trackedFiles =
116
147
  const sonar = mergeReports([
117
148
  sonarReport(kept, { sourceRoots, tracked }),
118
149
  linterReport('eslint', raw.eslint, { root: repoRoot, sourceRoots, tracked }),
119
- ...(opts.stylelint !== undefined ? [linterReport('stylelint', raw.stylelint, { root: repoRoot, sourceRoots, tracked })] : []),
150
+ linterReport('stylelint', raw.stylelint, { root: repoRoot, sourceRoots, tracked }),
120
151
  ]);
121
152
  const report = { schema: LINT_SCHEMA, ok: findings.length === 0 && errors.length === 0, repoRoot, changed: changed ? [...changed].sort() : null, counts: { error: findings.length }, engines, errors, findings };
122
153
  return { report, sonar, exit: errors.length ? 2 : findings.length ? 1 : 0 };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@starci/hfs",
3
- "version": "3.0.0",
4
- "description": "The HFS command line of a StarCi product repository: hfs check, init, explain, sync and work-hygiene. Self-contained: it carries the runtime files it reads.",
3
+ "version": "4.0.1",
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",
7
7
  "private": false,
@@ -72,9 +72,9 @@ const leasePath=resourceKey=>String(resourceKey??'').startsWith(PATH_LEASE_PREFI
72
72
  * The spelling two lease paths are compared in. The same file must compare equal however a workflow
73
73
  * spelled it (nivo wf-nivo-fe-debt-mug06w7h inc-52a4a5ee5b12: `apps/app/src/messages/vi.json` bare for
74
74
  * the fe repository vs `nivo-fe/apps/app/src/messages` repository-prefixed never overlapped). `canonicalOf`
75
- * (scripts/kernel/lease-canon.mjs) resolves a path to its repository-qualified form
76
- * `repository:<role>/<path>` — for a held row through its holder job, so a lease taken before
77
- * canonical keys existed still conflicts; paths on Windows compare case-insensitively, as its file
75
+ * (scripts/kernel/lease-canon.mjs) resolves a path to its app-relative form in a bound app
76
+ * (be/<path>, fe/<path>) — for a held row through its holder job, so every spelling of one file
77
+ * compares as one key; paths on Windows compare case-insensitively, as its file
78
78
  * systems do.
79
79
  */
80
80
  export const leaseCompareForm=(leasePathValue,{canonicalOf=null,row=null,platform=process.platform}={})=>{
@@ -54,7 +54,7 @@ export const JOB_ARTIFACT_ROLES=Object.freeze(['check-output','check-stdout','ch
54
54
  'direction','prompt','render','redline','critique','capture','dom','screenshot','video','trace','uat-run','metrics','salvage','scan','other']);
55
55
 
56
56
  export const LEDGER_SCHEMA='starci/runtime@1';
57
- export const LEDGER_VERSION=4;
57
+ export const LEDGER_VERSION=5;
58
58
 
59
59
  const need=(ok,message,code)=>{if(!ok)throw Object.assign(Error(message),code?{code}:{});};
60
60
  const json=value=>value===undefined||value===null?null:JSON.stringify(value);
@@ -127,7 +127,7 @@ const INIT_SQL_FILE=new URL('./migrations/runtime/0001-init.sql',import.meta.url
127
127
  const INIT_SQL=fs.readFileSync(INIT_SQL_FILE,'utf8');
128
128
  const INIT_SQL_SHA=sha256(INIT_SQL);
129
129
  /** Forward migrations after 0001-init, in order; each bumps user_version to its `version` (migrateLedger). */
130
- const FORWARD_MIGRATIONS=Object.freeze([{version:3,name:'0003-usage-unavailable'},{version:4,name:'0004-attempt-why'}]);
130
+ const FORWARD_MIGRATIONS=Object.freeze([{version:3,name:'0003-usage-unavailable'},{version:4,name:'0004-attempt-why'},{version:5,name:'0005-ended-workflow-views'}]);
131
131
  export const LEDGER_BUSY_TIMEOUT_MS=15000;
132
132
  /**
133
133
  * Writer pragmas. wal_autocheckpoint=0 on EVERY connection except the one checkpointer (openLedger({checkpointer:true}),