@starci/hfs 4.0.4 → 4.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.0.5 - 2026-10-01
4
+
5
+ - Changed: workflow worktree runtime copies, the services coverage scope for Sonar and codecov.yml, sonar-gate, canon-pins.
6
+
7
+ ## Unreleased (alpha.4, bumped in C0's batch)
8
+
9
+ - Changed (contract change `sonar-services-coverage`): Sonar and Codecov judge the services' coverage. The managed `sonar-project.properties` adds `sonar.javascript.lcov.reportPaths=be/coverage/lcov.info` and `sonar.coverage.inclusions=be/src/**/*.service.ts`; a new managed `codecov.yml` (slot `app.quality-config`, now `{sonar-project.properties,codecov.yml}`) holds the same paths at 100 on the project and the patch and ignores `fe/**`; the managed CI workflow uploads the be lcov with `codecov/codecov-action@v5` and the `CODECOV_TOKEN` secret after the unit run. Both files are rendered from one scope, `coverageScope(presets)`: the installed jest preset's `COVERAGE_SOURCES` on the be side (`loadPresets` now returns `coverageSources`). An app re-renders with `hfs sync --write`.
10
+ - Changed: the bundled runtime copies of `knowledge/sonar-gate.yaml` (coverage 100 overall and on new code, per file; every hotspot reviewed), `knowledge/hfs/slots.yaml` (the quality-config slot) and `modules/kernel/failure-codes.yaml` (the R11 law and HFS_SONAR_CONFIG text).
11
+
3
12
  ## 4.0.4 - 2026-10-01
4
13
 
5
14
  - Changed: the bundled runtime copy of knowledge/hfs/canon-pins.yaml pins @starci/test-world 1.0.3. No rule or command changed.
