@starci/eslint-canon-fe 9.13.16 → 9.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 9.14.1 - 2026-11-05
4
+
5
+ - Changed: the runtime copy of the canon pins names `@starci/hfs` 5.11.1 and the Grammar family 0.21.36.
6
+
7
+ ## 9.14.0 - 2026-11-05
8
+
9
+ - Added (PROJECT-LINT-3): `starciFeConfig` loads the ESLint rules of the app's rule packs (hfs.json `rulePacks`) for the fe side, one plugin per pack, each rule at its severity; a refused pack throws `HFS_RULE_PACK_INVALID`. The runtime copies carry `scripts/lib/rule-packs.mjs`, the `rulePacks` key of the declaration check and the canon pins of `@starci/hfs` 5.11.0.
10
+
3
11
  ## 9.13.16 - 2026-11-05
4
12
 
5
13
  - Changed: the runtime copy of the canon pins names `@starci/hfs` 5.10.4 and the Grammar family 0.21.34.
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@
8
8
  * Published IDs identify their owning law. Namespace migrations are explicit in the removed-vocabulary
9
9
  * catalog; compatibility aliases cannot create a second owner for a rule.
10
10
  */
11
+ import { packEslintConfigs } from "#runtime/scripts/lib/rule-packs.mjs";
11
12
  import { createRequire } from "node:module";
12
13
  import { join } from "node:path";
13
14
  import { pathToFileURL } from "node:url";
@@ -238,14 +239,29 @@ export const starciFeConfig = ({ hfs } = {}) => {
238
239
  throw new Error(`starciFeConfig: these published rules are not requested at error: ${notRequested.join(", ")}`);
239
240
  }
240
241
  const reactHooks = loadReactHooks();
241
- return buildFeConfig({
242
- hfs,
243
- plugin,
244
- reactHooks,
245
- tsPlugin: loadTsPlugin(),
246
- sonar: loadSonarPlugins(),
247
- source: { ...recommended, ...reactHooksRules(reactHooks) },
248
- });
242
+ return [
243
+ ...buildFeConfig({
244
+ hfs,
245
+ plugin,
246
+ reactHooks,
247
+ tsPlugin: loadTsPlugin(),
248
+ sonar: loadSonarPlugins(),
249
+ source: { ...recommended, ...reactHooksRules(reactHooks) },
250
+ }),
251
+ ...withPackRules(hfs, "fe"),
252
+ ];
253
+ };
254
+ /**
255
+ * The ESLint rules of the app's rule packs (hfs.json `rulePacks`, contexts/project/lint.md PROJECT-LINT-3) for one scope: the one sanctioned way an app adds
256
+ * a lint law, loaded by this factory so no tool config of the app defines a rule. A refused pack throws (HFS_RULE_PACK_INVALID): a lint run never guesses.
257
+ *
258
+ * @param hfs - The HFS view the config was built for (its repoRoot is the folder of the eslint.config.mjs).
259
+ * @param side - The scope: the fe side (the root packages take theirs from starciBePackagesConfig).
260
+ * @returns The pack blocks, none when the app declares no pack or the view is missing.
261
+ */
262
+ const withPackRules = (hfs, side) => {
263
+ const root = typeof hfs === "object" && hfs !== null && "repoRoot" in hfs && typeof hfs.repoRoot === "string" ? hfs.repoRoot : null;
264
+ return root === null ? [] : packEslintConfigs(root, side);
249
265
  };
