@starci/hfs 2.0.1 → 3.0.0
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 +12 -0
- package/README.md +12 -16
- package/bin/hfs.mjs +55 -48
- package/lint/run.mjs +130 -0
- package/package.json +2 -1
- package/report/sonar.mjs +14 -29
- package/runtime/knowledge/hfs/canon-pins.yaml +7 -7
- package/runtime/knowledge/sonar-gate.yaml +8 -7
- package/runtime/modules/kernel/failure-codes.yaml +8 -20
- package/runtime/scripts/checks/architecture/hfs-graph.mjs +1 -1
- package/runtime/scripts/checks/architecture/hfs.mjs +1 -1
- package/runtime/scripts/checks/architecture/index.mjs +18 -13
- package/runtime/scripts/checks/architecture/owners.mjs +12 -6
- package/runtime/scripts/checks/architecture/surface.mjs +91 -0
- package/runtime/scripts/checks/architecture/typescript.mjs +82 -9
- package/runtime/scripts/checks/architecture.mjs +2 -1
- package/runtime/scripts/checks/common.mjs +8 -1
- package/runtime/scripts/lib/hfs-check.mjs +11 -75
- package/runtime/scripts/lib/hfs-path-findings.mjs +84 -0
- package/runtime/scripts/lib/hfs-rules/pipeline.mjs +12 -13
- package/runtime/scripts/lib/hfs-rules/spec-placement.mjs +1 -3
- package/templates/be/ci-workflows/github/workflows/ci.yml +1 -9
- package/templates/be/hooks/husky/pre-push +2 -3
- package/templates/be/package-scripts/package.json +3 -6
- package/templates/be/quality-config/sonar-project.properties +1 -1
- package/templates/fe/ci-workflows/github/workflows/ci.yml +1 -15
- package/templates/fe/hooks/husky/pre-push +2 -3
- package/templates/fe/package-scripts/package.json +3 -7
- package/templates/fe/quality-config/sonar-project.properties +1 -1
- package/runtime/scripts/checks/architecture/size-growth.mjs +0 -73
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.0.0 - 2026-10-01
|
|
4
|
+
|
|
5
|
+
- 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`.
|
|
6
|
+
- Changed: 48 TS-attached architecture checks are served to eslint through one cached project graph (the `project-graph` law of both canons); `hfs check` keeps only findings with no TS file to sit on. The machine `size-growth` is gone (eslint `file-size-growth` holds the law).
|
|
7
|
+
- Fixed: workspace package imports resolve to their source (package `exports`/`types` mapped from dist to src through the package tsconfig), so `hfs check` needs no build and consumed exports are not reported unused; `ARCH_OWNER_EXPORT_BYPASS` accepts every entry the package's `exports` maps.
|
|
8
|
+
|
|
9
|
+
- Changed: Back-end apps have no composition spec. App slots admit only their production composition files, and R102 rejects a spec under `apps/<app>/src`. The e2e world proves that each app boots.
|
|
10
|
+
|
|
11
|
+
## 2.0.2 - 2026-10-01
|
|
12
|
+
|
|
13
|
+
- Fixed: `hfs` printed nothing and checked nothing when started through a node_modules junction or symlink (every lane worktree, some npx shims): the entry-point test compared `argv[1]` with the resolved module path. It now compares real paths (`isMain` of `scripts/checks/common.mjs`, shared by the bin and the architecture entry). Spec: `tests/hfs-cli-worktree.spec.mjs` runs `hfs check` inside a `git worktree add` checkout with a junctioned node_modules and requires a report or a loud refusal.
|
|
14
|
+
|
|
3
15
|
## 2.0.1 - 2026-10-01
|
|
4
16
|
|
|
5
17
|
- Fixed: `hfs sync` validates the whole hfs.json declaration against the manifest before rendering (a pre-2.0 `connections` list of names is refused), so a pin bump can never leave eslint unable to start.
|
package/README.md
CHANGED
|
@@ -6,8 +6,8 @@ catalog slice, the loader and the architecture machine with every file it import
|
|
|
6
6
|
checkout. The machine loads `typescript` from the repository it checks (never its own copy), so run `npm ci` first.
|
|
7
7
|
|
|
8
8
|
```sh
|
|
9
|
-
npx hfs
|
|
10
|
-
npx hfs
|
|
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
|
|
10
|
+
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
11
|
npx hfs init [--repo <dir>] [--stdout] # write a starter hfs.json (never overwrites); --stdout only prints
|
|
12
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
13
|
npx hfs explain <path> [--repo <dir>] [--json]
|
|
@@ -53,7 +53,7 @@ Its own checks (`scripts/lib/hfs-check.mjs`, `scripts/lib/hfs-rules/`; the rende
|
|
|
53
53
|
| `HFS_UNTRACKED_ROOT_ENTRY` | error | an entry git neither tracks nor ignores (`git ls-files -o --exclude-standard`), outside an `ignored` slot |
|
|
54
54
|
| `HFS_PLAINTEXT_SECRET` | error | a tracked plaintext secret: an env, key or credentials file, a value the push scan refuses (never printed), or an `.enc` that is no sops envelope (R06) |
|
|
55
55
|
| `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
|
-
| `HFS_CI_MISSING_CANON` | error | `ci.yml` without a `run
|
|
56
|
+
| `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
57
|
| `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
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) |
|
|
59
59
|
| `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`) |
|
|
@@ -75,11 +75,11 @@ The managed files are judged by `sync/managed.mjs` and `sync/ts-strict.mjs` (the
|
|
|
75
75
|
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
76
|
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
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
|
|
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
79
|
(`fe.tool-config-repo`): it carries the repository's task graph, which no preset can render.
|
|
80
80
|
|
|
81
81
|
The architecture machine (`scripts/checks/architecture.mjs` of the runtime, the same code bundled here): tiers and import
|
|
82
|
-
direction, owner public API, cycles, module registration and composition, clones, dead exports, required files,
|
|
82
|
+
direction, owner public API, cycles, module registration and composition, clones, dead exports, required files,
|
|
83
83
|
the source-shape, contract-form and front-end rules, and the repository-tree rules. Each violation and each error is one
|
|
84
84
|
finding under the machine's own rule id (`BE_TIER_DIRECTION`, `HFS_UNUSED_EXPORT`, `ARCH_OWNER_EXPORT_BYPASS`, ...). A
|
|
85
85
|
repository without `typescript` installed fails with `ARCH_TYPESCRIPT_MISSING`; the check never passes because it could not run.
|
|
@@ -88,7 +88,7 @@ repository without `typescript` installed fails with `ARCH_TYPESCRIPT_MISSING`;
|
|
|
88
88
|
`origin/main` (else `main`; `--base <ref>` names another ref): the slot and pin checks on the changed paths, the machine on the owners
|
|
89
89
|
of the changed source files without clones and dead exports, and no file-system tree checks. The required-file and minimum-instance
|
|
90
90
|
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
|
-
pass.
|
|
91
|
+
pass.
|
|
92
92
|
|
|
93
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
94
|
except `hfs init`, which writes `hfs.json` only when none exists.
|
|
@@ -98,22 +98,18 @@ except `hfs init`, which writes `hfs.json` only when none exists.
|
|
|
98
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
99
|
fails while any is open. Nothing is configured per repository; the pieces are managed files (`hfs sync`) and this package.
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
|---|---|---|
|
|
103
|
-
| `hfs check` (repository, managed-file and architecture-machine findings) | `reports/hfs.sonar.json`, from `hfs check --sonar reports/hfs.sonar.json` | `sonar.externalIssuesReportPaths`, engine `starci-hfs`, rule id = the finding code |
|
|
104
|
-
| ESLint (the BE and FE canon plugins alike) | `reports/eslint.json` (`npm run lint:report`) then `reports/eslint.sonar.json`, from `hfs report eslint reports/eslint.json reports/eslint.sonar.json` | `sonar.externalIssuesReportPaths`, engine `eslint`, rule id = the ESLint rule |
|
|
105
|
-
| stylelint (front end) | `reports/stylelint.json` (`npm run lint:report:css`) then `reports/stylelint.sonar.json`, from `hfs report stylelint ...` | `sonar.externalIssuesReportPaths`, engine `stylelint`, rule id = the stylelint rule |
|
|
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).
|
|
106
102
|
|
|
107
|
-
`--sonar` writes the
|
|
108
|
-
failing
|
|
103
|
+
`--sonar` writes the findings as a Generic Issue Import document (SonarQube 10.3+ format: `{ rules, issues }`) before the verdict, so a
|
|
104
|
+
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;
|
|
109
105
|
impacts are HIGH (maintainability). `info` findings (the soft-size backlog) are report-only and not imported. The output is sorted, so two runs
|
|
110
106
|
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
|
|
111
107
|
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.
|
|
112
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.
|
|
113
109
|
|
|
114
|
-
The managed `sonar-project.properties` carries `sonar.externalIssuesReportPaths` (`reports/
|
|
115
|
-
`SONAR_HOST_URL`); the managed CI workflow produces the
|
|
116
|
-
reaches Sonar while the job stays failed. There is no `continue-on-error`. Both profiles run `npm run
|
|
110
|
+
The managed `sonar-project.properties` carries `sonar.externalIssuesReportPaths` (`reports/lint.sonar.json` only) and no `sonar.host.url` (the host is
|
|
111
|
+
`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
|
|
117
113
|
clones with its own token rule), so the machine enforces it (R21) and its findings are imported like every other.
|
|
118
114
|
|
|
119
115
|
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,
|
package/bin/hfs.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// hfs - the HFS command line of a StarCi product repository.
|
|
3
|
-
// hfs check [--repo <dir>] [--json] [--fast] [--base <ref>]
|
|
3
|
+
// hfs check [--repo <dir>] [--json] [--fast] [--base <ref>]
|
|
4
4
|
// every tracked path has a slot; required files exist; nothing forbidden or
|
|
5
5
|
// tracked-that-must-be-ignored; pins match; every managed file equals its render
|
|
6
6
|
// (sync/managed.mjs, the .gitignore block and sonar-project.properties included); no empty or ghost
|
|
@@ -11,15 +11,14 @@
|
|
|
11
11
|
// architecture machine (tiers, owners, clones, dead exports, module registration, the
|
|
12
12
|
// front-end and back-end source rules), each violation a finding with its why.
|
|
13
13
|
// --fast: only what changed since the merge-base with origin/main (else main; --base
|
|
14
|
-
// overrides it
|
|
14
|
+
// overrides it); the machine
|
|
15
15
|
// runs on those owners without clones and dead exports. No merge-base is a refusal
|
|
16
16
|
// (exit 2), never a silent full pass. Exit 1 on any error-level finding.
|
|
17
|
-
// --sonar: also write the error findings as a Sonar Generic Issue Import file (report/sonar.mjs),
|
|
18
|
-
// before the verdict, so a failing check still leaves the report Sonar imports.
|
|
19
|
-
// hfs report <eslint|stylelint> <in> <out> [--repo <dir>] convert a linter's json output into a Sonar Generic Issue Import file
|
|
20
|
-
// (one converter for both linters; a finding on a file Sonar does not index is filed on the first
|
|
21
|
-
// source file of sonar.sources, the real path in its message).
|
|
22
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).
|
|
23
22
|
// hfs explain <path> [--repo <dir>] [--json] which slot owns the path, its tier, allowed imports, required tests.
|
|
24
23
|
// hfs emit-contracts [--repo <dir>] write contracts/<app>/schema.graphql of every api app that serves GraphQL (emit/contracts.mjs):
|
|
25
24
|
// printSchema(lexicographicSortSchema) of the resolvers the app root composes; no env, no database, no network.
|
|
@@ -35,6 +34,7 @@
|
|
|
35
34
|
import fs from 'node:fs';
|
|
36
35
|
import path from 'node:path';
|
|
37
36
|
import { fileURLToPath } from 'node:url';
|
|
37
|
+
import { isMain } from '../runtime/scripts/checks/common.mjs';
|
|
38
38
|
import { checkRepository, explainPath, initRepo, trackedFiles } from '../runtime/scripts/lib/hfs-check.mjs';
|
|
39
39
|
import { HfsSlotsError } from '../runtime/scripts/lib/hfs-slots.mjs';
|
|
40
40
|
import { formatFindings } from '../sync/format.mjs';
|
|
@@ -44,10 +44,11 @@ import { managedFindings } from '../sync/managed.mjs';
|
|
|
44
44
|
import { emitContracts } from '../emit/contracts.mjs';
|
|
45
45
|
import { ScaffoldError, newService, newSpec } from '../scaffold/service.mjs';
|
|
46
46
|
import { contractEmitFindings } from '../runtime/scripts/lib/hfs-rules/contract.mjs';
|
|
47
|
-
import {
|
|
47
|
+
import { lintRepository, parseLintArgs, printLintText } from '../lint/run.mjs';
|
|
48
|
+
import { writeReport } from '../report/sonar.mjs';
|
|
48
49
|
|
|
49
|
-
const USAGE = `hfs check [--repo <dir>] [--json] [--fast] [--base <ref>]
|
|
50
|
-
hfs
|
|
50
|
+
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>]
|
|
51
52
|
hfs init [--repo <dir>] [--stdout]
|
|
52
53
|
hfs emit-contracts [--repo <dir>]
|
|
53
54
|
hfs explain <path> [--repo <dir>] [--json]
|
|
@@ -57,7 +58,7 @@ hfs new service <dir> <name> [--inject <Decorator>=<module>:<Type> | <Class>=<mo
|
|
|
57
58
|
hfs new spec <file>.service.ts [--repo <dir>]
|
|
58
59
|
`;
|
|
59
60
|
const PER_CODE_LIMIT = 25;
|
|
60
|
-
const VALUE_FLAGS = new Set(['--repo', '--base', '--
|
|
61
|
+
const VALUE_FLAGS = new Set(['--repo', '--base', '--inject']);
|
|
61
62
|
/** Flags that may repeat: their values are collected in order. */
|
|
62
63
|
const LIST_FLAGS = new Set(['--inject']);
|
|
63
64
|
const BOOL_FLAGS = new Set(['--json', '--stdout', '--fast']);
|
|
@@ -96,13 +97,6 @@ function printCheck(result, out) {
|
|
|
96
97
|
out(`\n${counts.error} error finding${counts.error === 1 ? '' : 's'}, ${counts.info} report-only\n`);
|
|
97
98
|
}
|
|
98
99
|
|
|
99
|
-
/** The check's error findings as the Sonar import file: findings outside `sonar.sources` are filed on the first source file. */
|
|
100
|
-
function writeSonarReport({ repoRoot, file, result }) {
|
|
101
|
-
let properties = '';
|
|
102
|
-
try { properties = fs.readFileSync(path.join(repoRoot, 'sonar-project.properties'), 'utf8'); } catch { /* no properties: every finding keeps its own path */ }
|
|
103
|
-
writeReport(file, sonarReport(result.findings, { sourceRoots: sourceRootsOf(properties), tracked: trackedFiles(repoRoot) }));
|
|
104
|
-
}
|
|
105
|
-
|
|
106
100
|
function printExplain(e, out) {
|
|
107
101
|
out(`${e.path}\n`);
|
|
108
102
|
if (e.status === 'no-slot') {
|
|
@@ -120,47 +114,58 @@ function printExplain(e, out) {
|
|
|
120
114
|
if (e.code) out(` ${e.code}: ${e.titleVi}\n ${e.whyVi}\n`);
|
|
121
115
|
}
|
|
122
116
|
|
|
117
|
+
/** The repository pass: slots, managed files, formatter, contract snapshots and the architecture machine's check surface. `only` limits the formatter to those files. */
|
|
118
|
+
async function runCheck({ repoRoot, fast = false, base, only, presets, prettier }) {
|
|
119
|
+
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).
|
|
121
|
+
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.
|
|
123
|
+
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') {
|
|
127
|
+
if (fast) contracts = { status: 'skipped', reason: '--fast does not emit the apps' };
|
|
128
|
+
else {
|
|
129
|
+
const emitted = contractEmitFindings({ repoRoot, files: tracked, repo: declared, emit: emitContracts });
|
|
130
|
+
extraFindings.push(...emitted.findings);
|
|
131
|
+
contracts = { status: 'checked', apps: emitted.apps };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const result = checkRepository({ repoRoot, fast, base, extraFindings });
|
|
135
|
+
if (contracts) result.contracts = contracts;
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** `hfs lint`: ESLint, the repository checks and (front end) stylelint as one report; see lint/run.mjs. */
|
|
140
|
+
async function lintMain(argv, { stdout, presets, prettier }) {
|
|
141
|
+
const opts = parseLintArgs(argv);
|
|
142
|
+
const repoRoot = path.resolve(opts.repo ?? process.cwd());
|
|
143
|
+
const { report, sonar, exit } = await lintRepository({
|
|
144
|
+
repoRoot, opts, trackedFiles,
|
|
145
|
+
hfsCheck: (root) => runCheck({ repoRoot: root, only: opts.changed ?? undefined, presets, prettier }),
|
|
146
|
+
});
|
|
147
|
+
if (opts.sonar !== undefined) writeReport(path.resolve(opts.sonar), sonar);
|
|
148
|
+
if (opts.format === 'json') stdout(`${JSON.stringify(report, null, 2)}
|
|
149
|
+
`); else printLintText(report, stdout);
|
|
150
|
+
return exit;
|
|
151
|
+
}
|
|
152
|
+
|
|
123
153
|
/** `presets` and `prettier` are test seams: the Sonar exclusions sync would load from the repository's installed preset, and the repository's own prettier. */
|
|
124
154
|
export async function main(argv, { stdout = (s) => process.stdout.write(s), stderr = (s) => process.stderr.write(s), presets, prettier } = {}) {
|
|
125
155
|
const [verb, ...rest] = argv;
|
|
126
|
-
if (!['check', 'init', 'explain', 'sync', 'work-hygiene', '
|
|
156
|
+
if (!['check', 'lint', 'init', 'explain', 'sync', 'work-hygiene', 'emit-contracts', 'new'].includes(verb)) { stderr(USAGE); return 2; }
|
|
127
157
|
try {
|
|
128
158
|
if (verb === 'sync' || verb === 'work-hygiene') return await syncMain(argv);
|
|
159
|
+
if (verb === 'lint') return await lintMain(rest, { stdout, presets, prettier });
|
|
129
160
|
const opts = parse(rest);
|
|
130
161
|
const repoRoot = path.resolve(opts.repo ?? process.cwd());
|
|
131
162
|
if (verb === 'check') {
|
|
132
163
|
if (opts.positional.length) throw new Error('hfs check takes no path');
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const extraFindings = [...await managedFindings({ repoRoot, tracked, presets }), ...(opts.fast === true ? [] : await formatFindings({ repoRoot, files: tracked, prettier }))];
|
|
136
|
-
// R23 against the app itself (full pass only): the committed snapshots equal what `emit-contracts` writes now.
|
|
137
|
-
let contracts = null;
|
|
138
|
-
let declared = null;
|
|
139
|
-
try { declared = JSON.parse(fs.readFileSync(path.join(repoRoot, 'hfs.json'), 'utf8')); } catch { /* checkRepository reports the unreadable declaration */ }
|
|
140
|
-
if (declared?.profile === 'be') {
|
|
141
|
-
if (opts.fast === true) contracts = { status: 'skipped', reason: '--fast does not emit the apps' };
|
|
142
|
-
else {
|
|
143
|
-
const emitted = contractEmitFindings({ repoRoot, files: tracked, repo: declared, emit: emitContracts });
|
|
144
|
-
extraFindings.push(...emitted.findings);
|
|
145
|
-
contracts = { status: 'checked', apps: emitted.apps };
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
const result = checkRepository({ repoRoot, fast: opts.fast === true, base: opts.base, extraFindings });
|
|
149
|
-
if (contracts) result.contracts = contracts;
|
|
150
|
-
if (opts.sonar !== undefined) writeSonarReport({ repoRoot, file: path.resolve(opts.sonar), result });
|
|
164
|
+
if (opts.base !== undefined && opts.fast !== true) throw new Error('--base names the ref --fast compares with; it needs --fast');
|
|
165
|
+
const result = await runCheck({ repoRoot, fast: opts.fast === true, base: opts.base, presets, prettier });
|
|
151
166
|
if (opts.json) stdout(`${JSON.stringify(result, null, 2)}\n`); else printCheck(result, stdout);
|
|
152
167
|
return result.ok ? 0 : 1;
|
|
153
168
|
}
|
|
154
|
-
if (verb === 'report') {
|
|
155
|
-
if (opts.positional.length !== 3 || !LINTER_KINDS.includes(opts.positional[0])) throw new Error(`hfs report takes a linter (${LINTER_KINDS.join(' or ')}), an input and an output file`);
|
|
156
|
-
const [kind, input, output] = opts.positional;
|
|
157
|
-
let properties = '';
|
|
158
|
-
try { properties = fs.readFileSync(path.join(repoRoot, 'sonar-project.properties'), 'utf8'); } catch { /* no properties: every finding keeps its own path */ }
|
|
159
|
-
const sourceRoots = sourceRootsOf(properties);
|
|
160
|
-
const issues = convertReportFile({ kind, input: path.resolve(input), output: path.resolve(output), root: repoRoot, sourceRoots, tracked: sourceRoots.length ? trackedFiles(repoRoot) : [] });
|
|
161
|
-
stdout(`hfs report ${kind}: ${issues} issue${issues === 1 ? '' : 's'} written to ${output}\n`);
|
|
162
|
-
return 0;
|
|
163
|
-
}
|
|
164
169
|
if (verb === 'emit-contracts') {
|
|
165
170
|
if (opts.positional.length) throw new Error('hfs emit-contracts takes no path');
|
|
166
171
|
const declaration = JSON.parse(fs.readFileSync(path.join(repoRoot, 'hfs.json'), 'utf8'));
|
|
@@ -201,4 +206,6 @@ export async function main(argv, { stdout = (s) => process.stdout.write(s), stde
|
|
|
201
206
|
}
|
|
202
207
|
}
|
|
203
208
|
|
|
204
|
-
|
|
209
|
+
// Run when this file is the entry point, compared by real path: through a node_modules junction or an npx shim argv[1] is
|
|
210
|
+
// the junction path while import.meta.url is the resolved one, and a plain comparison silently skipped main (hfs printed nothing).
|
|
211
|
+
if (isMain(import.meta.url)) process.exitCode = await main(process.argv.slice(2));
|
package/lint/run.mjs
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// hfs lint - the ONE lint entry and the ONE report of a StarCi repository.
|
|
2
|
+
//
|
|
3
|
+
// hfs lint [--repo <dir>] [--changed <file>...] [--fix] [--format text|json] [--sonar <file>] [--stylelint <glob>]
|
|
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.
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { createRequire } from 'node:module';
|
|
17
|
+
import { spawnSync } from 'node:child_process';
|
|
18
|
+
import { linterReport, mergeReports, sonarReport, sourceRootsOf } from '../report/sonar.mjs';
|
|
19
|
+
|
|
20
|
+
export const LINT_SCHEMA = 'starci/lint@1';
|
|
21
|
+
const CODE_PREFIX = /^\[([A-Z][A-Z0-9_]+)\] /;
|
|
22
|
+
const posix = (file) => String(file).replace(/\\/g, '/').replace(/^\.\//, '');
|
|
23
|
+
|
|
24
|
+
/** The flags of `hfs lint`; `--changed` takes every argument up to the next flag. */
|
|
25
|
+
export function parseLintArgs(argv) {
|
|
26
|
+
const opts = { changed: null, fix: false, format: 'text', repo: undefined, sonar: undefined, stylelint: undefined };
|
|
27
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
28
|
+
const arg = argv[i];
|
|
29
|
+
if (arg === '--changed') {
|
|
30
|
+
opts.changed = [];
|
|
31
|
+
while (i + 1 < argv.length && !argv[i + 1].startsWith('--')) opts.changed.push(posix(argv[++i]));
|
|
32
|
+
} else if (arg === '--fix') opts.fix = true;
|
|
33
|
+
else if (['--format', '--repo', '--sonar', '--stylelint'].includes(arg)) {
|
|
34
|
+
if (argv[i + 1] === undefined) throw new Error(`${arg} needs a value`);
|
|
35
|
+
opts[arg.slice(2)] = argv[++i];
|
|
36
|
+
} else throw new Error(`unknown argument ${arg}`);
|
|
37
|
+
}
|
|
38
|
+
if (!['text', 'json'].includes(opts.format)) throw new Error('--format is text or json');
|
|
39
|
+
return opts;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** A linter's json results through the repository's own install: `{ results }` or `{ error }`. */
|
|
43
|
+
function runLinter({ repoRoot, pkg, bin, args }) {
|
|
44
|
+
let entry;
|
|
45
|
+
try {
|
|
46
|
+
const manifest = createRequire(path.join(repoRoot, 'package.json')).resolve(`${pkg}/package.json`);
|
|
47
|
+
const declared = JSON.parse(fs.readFileSync(manifest, 'utf8')).bin;
|
|
48
|
+
entry = path.join(path.dirname(manifest), typeof declared === 'string' ? declared : declared[bin]);
|
|
49
|
+
} catch {
|
|
50
|
+
return { error: `${pkg} is not installed under ${repoRoot}` };
|
|
51
|
+
}
|
|
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.
|
|
54
|
+
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]}` }; }
|
|
56
|
+
if (!Array.isArray(results)) return { error: `${pkg} produced a report that is not a result list` };
|
|
57
|
+
return { results };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const eslintFindings = (results, repoRoot) => results.flatMap((result) => (result.messages ?? []).map((message) => ({
|
|
61
|
+
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,
|
|
63
|
+
})));
|
|
64
|
+
|
|
65
|
+
const stylelintFindings = (results, repoRoot) => results.flatMap((result) => (result.warnings ?? []).map((warning) => ({
|
|
66
|
+
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,
|
|
68
|
+
})));
|
|
69
|
+
|
|
70
|
+
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
|
+
|
|
72
|
+
/**
|
|
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.
|
|
75
|
+
*/
|
|
76
|
+
export async function lintRepository({ repoRoot, opts, hfsCheck, trackedFiles = () => [] }) {
|
|
77
|
+
const changed = opts.changed === null ? null : new Set(opts.changed);
|
|
78
|
+
const existing = changed ? [...changed].filter((file) => fs.existsSync(path.join(repoRoot, file))) : [];
|
|
79
|
+
const errors = [];
|
|
80
|
+
const findings = [];
|
|
81
|
+
const raw = { eslint: [], stylelint: [] };
|
|
82
|
+
const engines = {};
|
|
83
|
+
|
|
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 : ['.'])] });
|
|
87
|
+
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' };
|
|
91
|
+
|
|
92
|
+
if (opts.stylelint !== undefined) {
|
|
93
|
+
const styles = existing.filter((file) => file.endsWith('.css'));
|
|
94
|
+
if (changed === null || styles.length) {
|
|
95
|
+
const linted = runLinter({ repoRoot, pkg: 'stylelint', bin: 'stylelint', args: [...(changed ? styles : [opts.stylelint]), '--formatter', 'json', ...(opts.fix ? ['--fix'] : [])] });
|
|
96
|
+
if (linted.error) errors.push(linted.error);
|
|
97
|
+
else { raw.stylelint = linted.results; findings.push(...stylelintFindings(linted.results, repoRoot)); }
|
|
98
|
+
}
|
|
99
|
+
engines.stylelint = { files: changed ? styles.length : null };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let checked = { findings: [] };
|
|
103
|
+
try { checked = await hfsCheck(repoRoot); } catch (error) { errors.push(`hfs check could not run: ${String(error?.message ?? error)}`); }
|
|
104
|
+
const repoErrors = checked.findings.filter((finding) => finding.level === 'error');
|
|
105
|
+
const kept = changed === null ? repoErrors : repoErrors.filter((finding) => !finding.path || changed.has(posix(finding.path)));
|
|
106
|
+
for (const finding of kept) {
|
|
107
|
+
findings.push({ engine: 'hfs', rule: finding.code, code: finding.code, severity: 'error', path: finding.path ? posix(finding.path) : null, line: finding.line ?? null, column: finding.column ?? null, message: finding.message, titleVi: finding.titleVi, nextStepVi: finding.nextStepVi });
|
|
108
|
+
}
|
|
109
|
+
engines.hfs = { findings: kept.length };
|
|
110
|
+
findings.sort(byLocation);
|
|
111
|
+
|
|
112
|
+
let properties = '';
|
|
113
|
+
try { properties = fs.readFileSync(path.join(repoRoot, 'sonar-project.properties'), 'utf8'); } catch { /* no properties: every finding keeps its own path */ }
|
|
114
|
+
const sourceRoots = sourceRootsOf(properties);
|
|
115
|
+
const tracked = sourceRoots.length ? trackedFiles(repoRoot) : [];
|
|
116
|
+
const sonar = mergeReports([
|
|
117
|
+
sonarReport(kept, { sourceRoots, tracked }),
|
|
118
|
+
linterReport('eslint', raw.eslint, { root: repoRoot, sourceRoots, tracked }),
|
|
119
|
+
...(opts.stylelint !== undefined ? [linterReport('stylelint', raw.stylelint, { root: repoRoot, sourceRoots, tracked })] : []),
|
|
120
|
+
]);
|
|
121
|
+
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
|
+
return { report, sonar, exit: errors.length ? 2 : findings.length ? 1 : 0 };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function printLintText(report, out) {
|
|
126
|
+
for (const f of report.findings) out(`${f.path ?? '-'}${f.line ? `:${f.line}${f.column ? `:${f.column}` : ''}` : ''} ${f.engine}/${f.rule} ${f.message}\n`);
|
|
127
|
+
for (const e of report.errors) out(`ERROR ${e}\n`);
|
|
128
|
+
const per = Object.keys(report.engines).map((engine) => `${engine} ${report.findings.filter((f) => f.engine === engine).length}`).join(', ');
|
|
129
|
+
out(`\nhfs lint: ${report.counts.error} finding${report.counts.error === 1 ? '' : 's'} (${per})${report.errors.length ? `, ${report.errors.length} tool error${report.errors.length === 1 ? '' : 's'}` : ''}\n`);
|
|
130
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@starci/hfs",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
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.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"files": [
|
|
16
16
|
"bin/hfs.mjs",
|
|
17
17
|
"emit/**",
|
|
18
|
+
"lint/**",
|
|
18
19
|
"scaffold/**",
|
|
19
20
|
"report/**",
|
|
20
21
|
"runtime/**",
|
package/report/sonar.mjs
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// into the same document format, engineId `eslint` / `stylelint`, rule id = the linter's
|
|
9
|
-
// rule. Sonar's own ESLint import (sonar.eslint.reportPaths) is not used: it drops an issue
|
|
10
|
-
// on a file outside sonar.sources, and a stylelint result has no native import at all.
|
|
1
|
+
// Sonar documents of the ONE lint entry `hfs lint` (contract changes hfs-sonar-import, hfs-lint-entry): the building blocks that turn the
|
|
2
|
+
// findings of the StarCi canon into a Sonar Generic Issue Import document (SonarQube 10.3+ format: `{ rules, issues }`).
|
|
3
|
+
// sonarReport(findings) the repository findings of `hfs check`, engineId `starci-hfs`, rule id = the finding code
|
|
4
|
+
// linterReport(kind, results) a linter's own json output (`eslint -f json`, `stylelint --formatter json`), engineId `eslint` / `stylelint`,
|
|
5
|
+
// rule id = the linter's rule. Sonar's own ESLint import (sonar.eslint.reportPaths) is not used: it drops an
|
|
6
|
+
// issue on a file outside sonar.sources, and a stylelint result has no native import at all.
|
|
7
|
+
// mergeReports(reports) the documents of one `hfs lint` run as ONE file (reports/lint.sonar.json, sonar.externalIssuesReportPaths)
|
|
11
8
|
// One placement rule for every engine: Sonar imports an issue only on a file it indexes (a tracked source or stylesheet under
|
|
12
9
|
// sonar.sources). A finding on any other path (hfs.json, a workflow, a package under an unindexed root, e2e/, a directory) is filed
|
|
13
10
|
// on the first source file of sonar.sources and its message names the real path, so no finding is dropped.
|
|
@@ -42,6 +39,12 @@ function document(rules, issues) {
|
|
|
42
39
|
return { rules: [...rules.values()].filter((rule) => used.has(rule.id)).sort((a, b) => cmp(a.id, b.id)), issues: sorted };
|
|
43
40
|
}
|
|
44
41
|
|
|
42
|
+
/** One document of several (the starci-hfs, eslint and stylelint documents of one `hfs lint` run): rules by id, issues in the one sort order. */
|
|
43
|
+
export function mergeReports(reports) {
|
|
44
|
+
const rules = new Map(reports.flatMap((report) => report.rules).map((rule) => [rule.id, rule]));
|
|
45
|
+
return document(rules, reports.flatMap((report) => report.issues));
|
|
46
|
+
}
|
|
47
|
+
|
|
45
48
|
/**
|
|
46
49
|
* The first source file of the repository (sorted) under one of `sourceRoots`: Sonar imports an issue only on a file it
|
|
47
50
|
* indexes, and a finding about hfs.json, a workflow, a config file or a directory is not on one, so it is filed on this
|
|
@@ -106,7 +109,6 @@ const LINTERS = Object.freeze({
|
|
|
106
109
|
text: (message) => message.message,
|
|
107
110
|
describe: (id) => `ESLint rule ${id} of the StarCi canon (@starci/eslint-canon-be and @starci/eslint-canon-fe) or of a plugin it composes.`,
|
|
108
111
|
describeError: 'ESLint could not lint a file: a parse error or an invalid configuration.',
|
|
109
|
-
shape: 'an array of results with a filePath and messages',
|
|
110
112
|
},
|
|
111
113
|
stylelint: {
|
|
112
114
|
engine: STYLELINT_ENGINE,
|
|
@@ -117,11 +119,10 @@ const LINTERS = Object.freeze({
|
|
|
117
119
|
text: stylelintText,
|
|
118
120
|
describe: (id) => `Stylelint rule ${id} of the StarCi CSS canon (@starci/stylelint-canon, HFS R61).`,
|
|
119
121
|
describeError: 'Stylelint could not read a stylesheet: a parse error or an invalid option.',
|
|
120
|
-
shape: 'an array of results with a source and warnings',
|
|
121
122
|
},
|
|
122
123
|
});
|
|
123
124
|
|
|
124
|
-
|
|
125
|
+
const LINTER_KINDS = Object.freeze(Object.keys(LINTERS));
|
|
125
126
|
|
|
126
127
|
/**
|
|
127
128
|
* A linter's json results (`eslint -f json`: [{ filePath, messages: [{ ruleId, line, endLine, message }] }]; `stylelint
|
|
@@ -162,19 +163,3 @@ export function writeReport(file, report) {
|
|
|
162
163
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
163
164
|
fs.writeFileSync(file, `${JSON.stringify(report, null, 2)}\n`);
|
|
164
165
|
}
|
|
165
|
-
|
|
166
|
-
/** `hfs report <eslint|stylelint> <in> <out> [--repo <dir>]`: convert a linter's json report; returns the number of issues written. */
|
|
167
|
-
export function convertReportFile({ kind, input, output, root, sourceRoots = [], tracked = [] }) {
|
|
168
|
-
const linter = LINTERS[kind];
|
|
169
|
-
if (linter === undefined) throw new Error(`unknown linter ${kind}; expected ${LINTER_KINDS.join(' or ')}`);
|
|
170
|
-
let results;
|
|
171
|
-
try {
|
|
172
|
-
results = JSON.parse(fs.readFileSync(input, 'utf8'));
|
|
173
|
-
} catch (error) {
|
|
174
|
-
throw new Error(`${input} is not a readable ${kind} json report (${error.message})`);
|
|
175
|
-
}
|
|
176
|
-
if (!Array.isArray(results)) throw new Error(`${input} is not a ${kind} json report: expected ${linter.shape}`);
|
|
177
|
-
const report = linterReport(kind, results, { root, sourceRoots, tracked });
|
|
178
|
-
writeReport(output, report);
|
|
179
|
-
return report.issues.length;
|
|
180
|
-
}
|
|
@@ -10,33 +10,33 @@ pins:
|
|
|
10
10
|
# --- @starci packages. Every @starci package is a PUBLISHED npm package (install: registry): a product repository
|
|
11
11
|
# installs the exact pinned version from the npm registry in the root and every workspace package.json, never `file:`.
|
|
12
12
|
'@starci/grammar':
|
|
13
|
-
version: 0.
|
|
13
|
+
version: 0.8.0
|
|
14
14
|
group: starci
|
|
15
15
|
install: registry
|
|
16
16
|
side: fe
|
|
17
17
|
source: packages/grammar/package.json
|
|
18
|
-
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.7.2
|
|
18
|
+
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).
|
|
19
19
|
'@starci/eslint-canon-be':
|
|
20
|
-
version: 2.0
|
|
20
|
+
version: 2.1.0
|
|
21
21
|
group: starci
|
|
22
22
|
install: registry
|
|
23
23
|
side: be
|
|
24
24
|
source: packages/eslint/be/package.json
|
|
25
25
|
why: '2.0.0 (C0 release): starciBeConfig({ hfs: loadHfs(import.meta.url) }) typed factory and the BE-CONVENTION laws (R36 sql-text-only, R83 no-repository-class, R85/R87/R89 service-deps, R43 config-parsed-in-main, R47/R48 test world, R95 operations, R98-R101 builders).'
|
|
26
26
|
'@starci/eslint-canon-fe':
|
|
27
|
-
version: 7.
|
|
27
|
+
version: 7.1.0
|
|
28
28
|
group: starci
|
|
29
29
|
install: registry
|
|
30
30
|
side: fe
|
|
31
31
|
source: packages/eslint/fe/package.json
|
|
32
32
|
why: '7.0.0: the front end has no tests (test rules removed, FE_NO_TESTS R97 in hfs); no-vietnamese-in-source (R91).'
|
|
33
33
|
'@starci/stylelint-canon':
|
|
34
|
-
version: 2.0.
|
|
34
|
+
version: 2.0.1
|
|
35
35
|
group: starci
|
|
36
36
|
install: registry
|
|
37
37
|
side: fe
|
|
38
38
|
source: packages/stylelint/package.json
|
|
39
|
-
why: '2.0.0 adds status-contrast (HeroUI soft pairs measured per theme), fixes brand-layer-shape on the shared :root,.light,.dark block and the info soft pair, and derives appTokens with loadAppTokens(import.meta.url) for the managed one-line stylelint.config.mjs.'
|
|
39
|
+
why: '2.0.1 accepts `--font-sans` and `--font-mono` in the brand layer (the vocabulary follows the grammar 0.8.0). 2.0.0 adds status-contrast (HeroUI soft pairs measured per theme), fixes brand-layer-shape on the shared :root,.light,.dark block and the info soft pair, and derives appTokens with loadAppTokens(import.meta.url) for the managed one-line stylelint.config.mjs.'
|
|
40
40
|
'@starci/tsconfig':
|
|
41
41
|
version: 2.0.0
|
|
42
42
|
group: starci
|
|
@@ -57,7 +57,7 @@ pins:
|
|
|
57
57
|
source: packages/jest-preset/package.json
|
|
58
58
|
why: '2.1.0: four projects on the test world, the unit kit (mockEntityManager, fakeTransaction, fakeIds, FakeClock, Outcome matchers) and the recordingOutbox claim side.'
|
|
59
59
|
'@starci/hfs':
|
|
60
|
-
version:
|
|
60
|
+
version: 3.0.0
|
|
61
61
|
group: starci
|
|
62
62
|
install: registry
|
|
63
63
|
side: both
|
|
@@ -32,13 +32,14 @@ newCode:
|
|
|
32
32
|
metric: new_security_hotspots_reviewed
|
|
33
33
|
minReviewedPercent: 100
|
|
34
34
|
unreviewedMax: 0
|
|
35
|
-
# The whole code, not only the new code. Every finding of the HFS canon reaches Sonar through ONE
|
|
36
|
-
# (packages/hfs, contract
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
35
|
+
# The whole code, not only the new code. Every finding of the HFS canon reaches Sonar through ONE entry, `hfs lint`
|
|
36
|
+
# (packages/hfs, contract changes hfs-sonar-import and hfs-lint-entry), which writes ONE file, reports/lint.sonar.json
|
|
37
|
+
# (`npm run lint -- --sonar reports/lint.sonar.json`), named by sonar.externalIssuesReportPaths; the gate holds the open-issue count of
|
|
38
|
+
# the whole project at zero. The file carries three engines:
|
|
39
|
+
# starci-hfs the repository findings of `hfs check` (managed files, tree, pins, CI, contracts; the rule id is the finding code)
|
|
40
|
+
# eslint ESLint's json over the repository (the BE and FE canon plugins alike, per-file and project-graph rules)
|
|
41
|
+
# stylelint stylelint's json over the style glob (front ends)
|
|
42
|
+
# The engines share ONE placement rule (a finding on a file Sonar does not index is filed on the first source file); Sonar's own
|
|
42
43
|
# ESLint import (sonar.eslint.reportPaths) is not used because it drops such issues.
|
|
43
44
|
# A SonarQube gate condition cannot filter issues by engine, so the condition is the project's open issues (`violations`,
|
|
44
45
|
# overall code): the three engines above are the imports it counts, and every native Sonar issue counts with them. That is
|