package/README.md CHANGED
@@ -70,7 +70,7 @@ Its own checks (`scripts/lib/hfs-check.mjs`, `scripts/lib/hfs-rules/`; the rende
70
70
  | `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) |
71
71
  | `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/lib/hfs-rules/fe-no-tests.mjs`) |
72
72
  | `HFS_GITIGNORE_BLOCK_DRIFT` | error | the managed `.gitignore` block differs from its render (R04; `sync/managed.mjs`) |
73
- | `HFS_SONAR_CONFIG` | error | `sonar-project.properties` differs from its render: no `sonar.host.url`, the `sonar.exclusions` of the installed jest preset, no coverage import (R11; `sync/managed.mjs`) |
73
+ | `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`) |
74
74
  | `HFS_FORMAT` | error | a tracked file the repository's own prettier would change (R19; `sync/format.mjs`, not under `--fast`) |
75
75
  | `HFS_FORMAT_TOOL_MISSING` | refusal (exit 2) | prettier is not installed in the repository; the format check is never skipped |
76
76
 
@@ -114,12 +114,14 @@ The managed `sonar-project.properties` carries `sonar.externalIssuesReportPaths`
114
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
115
115
  clones with its own token rule), so the machine enforces it (R21) and its findings are imported like every other.
116
116
 
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,
118
- duplicated lines density, cognitive complexity through the S3776 rule). A SonarQube gate condition cannot filter by engine, so the condition counts every
117
+ The gate is `knowledge/sonar-gate.yaml`, the one declaration: the new-code conditions (coverage 100, duplication, blocker and critical issues, hotspots) and an `overall` part (coverage 100, 0 open issues on the whole code,
118
+ every hotspot reviewed, duplicated lines density, cognitive complexity through the S3776 rule). A SonarQube gate condition cannot filter by engine, so the condition counts every
119
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
120
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.
121
121
 
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`.
122
+ 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=be/src/**/*.service.ts` 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.
123
+
124
+ The upload needs the repository secret `CODECOV_TOKEN` (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`.
123
125
 
124
126
  ## Maintaining the bundle
125
127
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starci/hfs",
3
- "version": "4.0.4",
3
+ "version": "4.0.5",
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",
@@ -43,9 +43,9 @@ import { redactBytes, redactData, redactText } from '../scripts/lib/redact.mjs';
43
43
  const require = createRequire(import.meta.url);
44
44
  const ENGINE_DIR = path.dirname(fileURLToPath(import.meta.url));
45
45
  export const MACHINE_SCHEMA = 'starci/machine@1';
46
- export const MACHINE_VERSION = 2;
46
+ export const MACHINE_VERSION = 3;
47
47
  /** Forward migrations after 0001-init, in order; each bumps user_version to its `version` (migrateMachine). */
48
- export const MACHINE_MIGRATIONS = Object.freeze([{ version: 2, name: '0002-worktrees-no-workflow-kind' }]);
48
+ export const MACHINE_MIGRATIONS = Object.freeze([{ version: 2, name: '0002-worktrees-no-workflow-kind' }, { version: 3, name: '0003-worktrees-workflow-orca' }]);
49
49
  export const MACHINE_BUSY_TIMEOUT_MS = 15000;
50
50
  /** Test seam: STARCI_MACHINE_BUSY_TIMEOUT_MS (a positive integer) replaces the writer's busy_timeout; unset in production. */
51
51
  export const busyTimeoutOf = (env = process.env) => { const n = Number(env?.STARCI_MACHINE_BUSY_TIMEOUT_MS); return Number.isInteger(n) && n > 0 ? n : MACHINE_BUSY_TIMEOUT_MS; };
@@ -0,0 +1,21 @@
1
+ -- 0003-worktrees-workflow-orca (starci/machine@1, user_version 2 -> 3). Applied by engine/machine-db.mjs migrateMachine on
2
+ -- the first writer open of an older machine.sqlite (after an integrity_check and a VACUUM INTO backup, with foreign_key_check
3
+ -- + quick_check before COMMIT); a fresh machine.sqlite runs 0001 then the forward files.
4
+ --
5
+ -- One worktree per Kernel workflow replaces the per-op worktree (owner decision WFWT, final). Orca creates and owns it (the
6
+ -- Kernel launch), so the registry keys it by Orca's worktree id (orca_id, unique among the rows that have one), records
7
+ -- the workflow's last checkpoint (checkpoint_sha) and the moment its finish asked for its release (release_pending_at: the
8
+ -- host-side GC removes it once the Kernel's and the ops' terminals are released; never from inside itself). The draw
9
+ -- critic's placement is an Orca worktree too (kind critic). worktrees.kind no longer accepts 'op': a row of that kind
10
+ -- describes the removed mechanism and is deleted (a directory it named, if any is left, is an unregistered worktree the
11
+ -- worktree GC and the footprint scan report); then the table's CHECK is rewritten in place (writable_schema: the views over worktrees stay as they are).
12
+ DELETE FROM worktrees WHERE kind='op';
13
+ ALTER TABLE worktrees ADD COLUMN orca_id TEXT;
14
+ ALTER TABLE worktrees ADD COLUMN checkpoint_sha TEXT;
15
+ ALTER TABLE worktrees ADD COLUMN release_pending_at INTEGER;
16
+ CREATE UNIQUE INDEX IF NOT EXISTS ux_worktrees_orca_id ON worktrees(orca_id) WHERE orca_id IS NOT NULL;
17
+ CREATE INDEX IF NOT EXISTS ix_worktrees_workflow ON worktrees(workflow_id,kind,removed_at);
18
+ PRAGMA writable_schema=ON;
19
+ UPDATE sqlite_master
20
+ SET sql=replace(sql, 'CHECK(kind IN (''op'',''land-scratch'',', 'CHECK(kind IN (''workflow'',''critic'',''land-scratch'',')
21
+ WHERE type='table' AND name='worktrees';
@@ -16,6 +16,13 @@ purpose: >-
16
16
  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
+ '@starci/heroicons':
20
+ version: 0.3.1
21
+ group: starci
22
+ install: registry
23
+ side: fe
24
+ source: packages/heroicons/package.json
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.'
19
26
  '@starci/grammar':
20
27
  version: 0.8.1
21
28
  group: starci
@@ -24,14 +31,14 @@ pins:
24
31
  source: packages/grammar/package.json
25
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).
26
33
  '@starci/eslint-canon-be':
27
- version: 3.0.4
34
+ version: 3.0.5
28
35
  group: starci
29
36
  install: registry
