@starci/eslint-canon-be 3.0.7 → 3.0.8
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/README.md +1 -1
- package/lib/hfs.mjs +3 -3
- package/package.json +3 -3
- package/project-graph.mjs +4 -4
- package/runtime/engine/runtime-root.mjs +5 -0
- package/runtime/engine/yaml.mjs +3 -3
- package/runtime/knowledge/hfs/canon-pins.yaml +10 -10
- package/runtime/knowledge/hfs/slots.yaml +5 -5
- package/runtime/knowledge/patterns/fe/folder.yaml +2 -2
- package/runtime/knowledge/sonar-gate.yaml +2 -2
- package/runtime/scripts/api/git/lib.mjs +1 -1
- package/runtime/scripts/api/git/recorded-lines.mjs +19 -0
- package/runtime/scripts/api/sops/decrypt.mjs +1 -1
- package/runtime/scripts/{lib/hfs-allows.mjs → hfs/allows.mjs} +3 -3
- package/runtime/scripts/{checks → hfs}/architecture/backend.mjs +1 -1
- package/runtime/scripts/{checks → hfs}/architecture/config.mjs +2 -2
- package/runtime/scripts/{checks → hfs}/architecture/connection-map.mjs +1 -1
- package/runtime/scripts/{checks → hfs}/architecture/contract-fixture-guard.mjs +1 -1
- package/runtime/scripts/{checks → hfs}/architecture/fe-slot-allows.mjs +3 -3
- package/runtime/scripts/{checks → hfs}/architecture/feature-shape.mjs +1 -1
- package/runtime/scripts/{checks → hfs}/architecture/hfs-graph.mjs +1 -1
- package/runtime/scripts/{checks → hfs}/architecture/hfs.mjs +3 -3
- package/runtime/scripts/hfs/architecture/next-data-contract.mjs +96 -0
- package/runtime/scripts/{checks → hfs}/architecture/next-data.mjs +18 -105
- package/runtime/scripts/{checks → hfs}/architecture/required-files.mjs +1 -1
- package/runtime/scripts/{checks → hfs}/architecture/surface.mjs +1 -1
- package/runtime/scripts/{checks → hfs}/architecture/test-world-files.mjs +2 -2
- package/runtime/scripts/{checks → hfs}/architecture/typescript.mjs +1 -1
- package/runtime/scripts/hfs/manifest-shape.mjs +170 -0
- package/runtime/scripts/{lib/hfs-path-findings.mjs → hfs/path-findings.mjs} +6 -6
- package/runtime/scripts/{lib → hfs}/project-graph.mjs +0 -0
- package/runtime/scripts/{lib → hfs}/project-rule.mjs +3 -3
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/secrets.mjs +2 -2
- package/runtime/scripts/{lib/hfs-slots.mjs → hfs/slots.mjs} +52 -58
- package/runtime/scripts/{lib/hfs-view.mjs → hfs/view.mjs} +3 -3
- package/runtime/scripts/lib/language.mjs +2 -2
- package/runtime/scripts/lib/line-count.mjs +9 -0
- package/runtime/scripts/lib/path-key.mjs +2 -0
- package/runtime/scripts/{checks/common.mjs → lib/walk.mjs} +1 -12
- package/size-budget.mjs +2 -1
- package/runtime/scripts/lib/recorded-lines.mjs +0 -27
- /package/runtime/scripts/{checks → hfs}/architecture/background-unowned.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/client-reaches-server.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/clones.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/config-unread.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/constructor-deps.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/contracts.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/cross-app-duplicate.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/dead-exports.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/default-deny.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/doc-language.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/entrypoint.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/error-codes.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/error-masked.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/framework-pinned.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/frontend.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/hooks-are-hooks.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/i18n-keys.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/index.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/injection-token-exported.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/machine-ast.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/module-per-transport.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/owners.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/package-shape.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/reachability.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/register-once.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/registration.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/route-files-thin.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/schema-owner.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/source-names.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/sql-owner.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/sql-tokens.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/symbols.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/tiers.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/transport-owner.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/unit-spec-providers.mjs +0 -0
- /package/runtime/scripts/{lib → hfs}/repo-identity.mjs +0 -0
- /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/fe-no-tests.mjs +0 -0
- /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/read.mjs +0 -0
- /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/spec-placement.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/typescript-programs.mjs +0 -0
|
@@ -7,22 +7,32 @@
|
|
|
7
7
|
// Every check and lint rule of HFS reads knowledge/hfs/slots.yaml through this module; none keeps its own path
|
|
8
8
|
// list. The manifest shape is modules/schemas/hfs-slots.schema.yaml and hfs.json is modules/schemas/hfs-repo.schema.yaml;
|
|
9
9
|
// the installed runtime carries no npm dependency, so this file re-states those shapes instead of loading ajv
|
|
10
|
-
// (tests/hfs-slots.spec.mjs proves the two agree).
|
|
10
|
+
// (tests/hfs/hfs-slots.spec.mjs proves the two agree).
|
|
11
11
|
//
|
|
12
12
|
// A product is ONE app repository: hfs.json at the app root has kind `app` and declares its two sides, `be` and `fe`, each in
|
|
13
13
|
// the folder of that name. The resolver of the app answers for the whole tree: a root path with the slots of profile `app`,
|
|
14
14
|
// a path below a side folder with that side's resolver (profile be or fe, the side folder as its root: the side view), so
|
|
15
15
|
// every check and lint rule runs unchanged with a side folder as its repository root. Nothing crosses sides except the
|
|
16
16
|
// paths the declaration lists in `sides.<side>.reads` (a subset of the manifest's `sides.<side>.reads`).
|
|
17
|
+
//
|
|
18
|
+
// One loader, two manifest kinds. `kind: app` (the default; knowledge/hfs/slots.yaml) is the product standard above.
|
|
19
|
+
// `kind: runtime` (knowledge/hfs/runtime-slots.yaml, schema starci/runtime-slots@<major>) is the standard of the StarCi
|
|
20
|
+
// runtime repository itself: one profile `runtime`, no sides and no app kinds, slot ids runtime.<name>, the tracked value
|
|
21
|
+
// `generated` (a copy written only by the slot's `generatedBy`), and a top-level `pending` list, the one shrink-only
|
|
22
|
+
// allowlist of the runtime check (scripts/hfs/runtime-check.mjs). A runtime repository declares itself with
|
|
23
|
+
// hfs.json {"hfs": <major>, "kind": "runtime", "project": <name>}.
|
|
17
24
|
import fs from 'node:fs';
|
|
18
25
|
import path from 'node:path';
|
|
19
26
|
import { skillRoot } from '../../engine/runtime-root.mjs';
|
|
20
27
|
import { parseYaml } from '../../engine/yaml.mjs';
|
|
21
|
-
import { braceVariants, globExpression } from '
|
|
22
|
-
import { posixPath } from '
|
|
28
|
+
import { braceVariants, globExpression } from '../lib/glob.mjs';
|
|
29
|
+
import { posixPath } from '../lib/path-key.mjs';
|
|
23
30
|
import { isPlainObject } from '../../engine/plain-object.mjs';
|
|
31
|
+
import { APP_KIND, ENV_PREFIX, MANIFEST_KINDS, NAME, PRESENCE, RUNTIME_KIND, SEMVER, SLOT_ID, TESTS, TRACKED, manifestKind, runtimeSemanticProblems, runtimeShapeProblems, slotProblems, tierMapProblems } from './manifest-shape.mjs';
|
|
24
32
|
|
|
25
33
|
export const HFS_MANIFEST_FILE = 'knowledge/hfs/slots.yaml';
|
|
34
|
+
/** The manifest of kind runtime: the standard tree of the StarCi runtime repository (judged by scripts/hfs/runtime-check.mjs). */
|
|
35
|
+
export const RUNTIME_MANIFEST_FILE = 'knowledge/hfs/runtime-slots.yaml';
|
|
26
36
|
export const HFS_DECLARATION_FILE = 'hfs.json';
|
|
27
37
|
|
|
28
38
|
/** A refusal with a catalogued code (modules/kernel/failure-codes.yaml) and the facts a check reports. */
|
|
@@ -35,13 +45,6 @@ export class HfsSlotsError extends Error {
|
|
|
35
45
|
}
|
|
36
46
|
}
|
|
37
47
|
|
|
38
|
-
const SEMVER = /^(\d+)\.(\d+)\.(\d+)$/;
|
|
39
|
-
const NAME = /^[a-z][a-z0-9-]*$/;
|
|
40
|
-
const ENV_PREFIX = /^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$/;
|
|
41
|
-
const SLOT_ID = /^(app|repo|be|fe)\.[a-z0-9-]+(\.[a-z0-9-]+)*$/;
|
|
42
|
-
const PRESENCE = ['required', 'optional', 'opt-in', 'forbidden'];
|
|
43
|
-
const TRACKED = ['tracked', 'ignored', 'external'];
|
|
44
|
-
const TESTS = ['unit-beside', 'e2e', 'none'];
|
|
45
48
|
/** The sides of an app; each lives in the folder of its name and is the profile of its slots. */
|
|
46
49
|
const PROFILES = ['be', 'fe'];
|
|
47
50
|
export const SIDES = Object.freeze([...PROFILES]);
|
|
@@ -67,8 +70,6 @@ export function appRelativeMessages(side, sideRoot) {
|
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
const SCOPES = [APP_SCOPE, ...PROFILES];
|
|
70
|
-
const APP_KIND = 'app';
|
|
71
|
-
const strList = (v) => Array.isArray(v) && v.every((s) => typeof s === 'string' && s.length > 0);
|
|
72
73
|
|
|
73
74
|
// ------------------------------------------------------------------------------------------------ patterns
|
|
74
75
|
|
|
@@ -151,7 +152,9 @@ const fail = (code, message, details) => { throw new HfsSlotsError(code, message
|
|
|
151
152
|
function manifestShapeProblems(m) {
|
|
152
153
|
const bad = [];
|
|
153
154
|
if (!isPlainObject(m)) return ['the manifest is not a map'];
|
|
154
|
-
|
|
155
|
+
if (!MANIFEST_KINDS.includes(manifestKind(m))) return [`kind must be one of ${MANIFEST_KINDS.join(', ')}`];
|
|
156
|
+
if (manifestKind(m) === RUNTIME_KIND) return runtimeShapeProblems(m);
|
|
157
|
+
const allowed = new Set(['schema', 'kind', 'version', 'versioning', 'presenceValues', 'trackedValues', 'testValues', 'sides', 'appKinds', 'tiers', 'ruleParams', 'crossOwner', 'crossApp', 'slots', 'consumers']);
|
|
155
158
|
for (const key of Object.keys(m)) if (!allowed.has(key)) bad.push(`unknown top-level key ${key}`);
|
|
156
159
|
if (!/^starci\/hfs-slots@\d+$/.test(String(m.schema))) bad.push('schema must be starci/hfs-slots@<major>');
|
|
157
160
|
if (!SEMVER.test(String(m.version))) bad.push('version must be MAJOR.MINOR.PATCH');
|
|
@@ -174,12 +177,7 @@ function manifestShapeProblems(m) {
|
|
|
174
177
|
if (kinds !== undefined && (!Array.isArray(kinds) || !kinds.length || !kinds.every((k) => NAME.test(String(k))) || new Set(kinds).size !== kinds.length)) bad.push(`appKinds.${profile} must be a non-empty list of unique names`);
|
|
175
178
|
const tiers = m.tiers?.[profile];
|
|
176
179
|
if (tiers === undefined) continue;
|
|
177
|
-
|
|
178
|
-
for (const [tier, def] of Object.entries(tiers)) {
|
|
179
|
-
if (!NAME.test(tier)) bad.push(`tiers.${profile}.${tier} is not a tier name`);
|
|
180
|
-
if (!isPlainObject(def) || !Array.isArray(def.mayImport) || !def.mayImport.every((t) => NAME.test(String(t)))) bad.push(`tiers.${profile}.${tier}.mayImport must be a list of tier names`);
|
|
181
|
-
for (const key of Object.keys(def ?? {})) if (!['mayImport', 'acyclic', 'lowerLayerOnly'].includes(key)) bad.push(`tiers.${profile}.${tier}.${key} is not a tier field`);
|
|
182
|
-
}
|
|
180
|
+
bad.push(...tierMapProblems(profile, tiers));
|
|
183
181
|
}
|
|
184
182
|
const blockOk = (v) => isPlainObject(v) && Number.isInteger(v.lines) && v.lines >= 2 && Number.isInteger(v.tokens) && v.tokens >= 1 && Object.keys(v).length === 2;
|
|
185
183
|
const fileLinesOk = (v) => isPlainObject(v) && Number.isInteger(v.soft) && v.soft >= 1 && typeof v.hardGrowth === 'boolean' && Object.keys(v).length === 2;
|
|
@@ -204,37 +202,7 @@ function manifestShapeProblems(m) {
|
|
|
204
202
|
if (!fileLinesOk(rp.fe.fileLines) || !blockOk(rp.fe.duplicateBlock) || Object.keys(rp.fe).length !== 2) bad.push('ruleParams.fe needs fileLines {soft, hardGrowth} and duplicateBlock {lines >= 2, tokens >= 1}');
|
|
205
203
|
}
|
|
206
204
|
if (!Array.isArray(m.slots) || !m.slots.length) { bad.push('slots must be a non-empty list'); return bad; }
|
|
207
|
-
|
|
208
|
-
m.slots.forEach((slot, index) => {
|
|
209
|
-
const at = isPlainObject(slot) && typeof slot.id === 'string' ? `slot ${slot.id}` : `slots[${index}]`;
|
|
210
|
-
if (!isPlainObject(slot)) { bad.push(`${at} is not a map`); return; }
|
|
211
|
-
for (const key of Object.keys(slot)) if (!slotKeys.has(key)) bad.push(`${at}: unknown field ${key}`);
|
|
212
|
-
if (!SLOT_ID.test(String(slot.id))) bad.push(`${at}: id must look like be.transport.http`);
|
|
213
|
-
if (!Array.isArray(slot.profiles) || !slot.profiles.length || !slot.profiles.every((p) => SCOPES.includes(p)) || new Set(slot.profiles).size !== slot.profiles.length || (slot.profiles.includes(APP_SCOPE) && slot.profiles.length !== 1)) bad.push(`${at}: profiles must be [app] or a unique non-empty subset of be, fe`);
|
|
214
|
-
else if ((slot.profiles[0] === APP_SCOPE) !== String(slot.id).startsWith(`${APP_SCOPE}.`)) bad.push(`${at}: an app-root slot has profiles [app] and an id app.<name>, and only it`);
|
|
215
|
-
if (typeof slot.path !== 'string' || !slot.path) bad.push(`${at}: path is required`);
|
|
216
|
-
if (!PRESENCE.includes(slot.presence)) bad.push(`${at}: presence must be one of ${PRESENCE.join(', ')}`);
|
|
217
|
-
if (!TRACKED.includes(slot.tracked)) bad.push(`${at}: tracked must be one of ${TRACKED.join(', ')}`);
|
|
218
|
-
if (!NAME.test(String(slot.tier))) bad.push(`${at}: tier must be a tier name, none or inherit`);
|
|
219
|
-
if (!TESTS.includes(slot.tests)) bad.push(`${at}: tests must be one of ${TESTS.join(', ')}`);
|
|
220
|
-
if (slot.owner !== undefined && typeof slot.owner !== 'boolean') bad.push(`${at}: owner must be a boolean`);
|
|
221
|
-
if (slot.appKind !== undefined && !NAME.test(String(slot.appKind))) bad.push(`${at}: appKind must be a name`);
|
|
222
|
-
if (slot.minInstances !== undefined && !(Number.isInteger(slot.minInstances) && slot.minInstances >= 1)) bad.push(`${at}: minInstances must be a positive integer`);
|
|
223
|
-
if (slot.requiredWhen !== undefined && slot.requiredWhen !== 'connections') bad.push(`${at}: requiredWhen may only be connections`);
|
|
224
|
-
if (slot.requiredInstances !== undefined && !(isPlainObject(slot.requiredInstances) && Object.values(slot.requiredInstances).every((v) => strList(v) && v.length))) bad.push(`${at}: requiredInstances must map a variable to a non-empty list of names`);
|
|
225
|
-
for (const key of ['requires', 'allows', 'forbids', 'layers', 'kinds']) if (slot[key] !== undefined && !strList(slot[key])) bad.push(`${at}: ${key} must be a list of strings`);
|
|
226
|
-
if (slot.kinds !== undefined && strList(slot.kinds) && (!slot.kinds.length || new Set(slot.kinds).size !== slot.kinds.length || slot.kinds.some((k) => !NAME.test(k) || (slot.layers ?? []).includes(k)))) bad.push(`${at}: kinds must be a non-empty list of unique folder names that are not layers`);
|
|
227
|
-
if (slot.roles !== undefined && !(isPlainObject(slot.roles) && Object.keys(slot.roles).length && Object.entries(slot.roles).every(([role, file]) => NAME.test(role) && typeof file === 'string' && file && !file.includes('/')))) bad.push(`${at}: roles must map a role name to a file name`);
|
|
228
|
-
if (slot.composedBy !== undefined && !(strList(slot.composedBy) && slot.composedBy.length && new Set(slot.composedBy).size === slot.composedBy.length)) bad.push(`${at}: composedBy must be a non-empty list of unique app kinds`);
|
|
229
|
-
if (slot.budget !== undefined && !(isPlainObject(slot.budget) && Object.keys(slot.budget).length && Object.values(slot.budget).every((v) => Number.isInteger(v) && v >= 1))) bad.push(`${at}: budget must map names to positive integers`);
|
|
230
|
-
if (slot.managedBy !== undefined && !NAME.test(String(slot.managedBy))) bad.push(`${at}: managedBy must be a template id`);
|
|
231
|
-
if (slot.rules !== undefined && !(Array.isArray(slot.rules) && slot.rules.every((r) => /^[A-Z][A-Z0-9_]*\*?$/.test(String(r))) && new Set(slot.rules).size === slot.rules.length)) bad.push(`${at}: rules must be unique rule ids`);
|
|
232
|
-
if (slot.since !== undefined && !SEMVER.test(String(slot.since))) bad.push(`${at}: since must be a version`);
|
|
233
|
-
if (slot.retiredIn !== undefined && !(Number.isInteger(slot.retiredIn) && slot.retiredIn >= 2)) bad.push(`${at}: retiredIn must be a major`);
|
|
234
|
-
if (slot.retiredIn !== undefined && typeof slot.successor !== 'string') bad.push(`${at}: a retired slot names its successor`);
|
|
235
|
-
if (slot.tracked === 'external' && (typeof slot.goesTo !== 'string' || slot.presence !== 'forbidden')) bad.push(`${at}: an external slot is forbidden and says where it goes (goesTo)`);
|
|
236
|
-
if (slot.presence === 'forbidden' && slot.tracked !== 'external') bad.push(`${at}: a forbidden slot is external`);
|
|
237
|
-
});
|
|
205
|
+
m.slots.forEach((slot, index) => bad.push(...slotProblems(slot, index, APP_KIND, { appScope: APP_SCOPE, scopes: SCOPES })));
|
|
238
206
|
return bad;
|
|
239
207
|
}
|
|
240
208
|
|
|
@@ -277,6 +245,7 @@ function manifestSemanticProblems(m) {
|
|
|
277
245
|
for (const kind of slot.composedBy ?? []) if (!slot.profiles.every((p) => m.appKinds[p].includes(kind))) bad.push(`slot ${slot.id}: composedBy names ${kind}, which is not an app kind of every profile of the slot`);
|
|
278
246
|
if (slot.layers !== undefined && slot.tier !== 'none' && !slot.profiles.every((p) => m.tiers[p][slot.tier]?.lowerLayerOnly)) bad.push(`slot ${slot.id}: layers need a lowerLayerOnly tier`);
|
|
279
247
|
}
|
|
248
|
+
if (manifestKind(m) === RUNTIME_KIND) return [...bad, ...runtimeSemanticProblems(m)];
|
|
280
249
|
for (const side of PROFILES) for (const read of m.sides[side].reads) {
|
|
281
250
|
const [owner, ...rest] = read.split('/');
|
|
282
251
|
const below = rest.join('/');
|
|
@@ -317,9 +286,16 @@ export function loadSlotManifest({ root = skillRoot, file = path.join(root, HFS_
|
|
|
317
286
|
function declarationShapeProblems(d) {
|
|
318
287
|
const bad = [];
|
|
319
288
|
if (!isPlainObject(d)) return ['hfs.json is not an object'];
|
|
289
|
+
if (d.kind === RUNTIME_KIND) {
|
|
290
|
+
// The StarCi runtime repository: no sides and no apps; its tree is the runtime manifest's.
|
|
291
|
+
for (const key of Object.keys(d)) if (!['hfs', 'kind', 'project'].includes(key)) bad.push(`unknown key ${key} (a runtime declaration is {hfs, kind, project})`);
|
|
292
|
+
if (!(Number.isInteger(d.hfs) && d.hfs >= 1)) bad.push('hfs must be the pinned manifest major (an integer, 1 or more)');
|
|
293
|
+
if (!NAME.test(String(d.project))) bad.push('project must be a project name');
|
|
294
|
+
return bad;
|
|
295
|
+
}
|
|
320
296
|
for (const key of Object.keys(d)) if (!['hfs', 'kind', 'project', 'sides'].includes(key)) bad.push(`unknown key ${key}`);
|
|
321
297
|
if (!(Number.isInteger(d.hfs) && d.hfs >= 1)) bad.push('hfs must be the pinned manifest major (an integer, 1 or more)');
|
|
322
|
-
if (d.kind !== APP_KIND) bad.push(`kind must be ${APP_KIND}
|
|
298
|
+
if (d.kind !== APP_KIND) bad.push(`kind must be ${APP_KIND} (a product is one app repository with a be and an fe side) or ${RUNTIME_KIND} (the StarCi runtime repository)`);
|
|
323
299
|
if (!NAME.test(String(d.project))) bad.push('project must be a project name');
|
|
324
300
|
if (!isPlainObject(d.sides) || Object.keys(d.sides).sort().join() !== PROFILES.join()) { bad.push('sides must declare exactly be and fe'); return bad; }
|
|
325
301
|
const names = new Map();
|
|
@@ -382,8 +358,24 @@ function sideProblems(manifest, side, s) {
|
|
|
382
358
|
export function resolveRepoDeclaration(manifest, declaration, { file = HFS_DECLARATION_FILE, side = null } = {}) {
|
|
383
359
|
const problems = declarationShapeProblems(declaration);
|
|
384
360
|
if (problems.length) declarationInvalid(problems, file);
|
|
361
|
+
if (declaration.kind !== manifestKind(manifest)) declarationInvalid([`hfs.json is of kind ${declaration.kind}, but the manifest it is judged by is of kind ${manifestKind(manifest)}`], file);
|
|
385
362
|
if (declaration.hfs !== manifest.major)
|
|
386
363
|
fail('HFS_MANIFEST_MAJOR_MISMATCH', `hfs.json pins manifest major ${declaration.hfs} but the manifest is ${manifest.version}`, { pinned: declaration.hfs, manifest: manifest.version, manifestMajor: manifest.major, file });
|
|
364
|
+
if (declaration.kind === RUNTIME_KIND) {
|
|
365
|
+
if (side !== null) fail('HFS_DECLARATION_INVALID', 'a runtime repository has no sides', { file, side });
|
|
366
|
+
return Object.freeze({
|
|
367
|
+
hfs: declaration.hfs,
|
|
368
|
+
kind: RUNTIME_KIND,
|
|
369
|
+
project: declaration.project,
|
|
370
|
+
side: null,
|
|
371
|
+
profile: RUNTIME_KIND,
|
|
372
|
+
apps: Object.freeze([]),
|
|
373
|
+
optionalSlots: Object.freeze([]),
|
|
374
|
+
connections: Object.freeze([]),
|
|
375
|
+
reads: Object.freeze([]),
|
|
376
|
+
manifestVersion: manifest.version,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
387
379
|
const bad = PROFILES.flatMap((name) => sideProblems(manifest, name, declaration.sides[name]));
|
|
388
380
|
if (bad.length) declarationInvalid(bad, file);
|
|
389
381
|
if (side !== null && !PROFILES.includes(side)) fail('HFS_DECLARATION_INVALID', `${side} is not a side of an app (be, fe)`, { file, side });
|
|
@@ -599,7 +591,8 @@ function createScopeResolver(manifest, repo) {
|
|
|
599
591
|
const b = layerIndex(byId.get((toOwner ?? to).slot), toOwner?.root ?? to.root);
|
|
600
592
|
if (a >= 0 && b >= 0 && b <= a) return { allowed: false, reason: 'layerOrder', fromLayer: fromSlot.layers[a], toLayer: fromSlot.layers[b] };
|
|
601
593
|
}
|
|
602
|
-
|
|
594
|
+
// A runtime owner is imported file by file (crossOwner of knowledge/hfs/runtime-slots.yaml): no public entry.
|
|
595
|
+
if (toOwner && repo.kind !== RUNTIME_KIND) {
|
|
603
596
|
const relative = to.path === toOwner.root ? '' : to.path.slice(toOwner.root.length + 1);
|
|
604
597
|
const ownerTier = byId.get(toOwner.slot).tier;
|
|
605
598
|
const entry = isEntryFile(relative) || (ownerTier === 'package' && relative === 'src/index.ts') || (ownerTier === 'app' && relative === 'app.module.ts');
|
|
@@ -763,9 +756,10 @@ export function createSlotResolver(manifest, repo) {
|
|
|
763
756
|
});
|
|
764
757
|
}
|
|
765
758
|
|
|
766
|
-
/** The rule parameters of one profile (be: fileLines, duplicateBlock, infraOwners, suffixes, bannedSuffixes; fe: fileLines, duplicateBlock), as a frozen deep copy. */
|
|
759
|
+
/** The rule parameters of one profile (be: fileLines, duplicateBlock, infraOwners, suffixes, bannedSuffixes; fe: fileLines, duplicateBlock; runtime: ruleParams.runtime of a runtime manifest), as a frozen deep copy. */
|
|
767
760
|
export function ruleParams(manifest, profile) {
|
|
768
|
-
|
|
761
|
+
const profiles = manifestKind(manifest) === RUNTIME_KIND ? [RUNTIME_KIND] : PROFILES;
|
|
762
|
+
if (!profiles.includes(profile)) fail('HFS_MANIFEST_INVALID', `ruleParams has no profile ${profile}`, { profile });
|
|
769
763
|
const deepFreeze = (v) => { if (v && typeof v === 'object') Object.values(v).forEach(deepFreeze); return Object.freeze(v); };
|
|
770
764
|
return deepFreeze(structuredClone(manifest.ruleParams[profile]));
|
|
771
765
|
}
|
|
@@ -782,12 +776,12 @@ export function openHfs({ root = skillRoot, repoRoot, declaration, side = null,
|
|
|
782
776
|
// ------------------------------------------------------------------------------------------ rule catalog
|
|
783
777
|
|
|
784
778
|
export const HFS_RULES_FILE = 'knowledge/hfs/rules.yaml';
|
|
785
|
-
export const RULE_GATES = Object.freeze(['pre-commit', 'pre-push', 'settle', 'land', 'ci', 'sonar']);
|
|
786
|
-
export const ENFORCER_FAMILIES = Object.freeze(['eslint-be', 'eslint-fe', 'stylelint', 'machine', 'hfs', 'work-validate', 'sonar']);
|
|
779
|
+
export const RULE_GATES = Object.freeze(['pre-commit', 'pre-push', 'settle', 'land', 'ci', 'sonar', 'runtime']);
|
|
780
|
+
export const ENFORCER_FAMILIES = Object.freeze(['eslint-be', 'eslint-fe', 'stylelint', 'machine', 'hfs', 'work-validate', 'sonar', 'runtime']);
|
|
787
781
|
export const RULE_KINDS = Object.freeze(['codemod', 'lint', 'check', 'design']);
|
|
788
782
|
const FINDING_CODE = /^[A-Z][A-Z0-9]*(_[A-Z0-9]+)+$/;
|
|
789
783
|
const ENFORCER_ID = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
790
|
-
const FILE_ENFORCERS = ['machine', 'hfs', 'work-validate', 'sonar'];
|
|
784
|
+
const FILE_ENFORCERS = ['machine', 'hfs', 'work-validate', 'sonar', 'runtime'];
|
|
791
785
|
|
|
792
786
|
/** Shape and semantic problems of a parsed knowledge/hfs/rules.yaml, in the words of modules/schemas/hfs-rules.schema.yaml. */
|
|
793
787
|
function ruleCatalogProblems(d) {
|
|
@@ -853,7 +847,7 @@ function ruleCatalogProblems(d) {
|
|
|
853
847
|
if (e.at !== undefined) {
|
|
854
848
|
if (typeof e.at !== 'string' || !e.at.trim() || e.at.startsWith('/') || e.at.includes('..')) bad.push(`${eat}.at must be a repository-relative path`);
|
|
855
849
|
if (e.status === 'planned') bad.push(`${eat} is planned, so it has no file yet (at)`);
|
|
856
|
-
if (!FILE_ENFORCERS.includes(e.kind)) bad.push(`${eat}.at belongs to a machine, hfs, work-validate or
|
|
850
|
+
if (!FILE_ENFORCERS.includes(e.kind)) bad.push(`${eat}.at belongs to a machine, hfs, work-validate, sonar or runtime enforcer only`);
|
|
857
851
|
} else if (FILE_ENFORCERS.includes(e.kind) && e.status !== 'planned') bad.push(`${eat} exists, so it names the file (at) that emits its code`);
|
|
858
852
|
});
|
|
859
853
|
if (Array.isArray(r.gates)) {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// hfs-view.mjs - the frozen view of one side of an app (be/ or fe/, the side folder as its root) that the lint factories hand
|
|
2
2
|
// their rules (`settings.starci.hfs`). Both @starci/eslint-canon-be and @starci/eslint-canon-fe ship a byte copy of this file in their
|
|
3
|
-
// runtime/ bundle (
|
|
3
|
+
// runtime/ bundle (scripts/hfs/sync-runtime.mjs), so a path-scoped rule in either package asks the SAME resolver the
|
|
4
4
|
// architecture machine and `hfs check` use, and never tests a path with a regular expression of its own.
|
|
5
5
|
import path from 'node:path';
|
|
6
|
-
import { allowsFile } from './
|
|
7
|
-
import { openHfs } from './
|
|
6
|
+
import { allowsFile } from './allows.mjs';
|
|
7
|
+
import { openHfs } from './slots.mjs';
|
|
8
8
|
|
|
9
9
|
const posix = (p) => String(p).replace(/\\/g, '/');
|
|
10
10
|
const isAbsoluteAny = (text) => path.isAbsolute(text) || /^[A-Za-z]:\//.test(text);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// language.mjs - the one home of "is this text English?" for source, comments, tests and docs (HFS_LANGUAGE_NOT_ENGLISH,
|
|
2
2
|
// BE_SOURCE_FORM, FE_SOURCE_FORM). Shared by the architecture machine (docs) and by both lint canons (each ships a byte
|
|
3
|
-
// copy in its runtime/ bundle, kept by
|
|
3
|
+
// copy in its runtime/ bundle, kept by scripts/hfs/sync-runtime.mjs).
|
|
4
4
|
//
|
|
5
5
|
// Detection is structural on characters, never a word list: the letters Vietnamese adds to the Latin alphabet (a-breve, a-circumflex,
|
|
6
6
|
// d-stroke, e-circumflex, o-circumflex, o-horn, u-horn) and every vowel carrying a tone mark. Text is folded to NFC first, so a
|
|
@@ -26,7 +26,7 @@ export function secondLanguageHits(text) {
|
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* The Vietnamese fields of one failure-code entry: the operator text the owner mandated for the failure-code catalog
|
|
29
|
-
* (scripts/checks/failure-codes.mjs types the entry with exactly these). causes_vi is the list companion of the three scalar fields.
|
|
29
|
+
* (scripts/checks/check-failure-codes.mjs types the entry with exactly these). causes_vi is the list companion of the three scalar fields.
|
|
30
30
|
*/
|
|
31
31
|
export const FAILURE_CODE_VIETNAMESE_FIELDS = Object.freeze(['title_vi', 'meaning_vi', 'nextStep_vi', 'causes_vi']);
|
|
32
32
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// line-count.mjs - text line counts and the size-growth limit (pure).
|
|
2
|
+
|
|
3
|
+
/** The line count of a text; a trailing newline does not open a line. */
|
|
4
|
+
export const lineCount = (text) => {
|
|
5
|
+
const lines = String(text).split(/\r?\n/);
|
|
6
|
+
return lines.at(-1) === '' ? lines.length - 1 : lines.length;
|
|
7
|
+
};
|
|
8
|
+
/** The line count above which a file may not grow: the soft budget when `hardGrowth` holds, else no limit. */
|
|
9
|
+
export const hardGrowthLines = ({ soft, hardGrowth }) => (hardGrowth ? soft : Number.POSITIVE_INFINITY);
|
|
@@ -15,3 +15,5 @@ export const foldCase = (p) => (WIN ? p.toLowerCase() : p);
|
|
|
15
15
|
export const samePath = (a, b) => foldCase(a) === foldCase(b);
|
|
16
16
|
/** A path's comparable identity: absolute, forward slashes, no trailing slash, case-folded on Windows. */
|
|
17
17
|
export const pathKey = (p) => foldCase(slash(path.resolve(p)).replace(/\/+$/, ''));
|
|
18
|
+
/** A work-relative path in one spelling: forward slashes, no leading ./, no trailing slash. */
|
|
19
|
+
export const normWork = (p) => String(p ?? '').trim().replaceAll('\\', '/').replace(/^\.\/+/, '').replace(/\/+$/, '');
|
|
@@ -1,23 +1,12 @@
|
|
|
1
|
+
// walk.mjs - filesystem walking helpers: isInside (containment) and walkFiles (depth-first file listing).
|
|
1
2
|
import fs from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
|
-
import {fileURLToPath} from 'node:url';
|
|
4
4
|
|
|
5
5
|
/** Whether `target` is `root` itself or inside it (filesystem paths, either separator). */
|
|
6
6
|
export function isInside(root, target) {
|
|
7
7
|
const relative = path.relative(root, target);
|
|
8
8
|
return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
|
|
9
9
|
}
|
|
10
|
-
|
|
11
|
-
/** True when this module is the process entry point (`node file.mjs`, not an import). */
|
|
12
|
-
/** The real path of a file, or its resolved path when it cannot be read (a missing file is never the entry module). */
|
|
13
|
-
const realPathOf = (file) => { try { return fs.realpathSync.native(path.resolve(file)); } catch { return path.resolve(file); } };
|
|
14
|
-
/**
|
|
15
|
-
* True when the module at `metaUrl` is the process entry point. Both sides are compared as REAL paths: Node gives a module
|
|
16
|
-
* its symlink-resolved URL, while argv[1] keeps the path it was started with, so an entry reached through a node_modules
|
|
17
|
-
* junction or symlink (a lane worktree, an npx shim) would otherwise look like an import and silently do nothing.
|
|
18
|
-
*/
|
|
19
|
-
export const isMain = (metaUrl, argv = process.argv) => Boolean(argv[1]) && realPathOf(argv[1]) === realPathOf(fileURLToPath(metaUrl));
|
|
20
|
-
|
|
21
10
|
/**
|
|
22
11
|
* Every entry under `dir` that is not a directory, depth-first; `filter` sees the entry name and full
|
|
23
12
|
* path. `sorted` sorts each directory's entries by name (default: readdir order). `exclude`
|
package/size-budget.mjs
CHANGED
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { basename } from "node:path"
|
|
18
18
|
import { hfsOf } from "./lib/hfs.mjs"
|
|
19
|
-
import {
|
|
19
|
+
import { recordedLines } from "./runtime/scripts/api/git/recorded-lines.mjs"
|
|
20
|
+
import { hardGrowthLines, lineCount } from "./runtime/scripts/lib/line-count.mjs"
|
|
20
21
|
|
|
21
22
|
/** A migration is a file of the persistence slot named `<timestamp>-<name>.ts`. */
|
|
22
23
|
const isMigration = (hfs, filename) => hfs.slotOf(filename) === "be.persistence" && /^\d{13,14}-[^/]+\.[cm]?ts$/.test(basename(filename))
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
// recorded-lines.mjs - the one home of the file-size ratchet's arithmetic (R20 HFS_SIZE_GROWTH), shared by the back-end
|
|
2
|
-
// and front-end lint canons (each ships a byte copy in its runtime/ bundle, kept by packages/hfs/scripts/sync-runtime.mjs).
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { runGit } from '../api/git/lib.mjs';
|
|
5
|
-
|
|
6
|
-
/** The line count of a text; a trailing newline does not open a line. */
|
|
7
|
-
export const lineCount = (text) => {
|
|
8
|
-
const lines = String(text).split(/\r?\n/);
|
|
9
|
-
return lines.at(-1) === '' ? lines.length - 1 : lines.length;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
/** The line count above which a file may not grow: the soft budget when `hardGrowth` holds, else no limit. */
|
|
13
|
-
export const hardGrowthLines = ({ soft, hardGrowth }) => (hardGrowth ? soft : Number.POSITIVE_INFINITY);
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* The line count of a file at a git revision, or null when the file did not exist there or git is absent.
|
|
17
|
-
*
|
|
18
|
-
* @param {string} filename - Absolute path of the file.
|
|
19
|
-
* @param {string} ref - The revision, `HEAD` by default (the parent of the commit being made).
|
|
20
|
-
* @returns {number | null} The recorded line count.
|
|
21
|
-
*/
|
|
22
|
-
export const recordedLines = (filename, ref = 'HEAD') => {
|
|
23
|
-
const r = runGit(['show', `${ref}:./${path.basename(filename)}`], { dir: path.dirname(filename), maxBuffer: 64 * 1024 * 1024 });
|
|
24
|
-
// no repository, no such revision, or a new file: all mean there is no recorded size to compare with
|
|
25
|
-
if (r.error || r.status !== 0) return null;
|
|
26
|
-
return lineCount(r.stdout);
|
|
27
|
-
};
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|