250
266
  /**
251
267
  * The flat config of the front-end subpaths of an app's root workspace packages (`packages/<pkg>/src/workbench/**` and `src/public/**`), for the
@@ -260,6 +276,7 @@ export const starciFePackagesConfig = ({ hfs } = {}) => {
260
276
  const reactHooks = loadReactHooks();
261
277
  const graph = new Set(Object.keys(projectGraphRecommended));
262
278
  const source = { ...Object.fromEntries(Object.entries(recommended).filter(([name]) => !graph.has(name))), ...reactHooksRules(reactHooks) };
279
+ // The pack rules of the root packages come once, with starciBePackagesConfig of the same root config (one plugin per pack).
263
280
  return scopeToPackages(buildFeConfig({ hfs, plugin, reactHooks, tsPlugin: loadTsPlugin(), sonar: loadSonarPlugins(), source }));
264
281
  };
265
282
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starci/eslint-canon-fe",
3
- "version": "9.13.16",
3
+ "version": "9.14.1",
4
4
  "description": "Universal front-end lint laws for TypeScript and Next.js, independent of the selected Grammar family.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -169,6 +169,7 @@
169
169
  "scripts/lib/path-key.mjs",
170
170
  "scripts/lib/read-text.mjs",
171
171
  "scripts/lib/regex.mjs",
172
+ "scripts/lib/rule-packs.mjs",
172
173
  "scripts/lib/same-text.mjs",
173
174
  "scripts/lib/sops-envelope.mjs",
174
175
  "scripts/lib/stack-services.mjs",
@@ -38,7 +38,7 @@ pins:
38
38
  source: packages/next-data/package.json
39
39
  why: 'the SWR data helpers of every front end (fe-stack data, owner:next-data-package): pollingRestart, the viewer and tenant key helpers, viewerSwrConfig and the viewer cache; each operation hook stays in the app and the package holds no component.'
40
40
  '@starci/grammar':
41
- version: 0.21.34
41
+ version: 0.21.36
42
42
  group: starci
43
43
  install: registry
44
44
  side: fe
@@ -52,28 +52,28 @@ pins:
52
52
  source: packages/next-i18n/package.json
53
53
  why: 'the next-intl stack of every front end (fe-stack i18n, owner:next-i18n-package): defineI18n over the locales of the app, the locale-negotiating proxy, the locale-aware navigation, the request config that loads the catalogs of the app and the locale read of the [locale] layout; the app keeps its message catalogs and writes none of the stack.'
54
54
  '@starci/eslint-canon-be':
55
- version: 3.17.4
55
+ version: 3.18.1
56
56
  group: starci
57
57
  install: registry
58
58
  side: be
59
59
  source: packages/eslint-canon-be/package.json
60
60
  why: 'the back-end ESLint canon: `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, and the project graph is built per side; its bundled runtime copies (slots, canon-pins, failure codes, the architecture machine) put .starcistacks and .sops.yaml at the app root; `starciBeConfig({ hfs: loadHfs(import.meta.url) })` is the typed factory and the BE-CONVENTION laws are its rules.'
61
61
  '@starci/eslint-canon-fe':
62
- version: 9.13.16
62
+ version: 9.14.1
63
63
  group: starci
64
64
  install: registry
65
65
  side: fe
66
66
  source: packages/eslint-canon-fe/package.json
67
67
  why: 'the front-end ESLint canon: `loadHfs(import.meta.url)` of fe/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the fe side, and the project graph is built per side; its bundled runtime copies (slots, canon-pins, failure codes, the architecture machine) put .starcistacks and .sops.yaml at the app root; the front end has no tests (FE_NO_TESTS R97 in hfs) and no-vietnamese-in-source (R91) applies.'
68
68
  '@starci/eslint-grammar-core':
69
- version: 0.21.34
69
+ version: 0.21.36
70
70
  group: starci
71
71
  install: registry
72
72
  side: fe
73
73
  source: packages/eslint-grammar-core/package.json
74
74
  why: 'the StarCi Grammar component API lint, versioned with @starci/grammar; front ends compose this family package with the universal FE canon.'
75
75
  '@starci/eslint-grammar-offset-pop':
76
- version: 0.21.34
76
+ version: 0.21.36
77
77
  group: starci
78
78
  install: registry
79
79
  side: fe
@@ -155,7 +155,7 @@ pins:
155
155
  source: packages/mail/package.json
156
156
  why: 'outgoing mail of a back end: a NestJS module and the Mailer port over SMTP (connect, greeting, socket and total timeouts, STARTTLS required by default, recipient, message and attachment checks, a shared SMTP target policy, coded errors, typed escaping render helpers returning { subject, text, html }), with @starci/mail/testing for its specs; the only importer of nodemailer (R90).'
157
157
  '@starci/hfs':
158
- version: 5.10.4
158
+ version: 5.11.1
159
159
  group: starci
160
160
  install: registry
161
161
  side: both
@@ -149,7 +149,9 @@ function observeBlockProblems(observe) {
149
149
  bad.push('observe.reporter must name the optional error reporter');
150
150
  return bad;
151
151
  }
152
- const DECLARATION_KEYS = new Set(['hfs', 'kind', 'project', 'edition', 'db', 'sides', 'browser', 'modules', 'lexicon', 'observe']);
152
+ const DECLARATION_KEYS = new Set(['hfs', 'kind', 'project', 'edition', 'db', 'sides', 'browser', 'modules', 'lexicon', 'observe', 'rulePacks']);
153
+ /** A rule pack hfs.json names (PROJECT-LINT-3): an npm package name, each once. */
154
+ const RULE_PACK_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/u;
153
155
  const RUNTIME_DECLARATION_KEYS = new Set(['hfs', 'kind', 'project']);