30
37
  side: be
31
38
  source: packages/eslint/be/package.json
32
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.'
33
40
  '@starci/eslint-canon-fe':
34
- version: 8.0.4
41
+ version: 8.0.5
35
42
  group: starci
36
43
  install: registry
37
44
  side: fe
@@ -57,12 +64,12 @@ pins:
57
64
  side: both
58
65
  source: packages/prettier-config/package.json
59
66
  '@starci/jest-preset':
60
- version: 2.2.1
67
+ version: 2.2.2
61
68
  group: starci
62
69
  install: registry
63
70
  side: be
64
71
  source: packages/jest-preset/package.json
65
- why: '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.'
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.'
66
73
  '@starci/test-world':
67
74
  version: 1.0.3
68
75
  group: starci
@@ -71,7 +78,7 @@ pins:
71
78
  source: packages/test-world/package.json
72
79
  why: '1.0.3: the globalSetup registers the path aliases as TypeScript resolves them (the extends chain, paths from the config that declares them, the effective baseUrl), so a tests tsconfig that only extends the side config loads the declaration. 1.0.2: every path a declaration names (`stack`, seeds, the realm, Dockerfiles) resolves from the app root (the directory of hfs.json), where .starcistacks lives, never from the be side. The shared e2e library of every back end (R47, R48): the warm stack behind toxiproxy, the network-edge fakes, the Nest boot, the typed useTestWorld handle, useSandbox for contract specs, the outage lock and the starci-test-stack bin behind the managed test:stack script; a devDependency of every back end. 1.0.1: world.keycloak.events/sessions (the user events of the realm and live sessions through the admin API) and world.infra.postgresql.connection(name) (the database of one connection down while the others serve, under the outage lock).'
73
80
  '@starci/hfs':
74
- version: 4.0.4
81
+ version: 4.0.5
75
82
  group: starci
76
83
  install: registry
77
84
  side: both
@@ -246,7 +246,7 @@ slots:
246
246
  rules: [HFS_TOOL_CONFIG_LOCAL]
247
247
  - id: app.quality-config
248
248
  profiles: [app]
249
- path: sonar-project.properties
249
+ path: "{sonar-project.properties,codecov.yml}"
250
250
  presence: required
251
251
  tracked: tracked
252
252
  tier: none
@@ -373,7 +373,7 @@ slots:
373
373
  tracked: external
374
374
  tier: none
375
375
  tests: none
376
- goesTo: "D:/starci-lanes/<project>/<lane>/ (outside every repository) for a lane; a runtime op worktree (.starciwork/worktrees/<op>) is git-excluded, never tracked, and removed when its op settles"
376
+ goesTo: "D:/starci-lanes/<project>/<lane>/ (outside every repository) for a lane; a Kernel workflow's worktree is created and owned by Orca outside the app checkout, never tracked, and removed by the runtime's host-side controller once the workflow's finish marked it release-pending"
377
377
  - id: app.plaintext-env
378
378
  profiles: [app]
379
379
  path: "{.env,.env.*,.secrets/,**/*.pem,**/*.key}"
@@ -387,12 +387,12 @@ slots:
387
387
  # ----- side root (both sides): what the old standalone repository root held, less the app-root files ----------
388
388
  - id: repo.side-root-forbidden
389
389
  profiles: [be, fe]
390
- path: "{package.json,package-lock.json,hfs.json,README.md,.gitignore,.gitattributes,.husky/,.github/,.starciwork/,.starcistacks/,.sops.yaml,sonar-project.properties,.prettierrc,.prettierignore,scripts/}"
390
+ path: "{package.json,package-lock.json,hfs.json,README.md,.gitignore,.gitattributes,.husky/,.github/,.starciwork/,.starcistacks/,.sops.yaml,sonar-project.properties,codecov.yml,.prettierrc,.prettierignore,scripts/}"
391
391
  presence: forbidden
392
392
  tracked: external
393
393
  tier: none
394
394
  tests: none
