@starci/hfs 4.7.34 → 4.7.35
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 +6 -0
- package/README.md +8 -11
- package/lint/run.mjs +70 -29
- package/package.json +2 -2
- package/runtime/knowledge/grammars/index.yaml +102 -0
- package/runtime/knowledge/grammars/offset-pop/playbook.yaml +302 -0
- package/runtime/knowledge/grammars/starci/playbook.yaml +1159 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +22 -8
- package/runtime/knowledge/hfs/fe-stack.yaml +7 -0
- package/runtime/knowledge/hfs/rules.yaml +133 -83
- package/runtime/knowledge/hfs/slots.yaml +3 -3
- package/runtime/knowledge/ui/presentation/font.yaml +101 -0
- package/runtime/knowledge/ui/presentation/padding.yaml +216 -0
- package/runtime/modules/kernel/failure-codes.yaml +40 -20
- package/runtime/modules/kernel/removed-vocabulary.yaml +29 -0
- package/runtime/packages/cli/src/catalog.generated.mjs +7 -7
- package/runtime/scripts/gates/canon-pins.mjs +103 -0
- package/runtime/scripts/gates/gate.mjs +3 -36
- package/runtime/scripts/gates/output.mjs +40 -0
- package/runtime/scripts/gates/test-world-run.mjs +2 -4
- package/runtime/scripts/hfs/architecture/config.mjs +8 -8
- package/runtime/scripts/hfs/check.mjs +1 -1
- package/runtime/scripts/hfs/db-restamp-verb.mjs +2 -2
- package/runtime/scripts/hfs/declaration-shape.mjs +8 -2
- package/runtime/scripts/hfs/grammar-presentation.mjs +152 -0
- package/runtime/scripts/hfs/rule-catalog.mjs +2 -2
- package/runtime/scripts/hfs/rules/a15-findings.mjs +1 -0
- package/runtime/scripts/hfs/rules/fe-stack.mjs +44 -1
- package/runtime/scripts/hfs/slots.mjs +1 -0
- package/runtime/scripts/hfs/view.mjs +1 -0
- package/runtime/scripts/lib/canon-binding-text.mjs +129 -0
- package/runtime/scripts/lib/example-refs.mjs +1 -1
- package/runtime/scripts/lib/grammar-lint.mjs +48 -0
- package/runtime/scripts/lib/grammar-vendor-parity.mjs +31 -111
- package/runtime/scripts/lib/grammar-vendor-source.mjs +345 -0
- package/runtime/scripts/lib/numeric-match.mjs +5 -0
- package/runtime/scripts/lib/source-package-imports.mjs +111 -0
- package/runtime/scripts/work/draw/draw-layer.mjs +5 -4
- package/scaffold/app.mjs +7 -5
- package/src/main.mjs +26 -13
- package/stack/config.mjs +2 -1
- package/stack/render.mjs +24 -3
- package/sync/index.mjs +8 -2
- package/templates/app/skeleton/.starcistacks/dev/infra/compose/stack.yaml +0 -1
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +0 -1
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +0 -1
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +0 -1
- package/templates/fe/tool-config/eslint.config.mjs +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.7.35 - 2026-10-26
|
|
4
|
+
- Changed: the bundled canon pins select @starci/stylelint-canon 2.0.12, @starci/eslint-canon-be 3.4.44, @starci/eslint-canon-fe 9.0.0, the Grammar lint families 0.12.3 and @starci/grammar 0.12.3.
|
|
5
|
+
- Added: `app lint` composes the selected Grammar family lint with the universal front-end canon and refuses canon-fe 9 without it; the Grammar vendor-parity proof reads the generated HeroUI reference; the UI proof checks component ownership and surface layers.
|
|
6
|
+
- Fixed: managed stack renders match and leave unpinned the compose `version` the scaffold declares; the history-hook spec judges the stdin handshake, not a fixed kill; the restamp inventory is active-only and clearer.
|
|
7
|
+
- Changed: the runtime centralizes its parsing and gate summaries.
|
|
8
|
+
|
|
3
9
|
## 4.7.34 - 2026-10-26
|
|
4
10
|
- Fixed: `starci app db restamp` considers only active migrations when validating names and choosing unoccupied versions. Legacy archive names no longer block preview or apply, and archived files stay untouched.
|
|
5
11
|
- Changed: the loosening check reads the tree as it stands and never its history. The bundled `scripts/lib/loosening-state.mjs` (with `js-lex.mjs` and `loosening-config.mjs`) replaces the diff detector; `HFS_BYPASS_MARKER` (R303) reads it: a suppression word opens a comment, a test marker stands in code, a swallowed failure stands in a shell line or in the string a script hands a shell, a package.json is judged by its scripts, and the same words quoted as data are no bypass. It also refuses a coverage target or threshold below the canon, a relaxing switch, a disabled ESLint rule, a tsconfig strictness option turned off and an exception file that `knowledge/hfs/design-truths.yaml` does not list. `--passWithNoTests`, `--coverageThreshold`, `--no-error-on-unmatched-pattern` and a `--max-warnings` budget are bypasses in a script, a hook or a workflow.
|
package/README.md
CHANGED
|
@@ -84,9 +84,7 @@ Every file of a spec-required role of `ruleParams.be.unitRoles` — a `*.service
|
|
|
84
84
|
|
|
85
85
|
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(...)`, `EVENT_BUS` -> `recordingEventBus()`, `QUEUE_OUTBOX` -> `recordingQueueOutbox()`, `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).
|
|
86
86
|
|
|
87
|
-
`starci app check` reads the app's `hfs.json` and
|
|
88
|
-
whole architecture machine over each side folder. Every finding carries a why code and its Vietnamese text
|
|
89
|
-
(`modules/kernel/failure-codes.yaml`).
|
|
87
|
+
`starci app check` runs ESLint once per side through the app's installed managed flat config: the backend canon on `be/`, and the universal FE canon plus the explicitly selected Grammar-family package on `fe/`. It also reads the app's `hfs.json` and tracked paths (`git ls-files`), checks the work tree (the root rules once, the side rules per side), and runs the whole architecture machine over each side folder. HFS findings carry their why code and Vietnamese text (`modules/kernel/failure-codes.yaml`); ESLint findings retain their rule id and failure code.
|
|
90
88
|
|
|
91
89
|
Its own checks (`scripts/hfs/check.mjs`, `scripts/hfs/rules/`; the rendered-file checks (`sync/managed.mjs`) and the prettier check (`sync/format.mjs`) live in `sync/` because they read the templates and the repository's own install, and reach `starci app 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):
|
|
92
90
|
|
|
@@ -103,7 +101,7 @@ Its own checks (`scripts/hfs/check.mjs`, `scripts/hfs/rules/`; the rendered-file
|
|
|
103
101
|
| `HFS_CANON_PIN_DRIFT` | error | a dependency not at the exact version of `knowledge/hfs/canon-pins.yaml` |
|
|
104
102
|
| `HFS_SIZE_SOFT_BACKLOG` | info | a source file over `ruleParams.fileLines.soft`; report only, never fails |
|
|
105
103
|
| `HFS_MANAGED_FILE_DRIFT` | error | a managed file that exists but differs from its render (the `managedBy` slots: hooks, workflows, sonar, the back-end `be.tool-config` files, `app.format-config`, the `scripts` block of `app.package-manifest`, compared as parsed JSON) |
|
|
106
|
-
| `HFS_RULE_OFF_WITHOUT_REPLACEMENT` | error | `eslint.config.mjs`, or a front end's `stylelint.config.mjs`, differs from its
|
|
104
|
+
| `HFS_RULE_OFF_WITHOUT_REPLACEMENT` | error | `eslint.config.mjs`, or a front end's `stylelint.config.mjs`, differs from its managed factory render, so a rule could be off, warned or redefined in it |
|
|
107
105
|
| `HFS_TOOL_CONFIG_LOCAL` | error | a repository holds a tool config outside the managed set (`.eslintrc*`, `.eslintignore`, a second `eslint.config.*` or `stylelint.config.*`, another prettier or lint-staged config, or a jest config in a back end that is not the one root file), a file that defines an ESLint rule or a stylelint plugin, a tool configuration key in a `package.json` (`eslintConfig`, `stylelint`, `prettier`, `lint-staged`, `jest`), or a script that runs eslint, stylelint or prettier with a flag that swaps the configuration |
|
|
108
106
|
| `HFS_SONAR_CONFIG` | error | `sonar-project.properties` differs from its render (no host URL; the ESLint report and HFS import paths), or the stack declaration names another quality gate than the one of `knowledge/sonar-gate.yaml` |
|
|
109
107
|
| `HFS_TS_STRICT` | error | the root `tsconfig.json` sets, lowers or adds anything but `extends` the preset (`be.json`, `next.json`), the template's `exclude` and (back end) the three `paths`; the finding names the flag |
|
|
@@ -145,8 +143,8 @@ Its own checks (`scripts/hfs/check.mjs`, `scripts/hfs/rules/`; the rendered-file
|
|
|
145
143
|
|
|
146
144
|
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
|
|
147
145
|
files is the `managedBy` slots of `slots.yaml`; `starci app sync --write` renders them and `starci app check` compares them, so a hand edit and a forgotten `sync` are the same finding.
|
|
148
|
-
Each finding is reported once: the
|
|
149
|
-
The root and both sides are rendered by the one mechanism. The root: the `scripts` block of `app.package-manifest` (`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), `app.format-config`, `app.hooks`, the CI slots (`app.ci`, `app.ci-e2e`, `app.ci-images`), `app.quality-config` and the managed block of `app.git-meta`. A side: its `be.tool-config` or `fe.tool-config` slot — the side's tsconfig (resolving `@starci/tsconfig` from the root `node_modules`) and its
|
|
146
|
+
Each finding is reported once: the managed ESLint and stylelint factory configs 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.
|
|
147
|
+
The root and both sides are rendered by the one mechanism. The root: the `scripts` block of `app.package-manifest` (`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), `app.format-config`, `app.hooks`, the CI slots (`app.ci`, `app.ci-e2e`, `app.ci-images`), `app.quality-config` and the managed block of `app.git-meta`. A side: its `be.tool-config` or `fe.tool-config` slot — the side's tsconfig (resolving `@starci/tsconfig` from the root `node_modules`) and its managed ESLint factory config, the back end's build tsconfig, tests tsconfig and jest configuration, the front end's stylelint configuration. `lint` is the one lint gate, `starci app 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
|
|
150
148
|
(slot `app.task-graph`): it carries the task graph, which no preset can render.
|
|
151
149
|
|
|
152
150
|
The architecture machine (`scripts/hfs/architecture.mjs` of the runtime, the same code bundled here): tiers and import
|
|
@@ -156,12 +154,12 @@ finding under the machine's own rule id (`BE_TIER_DIRECTION`, `HFS_UNUSED_EXPORT
|
|
|
156
154
|
repository without `typescript` installed fails with `ARCH_TYPESCRIPT_MISSING`; the check never passes because it could not run.
|
|
157
155
|
|
|
158
156
|
`--fast` judges only what changed since the merge-base of `HEAD` with
|
|
159
|
-
`origin/main` (else `main`; `--base <ref>` names another ref): the slot and pin checks on the changed paths, the machine on the owners
|
|
157
|
+
`origin/main` (else `main`; `--base <ref>` names another ref): ESLint on changed source files, the slot and pin checks on the changed paths, the machine on the owners
|
|
160
158
|
of the changed source files without clones and dead exports, and no file-system tree checks. The required-file and minimum-instance
|
|
161
159
|
checks still cover the whole tree. With no merge-base `--fast` is a refusal (exit 2) that names the fix, never a silent full
|
|
162
160
|
pass.
|
|
163
161
|
|
|
164
|
-
Exit codes: 0 clean, 1 an error finding, 2 a refusal (not a Git work tree, bad flag, `--fast` with no merge-base). `starci app check` and `starci app lint` never write to the app
|
|
162
|
+
Exit codes: 0 clean, 1 an error finding, 2 a tool could not run or a refusal (not a Git work tree, bad flag, `--fast` with no merge-base). `starci app check` and `starci app lint` never write to the app
|
|
165
163
|
(`starci app lint --fix` lets ESLint and stylelint fix in place).
|
|
166
164
|
|
|
167
165
|
## Sonar
|
|
@@ -169,7 +167,7 @@ Exit codes: 0 clean, 1 an error finding, 2 a refusal (not a Git work tree, bad f
|
|
|
169
167
|
One mechanism for both sides: every finding of the canon is imported into Sonar, and the quality gate
|
|
170
168
|
fails while any is open. Nothing is configured per app; the pieces are managed files (`starci app sync`) and this package.
|
|
171
169
|
|
|
172
|
-
One entry, one report, one file: `npm run lint` is `starci app 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
|
|
170
|
+
One entry, one report, one file: `npm run lint` is `starci app 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, rule id = the finding code), `eslint` (the BE canon, universal FE canon and selected Grammar-family plugin, rule id = the ESLint rule) and `stylelint` over fe (rule id = the stylelint rule). App lint shares app check's ESLint runner and calls its HFS checks in process, so each side's ESLint config runs once. `starci app 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).
|
|
173
171
|
|
|
174
172
|
`--sonar` writes the findings as a Generic Issue Import document (SonarQube 10.3+ format: `{ rules, issues }`) before the verdict, so a
|
|
175
173
|
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;
|
|
@@ -202,8 +200,7 @@ derived from the machine's rule id lists). After changing any of those files, `k
|
|
|
202
200
|
`starci release sync-runtime --check` fails on a stale copy. Bump `version` here and in the pin when the behaviour changes.
|
|
203
201
|
|
|
204
202
|
The examples workflow (`.github/workflows/examples.yml`) runs `starci app lint` of this CLI at the root of every `examples/*`
|
|
205
|
-
app with an `hfs.json` (each `examples/<app>` root) and fails on any finding or any tool that could not run
|
|
206
|
-
`starci app check` alone would miss the machine's source rules, which the canons judge.
|
|
203
|
+
app with an `hfs.json` (each `examples/<app>` root) and fails on any finding or any tool that could not run. This includes the ESLint canons and repository checks also run by `starci app check`, plus stylelint over the frontend stylesheets.
|
|
207
204
|
|
|
208
205
|
## Serving knowledge to other packages
|
|
209
206
|
|
package/lint/run.mjs
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// turbo `lint` task); the root `npm run lint` stays the whole app. There is no second linter: a workspace lint is this lint, scoped.
|
|
18
18
|
import fs from 'node:fs';
|
|
19
19
|
import path from 'node:path';
|
|
20
|
-
import { createRequire } from 'node:module';
|
|
20
|
+
import { createRequire, registerHooks } from 'node:module';
|
|
21
21
|
import { spawn } from 'node:child_process';
|
|
22
22
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
23
23
|
import { linterReport, mergeReports, sonarReport, sourceRootsOf } from '../report/sonar.mjs';
|
|
@@ -25,6 +25,7 @@ import { SIDES, appRelativeMessages, loadSlotManifest, readRepoDeclaration } fro
|
|
|
25
25
|
import { STYLE_GLOB } from '../sync/index.mjs';
|
|
26
26
|
import { byCodeUnit } from '../report/order.mjs';
|
|
27
27
|
import { braceVariants, globExpression } from '../runtime/scripts/lib/glob.mjs';
|
|
28
|
+
import { grammarLintFor } from '../runtime/scripts/lib/grammar-lint.mjs';
|
|
28
29
|
|
|
29
30
|
// --side judges only that side; root HFS findings belong to be, its Sonar report stays on that side.
|
|
30
31
|
export const LINT_SCHEMA = 'starci/lint@1';
|
|
@@ -164,18 +165,46 @@ export function canonScope(config) {
|
|
|
164
165
|
};
|
|
165
166
|
}
|
|
166
167
|
|
|
167
|
-
/**
|
|
168
|
-
|
|
169
|
-
const
|
|
168
|
+
/** Resolve one canon from this app's install; a package from an enclosing repository cannot prove its lint contract. */
|
|
169
|
+
function ownCanonEntry({ repoRoot, cwd }, packageName) {
|
|
170
|
+
const require = createRequire(path.join(cwd, 'package.json'));
|
|
171
|
+
// The entry is imported below, so resolve with Node's import conditions even when a package publishes no require target.
|
|
172
|
+
// The synchronous hook exists only for this lookup; later package resolution keeps its own conditions.
|
|
173
|
+
const hook = registerHooks({ resolve: (specifier, context, nextResolve) => nextResolve(specifier, specifier === packageName
|
|
174
|
+
? { ...context, conditions: [...new Set([...context.conditions.filter(condition => condition !== 'require'), 'import'])] }
|
|
175
|
+
: context) });
|
|
176
|
+
let entry;
|
|
177
|
+
try { entry = require.resolve(packageName); }
|
|
178
|
+
finally { hook.deregister(); }
|
|
170
179
|
// The canon must be the app's own install: resolved inside the app root, or inside the app's node_modules when that folder is a link to the install
|
|
171
180
|
// (the runtime's examples lint in a sealed copy whose node_modules links the example's own install, scripts/hfs/example-secrets.mjs).
|
|
172
181
|
const ownInstall = [repoRoot, path.join(repoRoot, 'node_modules')].filter((dir) => fs.existsSync(dir)).map((dir) => fs.realpathSync(dir));
|
|
173
182
|
if (!ownInstall.some((root) => inside(root, fs.realpathSync(entry)))) throw new Error('the installed canon is outside the app root');
|
|
183
|
+
return entry;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Both installed FE canons supply the source config; family selection is explicit and an unavailable factory fails closed. */
|
|
187
|
+
export async function installedLintConfig({ repoRoot, cwd, side }) {
|
|
188
|
+
const entry = ownCanonEntry({ repoRoot, cwd }, `@starci/eslint-canon-${side}`);
|
|
174
189
|
const canon = await import(pathToFileURL(entry).href);
|
|
175
190
|
const factory = side === 'be' ? canon.starciBeConfig : canon.starciFeConfig;
|
|
176
191
|
if (typeof factory !== 'function' || typeof canon.loadHfs !== 'function') throw new Error('the installed canon has no supported config factory');
|
|
177
|
-
const
|
|
192
|
+
const hfs = canon.loadHfs(pathToFileURL(path.join(cwd, 'eslint.config.mjs')).href);
|
|
193
|
+
const config = await factory({ hfs });
|
|
178
194
|
if (!Array.isArray(config)) throw new Error('the installed canon did not return a flat config');
|
|
195
|
+
if (side !== 'fe') return config;
|
|
196
|
+
const family = grammarLintFor(hfs.grammar);
|
|
197
|
+
if (family.package === null) return config;
|
|
198
|
+
const familyCanon = await import(pathToFileURL(ownCanonEntry({ repoRoot, cwd }, family.package)).href);
|
|
199
|
+
if (typeof familyCanon[family.factory] !== 'function') throw new Error(`${family.package} has no supported ${family.factory} factory`);
|
|
200
|
+
const familyConfig = await familyCanon[family.factory]({ hfs });
|
|
201
|
+
if (!Array.isArray(familyConfig)) throw new Error(`${family.package} did not return a flat config`);
|
|
202
|
+
return [...config, ...familyConfig];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Read both installed canons' source selectors, then enumerate that scope; unavailable selectors cannot prove coverage. */
|
|
206
|
+
async function sourceFiles({ repoRoot, cwd, side, scope: folderScope }) {
|
|
207
|
+
const config = await installedLintConfig({ repoRoot, cwd, side });
|
|
179
208
|
const scope = canonScope(config);
|
|
180
209
|
const files = [];
|
|
181
210
|
const visit = (folder) => {
|
|
@@ -217,7 +246,7 @@ function checkedEslintResults({ results, expected, cwd, scope, changed }) {
|
|
|
217
246
|
}
|
|
218
247
|
|
|
219
248
|
/** Starts the ESLint child of every side that has work and returns one record per side: skipped, a scope error, or the running child. */
|
|
220
|
-
async function launchEslint({ app, repoRoot, opts, changed, existing, workspace, onFe }) {
|
|
249
|
+
async function launchEslint({ app, repoRoot, opts, changed, existing, workspace, onFe, run = runLinter }) {
|
|
221
250
|
const launches = [];
|
|
222
251
|
for (const side of app ? SIDES.filter((candidate) => opts.side === undefined || opts.side === candidate) : []) {
|
|
223
252
|
if (workspace !== null && side !== STYLE_SIDE) { launches.push({ side, skipped: `outside --workspace ${workspace}` }); continue; }
|
|
@@ -227,11 +256,38 @@ async function launchEslint({ app, repoRoot, opts, changed, existing, workspace,
|
|
|
227
256
|
let expected;
|
|
228
257
|
try { expected = changed ? sources : await sourceFiles({ repoRoot, cwd, side, scope: onFe }); }
|
|
229
258
|
catch (error) { launches.push({ side, scopeError: `eslint source scope is unavailable in ${side}/: ${String(error?.message ?? error)}` }); continue; }
|
|
230
|
-
launches.push({ side, cwd, expected, run:
|
|
259
|
+
launches.push({ side, cwd, expected, run: run({ cwd, pkg: 'eslint', bin: 'eslint', args: ['--format', 'json', ...(opts.fix ? ['--fix'] : []), ...(changed ? sources : [onFe ?? '.'])], bound: repoRoot }) });
|
|
231
260
|
}
|
|
232
261
|
return launches;
|
|
233
262
|
}
|
|
234
263
|
|
|
264
|
+
/** Execute each side's installed ESLint config once; check and lint share scope, coverage and tool-failure judgment. */
|
|
265
|
+
export async function eslintRepository({ repoRoot, app = readRepoDeclaration(loadSlotManifest(), repoRoot), opts = {}, changed = null, existing = [], workspace = null, onFe = null, run }) {
|
|
266
|
+
const findings = [], errors = [], raw = [];
|
|
267
|
+
const engine = { sides: {} };
|
|
268
|
+
if (!app.sides) throw new Error('ESLint runs at the app root, the folder of hfs.json');
|
|
269
|
+
const launches = await launchEslint({ app, repoRoot, opts, changed, existing, workspace, onFe, run });
|
|
270
|
+
const runs = await Promise.all(launches.map(launch => launch.run ?? null));
|
|
271
|
+
launches.forEach((launch, at) => {
|
|
272
|
+
const { side } = launch;
|
|
273
|
+
if (launch.skipped) { engine.sides[side] = { files: 0, skipped: launch.skipped }; return; }
|
|
274
|
+
if (launch.scopeError) { errors.push(launch.scopeError); engine.sides[side] = { files: 0 }; return; }
|
|
275
|
+
const linted = runs[at];
|
|
276
|
+
if (linted.error) errors.push(linted.error);
|
|
277
|
+
if (linted.results) {
|
|
278
|
+
const checked = checkedEslintResults({ results: linted.results, expected: launch.expected, cwd: launch.cwd, scope: onFe, changed: changed !== null });
|
|
279
|
+
errors.push(...checked.errors);
|
|
280
|
+
linted.results = checked.results;
|
|
281
|
+
const appRelative = appRelativeMessages(side, path.join(repoRoot, side));
|
|
282
|
+
for (const result of linted.results) for (const message of result.messages ?? []) message.message = appRelative(message.message);
|
|
283
|
+
raw.push(...linted.results);
|
|
284
|
+
findings.push(...eslintFindings(linted.results, repoRoot));
|
|
285
|
+
}
|
|
286
|
+
engine.sides[side] = { files: linted.results?.length ?? 0 };
|
|
287
|
+
});
|
|
288
|
+
return { findings, errors, raw, engine };
|
|
289
|
+
}
|
|
290
|
+
|
|
235
291
|
/** Starts stylelint over the fe stylesheets (the running child), or returns null when no stylesheet is in scope. */
|
|
236
292
|
function launchStylelint({ repoRoot, opts, changed, existing, onFe }) {
|
|
237
293
|
if (opts.side === 'be') return null;
|
|
@@ -244,7 +300,7 @@ function launchStylelint({ repoRoot, opts, changed, existing, onFe }) {
|
|
|
244
300
|
* Run the whole lint of the app at `repoRoot`. `hfsCheck(repoRoot)` returns the `starci app check` result (`{ findings, tracked }`); the CLI
|
|
245
301
|
* injects it. Returns `{ report, sonar, exit }`; `sonar` is the merged Generic Issue Import document.
|
|
246
302
|
*/
|
|
247
|
-
export async function lintRepository({ repoRoot, opts, hfsCheck, trackedFiles = () => [] }) {
|
|
303
|
+
export async function lintRepository({ repoRoot, opts, hfsCheck, trackedFiles = () => [], runLinter: linter }) {
|
|
248
304
|
const changed = opts.changed === null ? null : new Set(opts.changed);
|
|
249
305
|
const errors = [];
|
|
250
306
|
let workspace = null;
|
|
@@ -269,31 +325,16 @@ export async function lintRepository({ repoRoot, opts, hfsCheck, trackedFiles =
|
|
|
269
325
|
const onFe = workspace === null ? null : workspace.slice(STYLE_SIDE.length + 1);
|
|
270
326
|
|
|
271
327
|
// The linters and the app check are independent: every child starts first, the app check runs while they work, and the results are read in a fixed order.
|
|
272
|
-
const
|
|
328
|
+
const eslintRun = app ? eslintRepository({ app, repoRoot, opts, changed, existing, workspace, onFe, run: linter }) : null;
|
|
273
329
|
const styleLaunch = app ? launchStylelint({ repoRoot, opts, changed, existing, onFe }) : null;
|
|
274
330
|
const checkRun = app ? Promise.resolve().then(() => hfsCheck(repoRoot)).then((value) => ({ value }), (error) => ({ error })) : null;
|
|
275
331
|
|
|
276
332
|
// 1. ESLint, once per side, from the side folder with that side's config.
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
if (launch.scopeError) { errors.push(launch.scopeError); engines.eslint.sides[side] = { files: 0 }; return; }
|
|
283
|
-
const linted = eslintRuns[at];
|
|
284
|
-
if (linted.error) errors.push(linted.error);
|
|
285
|
-
if (linted.results) {
|
|
286
|
-
const checkedResults = checkedEslintResults({ results: linted.results, expected: launch.expected, cwd: launch.cwd, scope: onFe, changed: changed !== null });
|
|
287
|
-
errors.push(...checkedResults.errors);
|
|
288
|
-
linted.results = checkedResults.results;
|
|
289
|
-
// The side canon names side-relative paths in its messages; the report names every path from the app root.
|
|
290
|
-
const appRelative = appRelativeMessages(side, path.join(repoRoot, side));
|
|
291
|
-
for (const result of linted.results) for (const message of result.messages ?? []) message.message = appRelative(message.message);
|
|
292
|
-
raw.eslint.push(...linted.results);
|
|
293
|
-
findings.push(...eslintFindings(linted.results, repoRoot));
|
|
294
|
-
}
|
|
295
|
-
engines.eslint.sides[side] = { files: linted.results?.length ?? 0 };
|
|
296
|
-
});
|
|
333
|
+
const eslint = eslintRun ? await eslintRun : { engine: { sides: {} }, errors: [], raw: [], findings: [] };
|
|
334
|
+
engines.eslint = eslint.engine;
|
|
335
|
+
errors.push(...eslint.errors);
|
|
336
|
+
raw.eslint.push(...eslint.raw);
|
|
337
|
+
findings.push(...eslint.findings);
|
|
297
338
|
|
|
298
339
|
// 3. stylelint over the fe side's stylesheets.
|
|
299
340
|
if (app) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@starci/hfs",
|
|
3
|
-
"version": "4.7.
|
|
3
|
+
"version": "4.7.35",
|
|
4
4
|
"description": "The StarCi app command implementation for scaffolding, linting, checking and synchronizing one product repository.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"CHANGELOG.md"
|
|
51
51
|
],
|
|
52
52
|
"engines": {
|
|
53
|
-
"node": ">=22.
|
|
53
|
+
"node": ">=22.15.0"
|
|
54
54
|
},
|
|
55
55
|
"publishConfig": {
|
|
56
56
|
"access": "public"
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
schema: starci/knowledge-source@1
|
|
2
|
+
id: grammars
|
|
3
|
+
title: Grammar families
|
|
4
|
+
purpose: |
|
|
5
|
+
Catalog of authored UI Grammar families. Inventory only — not a default-family rule.
|
|
6
|
+
The selected design specification names the applicable installed family. The app binds that choice
|
|
7
|
+
explicitly in sides.fe.grammar; the catalog resolves its lint package and component contract.
|
|
8
|
+
A missing or ambiguous family never silently resolves to a family. DNA snapshots describe the
|
|
9
|
+
observed implementation; they do not install packages or prove current rendered behavior.
|
|
10
|
+
appliesTo:
|
|
11
|
+
- grammar
|
|
12
|
+
provenance:
|
|
13
|
+
limitations: |
|
|
14
|
+
Component names, tokens and gap tables describe the reference family at observation time.
|
|
15
|
+
Verify actual package identity/version, exported types, CSS and renderer source in the selected
|
|
16
|
+
repository before using snapshot APIs or gaps. Record current source references in the selected
|
|
17
|
+
.starciwork node; an old source census is not current UAT evidence. The current public consumer
|
|
18
|
+
entry observed for FE work is @starci/grammar/common — confirm installed exports before import.
|
|
19
|
+
The `common` entry is a DNA snapshot of that semantic entry and nothing more: it declares no palette,
|
|
20
|
+
no family boundary and no composition playbook, because the entry itself ships none of them.
|
|
21
|
+
guidance:
|
|
22
|
+
- id: catalog-not-default
|
|
23
|
+
requirement: |
|
|
24
|
+
Do not treat this catalog as selecting a default Grammar family. Family choice belongs to the
|
|
25
|
+
current design specification and its explicit sides.fe.grammar declaration.
|
|
26
|
+
rationale: |
|
|
27
|
+
Silent fallback to StarCi would invent product direction and override an absent/ambiguous design.
|
|
28
|
+
- id: verify-before-use
|
|
29
|
+
requirement: |
|
|
30
|
+
Before applying snapshot APIs or recorded gaps, inspect the selected repository's package
|
|
31
|
+
identity/version, exported types, CSS and renderer source.
|
|
32
|
+
rationale: |
|
|
33
|
+
Snapshots are historical; installed exports may differ. Inventing missing APIs is forbidden.
|
|
34
|
+
- id: ui-owns-universal
|
|
35
|
+
requirement: |
|
|
36
|
+
Common semantics, renderer anatomy, state and accessibility remain owned by knowledge/ui.
|
|
37
|
+
A family owns its scoped visual realization and promoted idioms; the product owns its facts,
|
|
38
|
+
content, routes and approved composition.
|
|
39
|
+
Apply FE-RULE-OWNERSHIP in knowledge/patterns/fe/index.yaml when placing requirements and checks.
|
|
40
|
+
rationale: |
|
|
41
|
+
Keeps universal UI law separate from family-scoped DNA and product composition.
|
|
42
|
+
topics:
|
|
43
|
+
- id: common
|
|
44
|
+
path: common/DNA.yaml
|
|
45
|
+
title: Common Grammar — DNA
|
|
46
|
+
summary: |
|
|
47
|
+
Generated DNA snapshot of the `@starci/grammar/common` public entry: custom properties,
|
|
48
|
+
the 110 renderers of its registry, their classes, stamped rule ids and recorded gaps. The DNA owns the observed package
|
|
49
|
+
version and source inventory. Common supplies anatomy, not colour. The snapshot is drift-checked.
|
|
50
|
+
- id: starci
|
|
51
|
+
path: starci/index.yaml
|
|
52
|
+
title: StarCi Core Grammar
|
|
53
|
+
summary: |
|
|
54
|
+
Historical snapshot of the StarCi Core family — DNA, idioms, playbook (including composing the
|
|
55
|
+
0.5.0 forms, overlays and navigation chrome) and family boundaries.
|
|
56
|
+
- id: offset-pop
|
|
57
|
+
path: offset-pop/index.yaml
|
|
58
|
+
title: Offset Pop Grammar
|
|
59
|
+
summary: |
|
|
60
|
+
Snapshot of the product-neutral Offset Pop family — cream canvas, ink outline plus hard offset
|
|
61
|
+
shadow, one pink decision accent with a text-safe accent for text — with DNA, idioms, a
|
|
62
|
+
mobile-first playbook for playful learning or consumer apps, and family boundaries including two
|
|
63
|
+
decisions flagged for the family owner.
|
|
64
|
+
families:
|
|
65
|
+
- id: common
|
|
66
|
+
title: common
|
|
67
|
+
componentContract:
|
|
68
|
+
package: '@starci/grammar'
|
|
69
|
+
entry: '@starci/grammar/common'
|
|
70
|
+
styleEntry: '@starci/grammar/common.css'
|
|
71
|
+
peers: [react, '@heroui/react', '@xyflow/react']
|
|
72
|
+
packageSnapshot: common/DNA.yaml
|
|
73
|
+
scope: |
|
|
74
|
+
DNA snapshot only. This catalog carries no Common-side family boundary, idiom set or composition
|
|
75
|
+
playbook: `@starci/grammar/common` is the semantic/props entry every visual family is defined over
|
|
76
|
+
(`defineGrammarFamily`), not a family that ships a palette, so `authority`, `reusableStyle`,
|
|
77
|
+
`productComposition` and `index` have nothing to name here. Universal semantics, renderer anatomy,
|
|
78
|
+
state and accessibility remain owned by knowledge/ui.
|
|
79
|
+
- id: starci
|
|
80
|
+
title: starci
|
|
81
|
+
contractRef: common
|
|
82
|
+
lint:
|
|
83
|
+
package: '@starci/eslint-grammar-starci'
|
|
84
|
+
factory: starciGrammarConfig
|
|
85
|
+
prefix: starci-grammar
|
|
86
|
+
authority: starci/family.yaml
|
|
87
|
+
packageSnapshot: starci/DNA.yaml
|
|
88
|
+
reusableStyle: starci/idioms.yaml
|
|
89
|
+
productComposition: starci/playbook.yaml
|
|
90
|
+
index: starci/index.yaml
|
|
91
|
+
- id: offset-pop
|
|
92
|
+
title: offset-pop
|
|
93
|
+
contractRef: common
|
|
94
|
+
lint:
|
|
95
|
+
package: '@starci/eslint-grammar-offset-pop'
|
|
96
|
+
factory: offsetPopGrammarConfig
|
|
97
|
+
prefix: starci-grammar
|
|
98
|
+
authority: offset-pop/family.yaml
|
|
99
|
+
packageSnapshot: offset-pop/DNA.yaml
|
|
100
|
+
reusableStyle: offset-pop/idioms.yaml
|
|
101
|
+
productComposition: offset-pop/playbook.yaml
|
|
102
|
+
index: offset-pop/index.yaml
|