154
156
  function productDeclarationProblems(declaration, profiles) {
155
157
  const bad = [];
@@ -159,6 +161,8 @@ function productDeclarationProblems(declaration, profiles) {
159
161
  bad.push(...dbBlockProblems(declaration.db), ...lexiconBlockProblems(declaration.lexicon), ...observeBlockProblems(declaration.observe));
160
162
  if (declaration.browser !== undefined && declaration.browser !== true)
161
163
  bad.push('browser is `true` when the app has a browser journey (slot app.browser), and is left out otherwise');
164
+ if (declaration.rulePacks !== undefined && (!Array.isArray(declaration.rulePacks) || declaration.rulePacks.length === 0 || declaration.rulePacks.some((name) => typeof name !== 'string' || !RULE_PACK_NAME.test(name)) || new Set(declaration.rulePacks).size !== declaration.rulePacks.length))
165
+ bad.push('rulePacks is a non-empty list of the installed rule pack packages the app adds its laws with, each once (PROJECT-LINT-3)');
162
166
  if (declaration.modules !== undefined && declaration.modules !== 'data')
163
167
  bad.push('modules is `"data"` when the app keeps its modules as data (seeds, no rendered registry package), and is left out otherwise');
164
168
  if (declaration.kind !== APP_KIND)
@@ -0,0 +1,243 @@
1
+ // rule-packs.mjs - an app's rule packs (PROJECT-LINT-3): the packages hfs.json `rulePacks` names, each adding the app's own laws to the harness's
2
+ // gates without a local rule in a tool config. A pack is an installed package that exports `./rule-pack`, an ES module whose default export is
3
+ // { schema: 'starci/rule-pack@1', name: '<the package name>', rules: [<rule>, ...] }
4
+ // and every rule carries an id (`NIVO-BLOCK-1`), a finding code (`NIVO_BLOCK_INCOMPLETE`), a severity (error or warning), the spec that proves it
5
+ // (a file of the package) and its doc (a file of the package or an https link), plus exactly one engine:
6
+ // check a machine rule `(context) => findings` of the hfs family: `starci app check` runs it over the tracked files (context.files, context.read)
7
+ // eslint { name, rule, sides } an ESLint rule the canon factories (eslint-canon-be, eslint-canon-fe, the packages config) load for the sides it names
8
+ // A pack only adds: a rule with any other key (an `off`, `overrides` or `disable`), a code the harness already reports, or an id or code another
9
+ // pack uses is refused (HFS_RULE_PACK_INVALID, R381) and the pack lends nothing. Pack findings go through the same policy as the harness's own:
10
+ // a check rule is an hfs finding (blocking on every branch), an eslint rule a lint finding (report under a branch profile, held by the ratchet).
11
+ import fs from 'node:fs';
12
+ import { createRequire } from 'node:module';
13
+ import path from 'node:path';
14
+ export const RULE_PACK_SCHEMA = 'starci/rule-pack@1';
15
+ /** The subpath a pack exports its manifest at. */
16
+ export const RULE_PACK_ENTRY = 'rule-pack';
17
+ /** The sides an eslint pack rule may name: the two side folders and the root packages. */
18
+ export const PACK_SIDES = Object.freeze(['be', 'fe', 'packages']);
19
+ const RULE_KEYS = new Set(['id', 'code', 'severity', 'spec', 'doc', 'check', 'eslint']);
20
+ const ID = /^[A-Z][A-Z0-9]*(?:-[A-Z0-9]+)+$/u;
21
+ const CODE = /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$/u;
22
+ const ESLINT_NAME = /^[a-z][a-z0-9-]*$/u;
23
+ const PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/u;
24
+ const plain = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
25
+ const text = (value) => (typeof value === 'string' && value.trim() !== '' ? value.trim() : null);
26
+ const FAILURE_CODES = new URL('../../modules/kernel/failure-codes.yaml', import.meta.url);
27
+ let harnessCodes = null;
28
+ /**
29
+ * Every code the harness reports (the top-level keys of its failure-code catalog), the codes a pack may not take; empty where the catalog is not
30
+ * shipped (a canon package's runtime copy), where the app check refuses such a pack instead.
31
+ */
32
+ export function reservedCodes() {
33
+ if (harnessCodes === null) {
34
+ let catalog = '';
35
+ try {
36
+ catalog = fs.readFileSync(FAILURE_CODES, 'utf8');
37
+ }
38
+ catch { /* not shipped beside this copy */ }
39
+ harnessCodes = new Set([...catalog.matchAll(/^([A-Z][A-Z0-9_]+):\s*$/gmu)].map((match) => match[1]));
40
+ }
41
+ return harnessCodes;
42
+ }
43
+ /** The app root above `start` (a side folder or the root itself): the nearest folder whose hfs.json declares kind app; null when there is none. */
44
+ export function appRootOf(start) {
45
+ for (let dir = path.resolve(start);; dir = path.dirname(dir)) {
46
+ try {
47
+ const declared = JSON.parse(fs.readFileSync(path.join(dir, 'hfs.json'), 'utf8'));
48
+ if (declared.kind === 'app')
49
+ return dir;
50
+ }
51
+ catch { /* no declaration here: one folder up */ }
52
+ if (path.dirname(dir) === dir)
53
+ return null;
54
+ }
55
+ }
56
+ /** The pack names hfs.json `rulePacks` declares (its shape is judged by the declaration check). */
57
+ export function declaredRulePacks(appRoot) {
58
+ try {
59
+ const declared = JSON.parse(fs.readFileSync(path.join(appRoot, 'hfs.json'), 'utf8'));
60
+ return Array.isArray(declared.rulePacks) ? declared.rulePacks.filter((name) => typeof name === 'string') : [];
61
+ }
62
+ catch {
63
+ return [];
64
+ }
65
+ }
66
+ /** The problems of one rule entry of pack `pack` (whose folder is `root`), or the rule when it is sound. */
67
+ function judgeRule(pack, root, entry, at) {
68
+ const where = `rule ${String(at)}`;
69
+ if (!plain(entry))
70
+ return { problems: [`${where} is not an object`] };
71
+ const problems = [];
72
+ const extra = Object.keys(entry).filter((key) => !RULE_KEYS.has(key));
73
+ if (extra.length > 0)
74
+ problems.push(`${where} has ${extra.join(', ')}: a pack only adds rules, it never turns one off, overrides or lowers it (keys ${[...RULE_KEYS].join(', ')})`);
75
+ const id = text(entry['id']), code = text(entry['code']), spec = text(entry['spec']), doc = text(entry['doc']);
76
+ const label = id ?? where;
77
+ if (id === null || !ID.test(id))
78
+ problems.push(`${where}: id must be an upper-case law id such as NIVO-BLOCK-1`);
79
+ if (code === null || !CODE.test(code))
80
+ problems.push(`${label}: code must be an upper-case finding code such as NIVO_BLOCK_INCOMPLETE`);
81
+ const severity = entry['severity'];
82
+ if (severity !== 'error' && severity !== 'warning')
83
+ problems.push(`${label}: severity must be error or warning`);
84
+ const inPack = (file) => { const resolved = path.resolve(root, file); return resolved.startsWith(`${path.resolve(root)}${path.sep}`) && fs.existsSync(resolved); };
85
+ if (spec === null || !inPack(spec))
86
+ problems.push(`${label}: spec must name the file of the package that proves the rule (${spec ?? 'none'} is not there)`);
87
+ if (doc === null || !(/^https:\/\/\S+$/u.test(doc) || inPack(doc)))
88
+ problems.push(`${label}: doc must be a file of the package or an https link (${doc ?? 'none'})`);
89
+ const check = entry['check'];
90
+ const eslint = entry['eslint'];
91
+ if ((check === undefined) === (eslint === undefined))
92
+ problems.push(`${label}: a rule has exactly one engine, check (a machine rule of the app check) or eslint (a rule the canon factories load)`);
93
+ if (check !== undefined && typeof check !== 'function')
94
+ problems.push(`${label}: check must be a function (context) => findings`);
95
+ let lint;
96
+ if (eslint !== undefined) {
97
+ const name = plain(eslint) ? text(eslint['name']) : null;
98
+ const rule = plain(eslint) ? eslint['rule'] : undefined;
99
+ const sides = plain(eslint) && Array.isArray(eslint['sides']) ? eslint['sides'] : [];
100
+ if (name === null || !ESLINT_NAME.test(name))
101
+ problems.push(`${label}: eslint.name must be a kebab-case rule name`);
102
+ if (!plain(rule) || typeof rule['create'] !== 'function')
103
+ problems.push(`${label}: eslint.rule must be an ESLint rule module with create()`);
104
+ if (sides.length === 0 || sides.some((side) => !PACK_SIDES.includes(side)))
105
+ problems.push(`${label}: eslint.sides must name some of ${PACK_SIDES.join(', ')}`);
106
+ if (name !== null && plain(rule))
107
+ lint = { name, rule: rule, sides: sides };
108
+ }
109
+ if (problems.length > 0)
110
+ return { problems };
111
+ return { rule: { pack, id: id, code: code, severity: severity, spec: spec, doc: doc, ...(typeof check === 'function' ? { check: check } : {}), ...(lint === undefined ? {} : { eslint: lint }) }, problems: [] };
112
+ }
113
+ /**
114
+ * The rule packs of the app at `appRoot`, loaded through the app's own install (a pack is an ES module required synchronously, so a pack has no
115
+ * top-level await) and judged. `reserved` holds the codes the harness reports: a pack never reuses one.
116
+ */
117
+ export function loadRulePacks(appRoot, reserved = reservedCodes()) {
118
+ const result = { rules: [], problems: [] };
119
+ const names = declaredRulePacks(appRoot);
120
+ const ids = new Map(), codes = new Map();
121
+ const requireHere = createRequire(path.join(appRoot, 'package.json'));
122
+ for (const name of names) {
123
+ const fail = (message) => { result.problems.push({ pack: name, message }); };
124
+ if (!PACKAGE_NAME.test(name)) {
125
+ fail('is not a package name');
126
+ continue;
127
+ }
128
+ let manifest, root;
129
+ try {
130
+ root = path.dirname(requireHere.resolve(`${name}/package.json`));
131
+ const loaded = requireHere(`${name}/${RULE_PACK_ENTRY}`);
132
+ manifest = loaded.default ?? loaded;
133
+ }
134
+ catch (error) {
135
+ fail(`cannot be loaded from the app's install (${name}/${RULE_PACK_ENTRY}): ${String(error?.message ?? error).split('\n')[0]}`);
136
+ continue;
137
+ }
138
+ if (!plain(manifest) || manifest['schema'] !== RULE_PACK_SCHEMA) {
139
+ fail(`its ${RULE_PACK_ENTRY} export is not a ${RULE_PACK_SCHEMA} manifest`);
140
+ continue;
141
+ }
142
+ if (manifest['name'] !== name) {
143
+ fail(`its manifest names ${String(manifest['name'])}, not the package`);
144
+ continue;
145
+ }
146
+ const extra = Object.keys(manifest).filter((key) => !['schema', 'name', 'rules'].includes(key));
147
+ if (extra.length > 0) {
148
+ fail(`its manifest has ${extra.join(', ')}: a pack holds only schema, name and the rules it adds`);
149
+ continue;
150
+ }
151
+ const entries = Array.isArray(manifest['rules']) ? manifest['rules'] : [];
152
+ if (entries.length === 0) {
153
+ fail('adds no rule');
154
+ continue;
155
+ }
156
+ const judged = entries.map((entry, at) => judgeRule(name, root, entry, at));
157
+ const problems = judged.flatMap((one) => one.problems);
158
+ const sound = judged.flatMap((one) => (one.rule === undefined ? [] : [one.rule]));
159
+ for (const [at, rule] of sound.entries()) {
160
+ if (sound.findIndex((other) => other.id === rule.id) < at)
161
+ problems.push(`${rule.id}: the id is also a rule of ${name}`);
162
+ if (sound.findIndex((other) => other.code === rule.code) < at)
163
+ problems.push(`${rule.id}: ${rule.code} is also a code of ${name}`);
164
+ if (reserved.has(rule.code))
165
+ problems.push(`${rule.id}: ${rule.code} is a code the harness reports; a pack adds its own codes and never stands in for a harness rule`);
166
+ const otherId = ids.get(rule.id), otherCode = codes.get(rule.code);
167
+ if (otherId !== undefined)
168
+ problems.push(`${rule.id}: the id is also a rule of ${otherId}`);
169
+ if (otherCode !== undefined)
170
+ problems.push(`${rule.id}: ${rule.code} is also a code of ${otherCode}`);
171
+ }
172
+ if (problems.length > 0) {
173
+ for (const message of problems)
174
+ fail(message);
175
+ continue;
176
+ }
177
+ for (const rule of sound) {
178
+ ids.set(rule.id, name);
179
+ codes.set(rule.code, name);
180
+ result.rules.push(rule);
181
+ }
182
+ }
183
+ return result;
184
+ }
185
+ /** One plugin object per pack and rule set, so two blocks of one config that name a pack hand ESLint the same plugin. */
186
+ const plugins = new Map();
187
+ const pluginOf = (pack, rules) => {
188
+ const key = `${pack}|${rules.map((rule) => rule.id).join(',')}`;
189
+ let made = plugins.get(key);
190
+ if (made === undefined) {
191
+ made = { rules: Object.fromEntries(rules.map((rule) => [rule.eslint?.name, rule.eslint?.rule])) };
192
+ plugins.set(key, made);
193
+ }
194
+ return made;
195
+ };
196
+ /**
197
+ * The flat-config blocks that load the eslint rules of the app's packs for `side`, for a canon factory: one plugin per pack (named by the package)
198
+ * and each rule at its severity. Throws when a pack is refused: a lint run never guesses which of an app's laws hold.
199
+ */
200
+ export function packEslintConfigs(start, side, files = ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']) {
201
+ const appRoot = appRootOf(start);
202
+ if (appRoot === null || declaredRulePacks(appRoot).length === 0)
203
+ return [];
204
+ const packs = loadRulePacks(appRoot);
205
+ if (packs.problems.length > 0)
206
+ throw new Error(`HFS_RULE_PACK_INVALID: ${packs.problems.map((problem) => `${problem.pack} ${problem.message}`).join('; ')} (hfs.json rulePacks, PROJECT-LINT-3)`);
207
+ const byPack = new Map();
208
+ for (const rule of packs.rules)
209
+ if (rule.eslint?.sides.includes(side))
210
+ byPack.set(rule.pack, [...(byPack.get(rule.pack) ?? []), rule]);
211
+ return [...byPack].map(([pack, rules]) => ({
212
+ files: [...files],
213
+ plugins: { [pack]: pluginOf(pack, rules) },
214
+ rules: Object.fromEntries(rules.map((rule) => [`${pack}/${rule.eslint?.name}`, rule.severity === 'error' ? 'error' : 'warn'])),
215
+ }));
216
+ }
217
+ /** The findings of the check rules of the app's packs over `files`, and one HFS_RULE_PACK_INVALID per refused pack or per rule that threw. */
218
+ export function rulePackFindings({ repoRoot, files, reserved, read }) {
219
+ const packs = loadRulePacks(repoRoot, reserved);
220
+ const out = packs.problems.map((problem) => ({ code: RULE_PACK_INVALID, level: 'error', path: 'hfs.json', message: `hfs.json rulePacks: ${problem.pack} ${problem.message} (R381, PROJECT-LINT-3)` }));
221
+ const context = Object.freeze({ repoRoot, files: Object.freeze([...files]), read });
222
+ for (const rule of packs.rules) {
223
+ if (rule.check === undefined)
224
+ continue;
225
+ let found;
226
+ try {
227
+ found = rule.check(context);
228
+ }
229
+ catch (error) {
230
+ out.push({ code: RULE_PACK_INVALID, level: 'error', path: 'hfs.json', message: `hfs.json rulePacks: ${rule.pack} ${rule.id} threw (${String(error?.message ?? error).split('\n')[0]}); a rule that cannot judge is no rule (R381)` });
231
+ continue;
232
+ }
233
+ for (const finding of Array.isArray(found) ? found : []) {
234
+ if (!plain(finding) || text(finding.path) === null || text(finding.message) === null) {
235
+ out.push({ code: RULE_PACK_INVALID, level: 'error', path: 'hfs.json', message: `hfs.json rulePacks: ${rule.pack} ${rule.id} returned a finding without a path and a message (R381)` });
236
+ continue;
237
+ }
238
+ out.push({ code: rule.code, level: rule.severity, path: String(finding.path), message: `${String(finding.message)} (${rule.id}, ${rule.doc})`, ...(typeof finding.line === 'number' ? { line: finding.line } : {}), ...(typeof finding.column === 'number' ? { column: finding.column } : {}) });
239
+ }
240
+ }
241
+ return out;
242
+ }
243
+ export const RULE_PACK_INVALID = 'HFS_RULE_PACK_INVALID';