395
- goesTo: "the app root: one package.json, lockfile, hfs.json, README, git and CI files, hooks, formatter, Sonar configuration, scripts/, .starciwork, .starcistacks and .sops.yaml per app"
395
+ goesTo: "the app root: one package.json, lockfile, hfs.json, README, git and CI files, hooks, formatter, Sonar and Codecov configuration, scripts/, .starciwork, .starcistacks and .sops.yaml per app"
396
396
  - id: be.tool-config
397
397
  profiles: [be]
398
398
  # Managed files (hfs sync renders them, hfs check compares them): the whole tool configuration of a back end. Each is
@@ -8,16 +8,24 @@ purpose: |
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.
11
- The gate has two parts: `newCode` (the recent work: duplication, blocker/critical issues, hotspots) and
12
- `overall` (the whole code: no imported HFS, ESLint or stylelint finding, no duplicated-lines excess, no cognitive-
13
- complexity issue). The one Sonar mechanism of the HFS canon is packages/hfs/README.md section "Sonar".
11
+ The gate has two parts: `newCode` (the recent work: coverage, duplication, blocker/critical issues, hotspots) and
12
+ `overall` (the whole code: coverage, no imported HFS, ESLint or stylelint finding, no bug, code smell or
13
+ vulnerability, every hotspot reviewed, no duplicated-lines excess, no cognitive-complexity issue). The one Sonar
14
+ mechanism of the HFS canon is packages/hfs/README.md section "Sonar".
14
15
  gate:
15
16
  name: starci-new-code
16
17
  # A project with no new-code baseline judges the whole project as new code (nivo inc-f92febebbb64); a fixed
17
18
  # window keeps "new code" the recent work on main, so the server gate is passable on a healthy main.
18
19
  newCodePeriod: {type: NUMBER_OF_DAYS, value: 30}
19
20
  newCode:
20
- # Sonar holds NO coverage condition: unit coverage is the runner's job (jest per-file 100 on `*.service.ts`), never an lcov import.
21
+ # Coverage of the services only. The be unit run writes coverage/lcov.info (@starci/jest-preset coverageReporters lcov) and the
22
+ # managed sonar-project.properties imports it (sonar.javascript.lcov.reportPaths=be/coverage/lcov.info) with
23
+ # sonar.coverage.inclusions=be/src/**/*.service.ts, the one scope rendered by hfs sync from the preset's COVERAGE_SOURCES (codecov.yml
24
+ # is rendered from the same constant). A handler, resolver, controller, module, config file or test is not a coverage target and
25
+ # fe/ is outside coverage entirely, so coverage on these conditions is the services' coverage alone.
26
+ coverage:
27
+ metric: new_coverage
28
+ minPercent: 100
21
29
  # SonarQube's ignoreSmallChanges: fewer changed lines than this are not held to duplication.
22
30
  ignoreBelowChangedLines: 20
23
31
  duplication:
@@ -49,6 +57,19 @@ overall:
49
57
  metric: violations
50
58
  max: 0
51
59
  engines: [starci-hfs, eslint, stylelint]
60
+ # The dashboard counts (`sonar-local dashboard`): each type of open issue, each held at `max`.
61
+ types: {bugs: BUG, code_smells: CODE_SMELL, vulnerabilities: VULNERABILITY}
62
+ # Every security hotspot of the whole code is reviewed (the dashboard's "hotspots reviewed").
63
+ hotspots:
64
+ metric: security_hotspots_reviewed
65
+ minReviewedPercent: 100
66
+ # The services' coverage (see newCode.coverage), overall and per file: with the inclusions in place each `*.service.ts` is its
67
+ # own measure, so one service below 100 fails the slice verdict (sonar-local evaluateSlice) and the dashboard verdict
68
+ # (sonar-local dashboard) although the project average may round to 100. A file outside the inclusions is never read.
69
+ coverage:
70
+ metric: coverage
71
+ minPercent: 100
72
+ perFile: true
52
73
  # HFS_DUPLICATE_CODE (R21): duplicated lines density of the whole project, the Sonar enforcer of the machine's clone check.
53
74
  # SonarJS runs its own token-based detection (sonar.cpd.* minimums are not read for TypeScript), so ruleParams.<profile>.duplicateBlock
54
75
  # is enforced by the machine and imported as starci-hfs issues, and this density is the Sonar-side backstop.
@@ -70,8 +91,10 @@ enforcedOps: [backend.implement, interface.implement, code.refactor]
70
91
  reasoning: |
71
92
  Chosen against the four projects the local server holds on 2026-09-29 (whole-project duplicated lines
72
93
  1.6 / 2.1 / 1.7 / 0.0 percent), so new code that meets them is at or below what main already carries.
73
- no coverage condition: the unit project fails below its per-file 100 threshold on `*.service.ts`, so Sonar never
74
- imports an lcov report and never judges coverage.
94
+ coverage 100 on services, overall and new code (owner 2026-10-01): the unit project already fails below its per-file
95
+ 100 threshold on `*.service.ts`; Sonar imports the same run's lcov with the same scope, so the dashboard, the slice
96
+ verdict and the server gate show the number the runner enforces, file by file. Only services count: business logic
97
+ lives there, the thin layers are covered through them, and the front end has no unit tests.
75
98
  duplication 3: the Sonar way value; the worst project today is 2.1, so 3 rejects a real copy-paste block and
76
99
  passes ordinary work.
77
100
  blocker/critical 0: the two severities that are defects, not style. main carries old debt of both kinds
@@ -1482,11 +1482,11 @@ HFS_SLOT_UNDECLARED:
1482
1482
  kind: check-finding
1483
1483
 
1484
1484
  HFS_SONAR_CONFIG:
1485
- title: "Sonar config is generated, with no host URL and no coverage import"
1485
+ title: "Sonar config is generated, with no host URL, importing the be lcov with the services as the only coverage scope"
1486
1486
  title_vi: "Cấu hình Sonar lệch"
1487
- meaning_vi: "Cấu hình Sonar lệch: `<detail>`. Sonar phải đọc đúng cấu hình sinh ra, không nhập báo cáo độ phủ."
1487
+ 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."
1488
1488
  causes_vi:
1489
- - "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, không nhập lcov hay `sonar.coverage.*`, 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."
1489
+ - "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.inclusions=be/src/**/*.service.ts` 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."
1490
1490
  - "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."
1491
1491
  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."
1492
1492
  owner: op-retry
@@ -212,6 +212,21 @@ export function mainCheckoutDamage(before, after) {
212
212
  return out;
213
213
  }
214
214
 
215
+ /**
216
+ * The link step of every worktree removal (git's here, Orca's in scripts/lib/worktrees.mjs removeOrcaWorktree): every link
217
+ * under `target` found WITHOUT following one (linksUnder), each removed as a link (removeLink: `cmd /c rmdir <link>`, never
218
+ * /s), outermost first, then a re-scan that must find ZERO. {ok, links, errors: [{path, code, message}]}; ok false: a link
219
+ * is stuck and the caller removes nothing.
220
+ */
221
+ export function removeLinksUnder(target) {
222
+ const out = { ok: false, links: 0, errors: [] };
223
+ if (!fs.existsSync(target)) { out.ok = true; return out; }
224
+ for (const link of linksUnder(target)) { if (removeLink(link)) out.links += 1; else out.errors.push({ path: link, code: 'LINK_STUCK', message: 'a link could not be removed' }); }
225
+ for (const l of linksUnder(target)) if (!out.errors.some((e) => e.path === l)) out.errors.push({ path: l, code: 'LINK_STUCK', message: 'a link is still there after removal' });
226
+ out.ok = out.errors.length === 0;
227
+ return out;
228
+ }
229
+
215
230
  /**
216
231
  * Remove a git worktree (the one algorithm; the 490-file .claude incident and nivo-fe inc-c8fbf76aa499):
217
232
  * 1. enumerate every link in it WITHOUT following one (linksUnder);
@@ -236,13 +251,9 @@ export function safeRemoveWorktree(worktree, { repo, git = null, retries = 5 } =
236
251
  if (refused) { out.errors.push({ path: target, code: 'REFUSED', message: `refusing to remove ${refused}` }); return out; }
237
252
  const before = mainRoot ? mainCheckoutGuard(mainRoot, { git: run }) : null;
238
253
  if (fs.existsSync(target)) {
239
- for (const link of linksUnder(target)) { if (removeLink(link)) out.links += 1; else out.errors.push({ path: link, code: 'LINK_STUCK', message: 'a link could not be removed' }); }
240
- const left = linksUnder(target);
241
- if (left.length) {
242
- for (const l of left) if (!out.errors.some((e) => e.path === l)) out.errors.push({ path: l, code: 'LINK_STUCK', message: 'a link is still there after removal' });
243
- out.reason = 'link-stuck';
244
- return out;
245
- }
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; }
246
257
  out.removed.links = out.links;
247
258
  const registered = trees.some((t) => same(t, target));
248
259
  if (registered && repo) run(['worktree', 'remove', '--force', target], { cwd: repo });
package/sync/index.mjs CHANGED
@@ -8,7 +8,8 @@
8
8
  // side (be, fe) for a side slot, whose files land under that side's folder. Two targets are not whole files of a managedBy slot
9
9
  // and are listed in this module: the marked block of the root .gitignore, and .starciwork/.gitignore, which lives inside the
10
10
  // .starciwork directory slot. Every file is rendered with the app's hfs.json (both sides and their apps) and, for the Sonar
11
- // exclusions, the jest preset the app installs for its be side. `--check` compares the sha256 of the rendered content with the
11
+ // exclusions and the one coverage scope (sonar.coverage.inclusions and codecov.yml alike), the jest preset the app installs for
12
+ // its be side. `--check` compares the sha256 of the rendered content with the
12
13
  // file on disk and fails on any drift; `--write` rewrites the drifted files. `.gitignore` is the one shared file: only the marked
13
14
  // block is managed and the app's own lines around it are left alone. The root package.json is managed by its `scripts` block
14
15
  // only (mode scripts, compared as parsed JSON): the rest of the file (dependencies, npm workspaces of fe/packages/*) is the app's.
@@ -98,7 +99,10 @@ export function render(text, vars, readTemplate = readBundled) {
98
99
  });
99
100
  }
100
101
 
101
- /** The Sonar exclusions from the jest preset the app installs for its be side: { sonarExclusions }. */
102
+ /**
103
+ * What the app's sync reads from the jest preset it installs for its be side: the Sonar exclusions and the coverage sources
104
+ * (`COVERAGE_SOURCES`, the globs jest collects coverage from): { sonarExclusions, coverageSources }.
105
+ */
102
106
  export async function loadPresets(root) {
103
107
  const name = '@starci/jest-preset';
104
108
  const require = createRequire(path.join(root, 'package.json'));
@@ -109,7 +113,21 @@ export async function loadPresets(root) {
109
113
  throw new SyncError('HFS_SYNC_PRESET_MISSING', `${name} is not installed under ${root}; set it to the exact version in knowledge/hfs/canon-pins.yaml and reinstall`);
110
114
  }
111
115
  const preset = require(resolved);
112
- return { sonarExclusions: preset.sonarExclusions() };
116
+ return { sonarExclusions: preset.sonarExclusions(), coverageSources: [...preset.COVERAGE_SOURCES] };
117
+ }
118
+
119
+ /** Where the be unit run writes the lcov report (jest `coverageDirectory` coverage under be/, reporter lcov), from the app root. */
120
+ export const LCOV_REPORT = 'be/coverage/lcov.info';
121
+
122
+ /**
123
+ * THE coverage scope of an app, from the app root: the preset's coverage sources on the be side (`be/src/**` + `/*.service.ts`).
124
+ * It is the one source of sonar.coverage.inclusions and of the codecov.yml status paths, so the two can never drift; fe/ is
125
+ * outside it (a front end has no tests).
126
+ */
127
+ export function coverageScope(presets) {
128
+ const sources = presets?.coverageSources;
129
+ if (!Array.isArray(sources) || !sources.length) throw new SyncError('HFS_SYNC_PRESET_MISSING', '@starci/jest-preset gives no COVERAGE_SOURCES: the coverage scope cannot be rendered');
130
+ return sources.map(glob => `be/${glob}`);
113
131
  }
114
132
 
115
133
  /**
@@ -156,6 +174,9 @@ export function variables(app, scope, presets, sonarKey) {
156
174
  sonarKey: sonarKey ?? app.project,
157
175
  sonarExclusions: [presets?.sonarExclusions, '**/.next/**', '**/node_modules/**', '**/src/messages/**'].filter(Boolean).join(','),
158
176
  sonarSources: ['be/apps', 'be/src', 'fe/apps', ...(packages ? ['fe/packages'] : [])].join(','),
177
+ lcovReport: LCOV_REPORT,
178
+ coverageInclusions: scope === APP_SCOPE ? coverageScope(presets).join(',') : '',
179
+ codecovPaths: scope === APP_SCOPE ? coverageScope(presets).map(glob => ` - ${JSON.stringify(glob)}`).join('\n') : '',
159
180
  tsconfigPaths: ['be/tsconfig.json', ...feTsconfigs].join(','),
160
181
  styleGlob: STYLE_GLOB,
161
182
  };
package/sync/managed.mjs CHANGED
@@ -20,7 +20,7 @@
20
20
  // `prettier`, `lint-staged`, `jest`). Forbidden tool-config files (.eslintrc*, .eslintignore, a second
21
21
  // eslint.config.*) are refused by hfs-check through the slot manifest under the same code.
22
22
  // HFS_SONAR_CONFIG (R11) sonar-project.properties differs from its render (the render names no host URL, sources and tests that
23
- // do not overlap, the `sonar.exclusions` of the jest preset (a back end), no coverage import, the ESLint report and the
23
+ // do not overlap, the `sonar.exclusions` of the jest preset (a back end), the be lcov with the services as the coverage scope, the ESLint report and the
24
24
  // HFS import files Sonar reads), or the stack declaration names a quality gate other than the one
25
25
  // gate of knowledge/sonar-gate.yaml (bundled in the runtime copy). One edit is one finding.
26
26
  // HFS_TS_STRICT (R22) the root tsconfig.json drift (either profile), named by flag (ts-strict.mjs) instead of by hash.
@@ -14,6 +14,7 @@ jobs:
14
14
  runs-on: ubuntu-latest
15
15
  env:
16
16
  SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
17
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
17
18
  steps:
18
19
  - uses: actions/checkout@v4
19
20
  - uses: actions/setup-node@v4
@@ -29,6 +30,14 @@ jobs:
29
30
  run: npm run typecheck
30
31
  - name: unit
31
32
  run: npm test -- --ci
33
+ - name: coverage upload
34
+ uses: codecov/codecov-action@v5
35
+ if: ${{ !cancelled() && env.CODECOV_TOKEN != '' }}
36
+ with:
37
+ token: ${{ env.CODECOV_TOKEN }}
38
+ files: {{lcovReport}}
39
+ disable_search: true
40
+ fail_ci_if_error: true
32
41
  - name: build be
33
42
  run: npm run build:be
34
43
  - name: build fe
@@ -0,0 +1,22 @@
1
+ {{header}}
2
+ # The be unit run's lcov ({{lcovReport}}), uploaded by the managed CI workflow. Coverage is the services' alone: the status
3
+ # paths are the one coverage scope hfs sync renders into sonar.coverage.inclusions too, held at 100 on the project and on
4
+ # the patch. fe/ is outside coverage (a front end has no tests).
5
+ codecov:
6
+ require_ci_to_pass: true
7
+ coverage:
8
+ status:
9
+ project:
10
+ default:
11
+ target: 100%
12
+ threshold: 0%
13
+ paths:
14
+ {{codecovPaths}}
15
+ patch:
16
+ default:
17
+ target: 100%
18
+ threshold: 0%
19
+ paths:
20
+ {{codecovPaths}}
21
+ ignore:
22
+ - "fe/**"
@@ -7,4 +7,6 @@ sonar.exclusions={{sonarExclusions}}
7
7
  sonar.test.inclusions=**/*.spec.ts
8
8
  sonar.typescript.tsconfigPaths={{tsconfigPaths}}
9
9
  sonar.externalIssuesReportPaths=reports/lint.sonar.json
10
+ sonar.javascript.lcov.reportPaths={{lcovReport}}
11
+ sonar.coverage.inclusions={{coverageInclusions}}
10
12
  sonar.nodejs.maxspace=8192