@starci/eslint-canon-fe 8.0.7 → 8.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/CHANGELOG.md +7 -0
- package/docs/grammar-boundary.md +1 -1
- package/file-layout.mjs +3 -1
- package/grammar-boundary.mjs +4 -4
- package/lib/hfs.mjs +3 -3
- package/lib/next.mjs +1 -1
- package/package.json +3 -3
- package/project-graph.mjs +4 -4
- package/props-and-slots.mjs +4 -3
- 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-growth.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
|
@@ -1,107 +1,14 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
1
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { canonical, isInside } from './config.mjs';
|
|
3
|
+
import { parseContract, installedSWR } from './next-data-contract.mjs';
|
|
5
4
|
import { relativePath, sourceLocation, unwrapExpression } from './typescript.mjs';
|
|
6
5
|
|
|
7
6
|
export const SWR_KEY_RULE_ID = 'FE_SWR_KEY_IDENTITY';
|
|
8
7
|
export const SWR_MUTATION_RULE_ID = 'FE_SWR_MUTATION_RESOURCE_IDENTITY';
|
|
9
8
|
export const SWR_DATA_RULE_IDS = [SWR_KEY_RULE_ID, SWR_MUTATION_RULE_ID];
|
|
10
9
|
|
|
11
|
-
const CONTRACT_SCHEMA = 'starci/next-data-lifecycle@1';
|
|
12
|
-
const SOURCE = /\.[cm]?[jt]sx?$/i;
|
|
13
|
-
const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
14
|
-
const BINDING = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/;
|
|
15
|
-
const TOP_KEYS = new Set(['schema', 'swr', 'hooks']);
|
|
16
|
-
const SWR_KEYS = new Set(['package', 'major']);
|
|
17
|
-
const HOOK_KEYS = new Set(['id', 'path', 'export', 'kind', 'resultBinding', 'identities']);
|
|
18
|
-
const IDENTITY_KEYS = new Set(['id', 'binding', 'gatesRequest', 'resource']);
|
|
19
10
|
const SWR_SPECIFIERS = new Set(['swr', 'swr/immutable', 'swr/mutation']);
|
|
20
11
|
|
|
21
|
-
function relativeSource(repository, value, label) {
|
|
22
|
-
repository = path.resolve(repository);
|
|
23
|
-
if (typeof value !== 'string' || !value.trim() || path.isAbsolute(value)) throw Error(`${label} must be a non-empty repository-relative source path.`);
|
|
24
|
-
const relative = slash(value.trim()).replace(/^\.\//, '');
|
|
25
|
-
if (relative.split('/').includes('..') || !SOURCE.test(relative) || /\.d\.[cm]?[jt]s$/i.test(relative)) {
|
|
26
|
-
throw Error(`${label} must be a production TypeScript or JavaScript source path.`);
|
|
27
|
-
}
|
|
28
|
-
const absolute = path.resolve(repository, ...relative.split('/'));
|
|
29
|
-
if (!isInside(repository, absolute)) throw Error(`${label} must stay inside the repository.`);
|
|
30
|
-
for (let current = absolute; current !== repository; current = path.dirname(current)) {
|
|
31
|
-
const stat = fs.lstatSync(current);
|
|
32
|
-
if (stat.isSymbolicLink()) throw Error(`${label} must not cross a symbolic-link or junction ancestor.`);
|
|
33
|
-
}
|
|
34
|
-
const stat = fs.lstatSync(absolute);
|
|
35
|
-
if (!stat.isFile() || !isInside(canonical(repository), canonical(absolute))) throw Error(`${label} must be a regular source file inside the repository.`);
|
|
36
|
-
return relative;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function parseContract(config) {
|
|
40
|
-
// The contract is declared in the app's one package.json (the app root; config.root is the fe side folder).
|
|
41
|
-
const manifest = JSON.parse(fs.readFileSync(path.join(config.packageRoot ?? config.root, 'package.json'), 'utf8'));
|
|
42
|
-
const next = manifest?.starci?.codePatterns?.next;
|
|
43
|
-
const value = next?.dataLifecycle;
|
|
44
|
-
if (value === undefined) return null;
|
|
45
|
-
if (next.schema !== 'starci/next-code-pattern-contract@1') throw Error('package.json#starci.codePatterns.next must use starci/next-code-pattern-contract@1.');
|
|
46
|
-
exactKeys(value, TOP_KEYS, 'package.json#starci.codePatterns.next.dataLifecycle');
|
|
47
|
-
if (value.schema !== CONTRACT_SCHEMA) throw Error(`dataLifecycle.schema must be ${CONTRACT_SCHEMA}.`);
|
|
48
|
-
exactKeys(value.swr, SWR_KEYS, 'dataLifecycle.swr');
|
|
49
|
-
if (value.swr.package !== 'swr' || !Number.isInteger(value.swr.major) || value.swr.major < 1) {
|
|
50
|
-
throw Error('dataLifecycle.swr must bind package swr and one positive integer major version.');
|
|
51
|
-
}
|
|
52
|
-
if (!Array.isArray(value.hooks) || value.hooks.length === 0) throw Error('dataLifecycle.hooks must contain at least one declared lifecycle call.');
|
|
53
|
-
const ids = new Set();
|
|
54
|
-
const hooks = value.hooks.map((hook, index) => {
|
|
55
|
-
const label = `dataLifecycle.hooks[${index}]`;
|
|
56
|
-
exactKeys(hook, HOOK_KEYS, label);
|
|
57
|
-
if (typeof hook.id !== 'string' || !hook.id.trim() || ids.has(hook.id.trim())) throw Error(`${label}.id must be unique and non-empty.`);
|
|
58
|
-
ids.add(hook.id.trim());
|
|
59
|
-
const source = relativeSource(config.root, hook.path, `${label}.path`);
|
|
60
|
-
if (typeof hook.export !== 'string' || !/^use[A-Z0-9_$][\w$]*$/.test(hook.export)) throw Error(`${label}.export must name one exported use* hook.`);
|
|
61
|
-
if (!['query', 'mutation'].includes(hook.kind)) throw Error(`${label}.kind must be query or mutation.`);
|
|
62
|
-
if (hook.resultBinding !== undefined && (typeof hook.resultBinding !== 'string' || !IDENTIFIER.test(hook.resultBinding))) {
|
|
63
|
-
throw Error(`${label}.resultBinding must be one local identifier.`);
|
|
64
|
-
}
|
|
65
|
-
if (!Array.isArray(hook.identities)) throw Error(`${label}.identities must be an array.`);
|
|
66
|
-
const identityIds = new Set(), bindings = new Set();
|
|
67
|
-
const identities = hook.identities.map((identity, identityIndex) => {
|
|
68
|
-
const identityLabel = `${label}.identities[${identityIndex}]`;
|
|
69
|
-
exactKeys(identity, IDENTITY_KEYS, identityLabel);
|
|
70
|
-
if (typeof identity.id !== 'string' || !identity.id.trim() || identityIds.has(identity.id.trim())) throw Error(`${identityLabel}.id must be unique and non-empty.`);
|
|
71
|
-
if (typeof identity.binding !== 'string' || !BINDING.test(identity.binding) || bindings.has(identity.binding)) throw Error(`${identityLabel}.binding must be a unique identifier or property path.`);
|
|
72
|
-
if (typeof identity.gatesRequest !== 'boolean' || typeof identity.resource !== 'boolean') throw Error(`${identityLabel} must explicitly declare gatesRequest and resource booleans.`);
|
|
73
|
-
identityIds.add(identity.id.trim());
|
|
74
|
-
bindings.add(identity.binding);
|
|
75
|
-
return { id: identity.id.trim(), binding: identity.binding, gatesRequest: identity.gatesRequest, resource: identity.resource };
|
|
76
|
-
});
|
|
77
|
-
if (hook.kind === 'mutation' && !identities.some(identity => identity.resource)) throw Error(`${label} mutation must declare at least one resource identity.`);
|
|
78
|
-
return { id: hook.id.trim(), path: source, export: hook.export, kind: hook.kind,
|
|
79
|
-
...(hook.resultBinding === undefined ? {} : { resultBinding: hook.resultBinding }), identities };
|
|
80
|
-
});
|
|
81
|
-
return { schema: value.schema, swr: { package: 'swr', major: value.swr.major }, hooks };
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function installedSWR(repository) {
|
|
85
|
-
const require = createRequire(path.join(repository, 'package.json'));
|
|
86
|
-
let manifest;
|
|
87
|
-
try { manifest = require.resolve('swr/package.json'); } catch {
|
|
88
|
-
let entry;
|
|
89
|
-
try { entry = require.resolve('swr'); } catch { throw Error('Installed swr package is unavailable.'); }
|
|
90
|
-
let current = path.dirname(entry);
|
|
91
|
-
while (isInside(repository, current) && current !== repository) {
|
|
92
|
-
const candidate = path.join(current, 'package.json');
|
|
93
|
-
if (fs.existsSync(candidate)) { manifest = candidate; break; }
|
|
94
|
-
current = path.dirname(current);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
if (!manifest) throw Error('Installed swr package.json is unavailable.');
|
|
98
|
-
const actualManifest = canonical(manifest);
|
|
99
|
-
const parsed = JSON.parse(fs.readFileSync(actualManifest, 'utf8'));
|
|
100
|
-
const major = Number.parseInt(String(parsed.version ?? '').split('.')[0], 10);
|
|
101
|
-
if (parsed.name !== 'swr' || !Number.isInteger(major)) throw Error('Installed swr package identity or version is invalid.');
|
|
102
|
-
return { name: parsed.name, version: parsed.version, major, root: canonical(path.dirname(actualManifest)) };
|
|
103
|
-
}
|
|
104
|
-
|
|
105
12
|
function normalizedSymbol(ts, checker, value) {
|
|
106
13
|
let symbol = value ?? null;
|
|
107
14
|
const seen = new Set();
|
|
@@ -543,22 +450,29 @@ function nullish(ts, checker, expression) {
|
|
|
543
450
|
return !symbol || (symbol.getDeclarations?.() ?? []).every(declaration => declaration.getSourceFile().isDeclarationFile);
|
|
544
451
|
}
|
|
545
452
|
|
|
546
|
-
|
|
453
|
+
/** True when taking `branch` of `condition` proves the declared identity is available. */
|
|
454
|
+
function branchMeansAvailable(ts, checker, condition, identity, branch) {
|
|
547
455
|
condition = unwrapExpression(ts, condition);
|
|
548
456
|
if (expressionReferences(ts, checker, condition, identity) && (ts.isIdentifier(condition)
|
|
549
|
-
|| ts.isPropertyAccessExpression(condition) || ts.isElementAccessExpression(condition))) return
|
|
457
|
+
|| ts.isPropertyAccessExpression(condition) || ts.isElementAccessExpression(condition))) return branch;
|
|
550
458
|
if (ts.isPrefixUnaryExpression(condition) && condition.operator === ts.SyntaxKind.ExclamationToken) {
|
|
551
|
-
|
|
552
|
-
return nested === null ? null : !nested;
|
|
459
|
+
return branchMeansAvailable(ts, checker, condition.operand, identity, !branch);
|
|
553
460
|
}
|
|
554
461
|
if (ts.isBinaryExpression(condition)) {
|
|
462
|
+
const operator = condition.operatorToken.kind;
|
|
463
|
+
if (operator === ts.SyntaxKind.AmpersandAmpersandToken || operator === ts.SyntaxKind.BarBarToken) {
|
|
464
|
+
// `a && b` is true only when both sides are true; `a || b` is false only when both sides are false.
|
|
465
|
+
const decisive = operator === ts.SyntaxKind.AmpersandAmpersandToken ? branch : !branch;
|
|
466
|
+
return decisive && [condition.left, condition.right]
|
|
467
|
+
.some(side => branchMeansAvailable(ts, checker, side, identity, branch));
|
|
468
|
+
}
|
|
555
469
|
const leftIdentity = expressionReferences(ts, checker, condition.left, identity) && nullish(ts, checker, condition.right);
|
|
556
470
|
const rightIdentity = expressionReferences(ts, checker, condition.right, identity) && nullish(ts, checker, condition.left);
|
|
557
|
-
if (!leftIdentity && !rightIdentity) return
|
|
558
|
-
if ([ts.SyntaxKind.ExclamationEqualsToken, ts.SyntaxKind.ExclamationEqualsEqualsToken].includes(
|
|
559
|
-
if ([ts.SyntaxKind.EqualsEqualsToken, ts.SyntaxKind.EqualsEqualsEqualsToken].includes(
|
|
471
|
+
if (!leftIdentity && !rightIdentity) return false;
|
|
472
|
+
if ([ts.SyntaxKind.ExclamationEqualsToken, ts.SyntaxKind.ExclamationEqualsEqualsToken].includes(operator)) return branch;
|
|
473
|
+
if ([ts.SyntaxKind.EqualsEqualsToken, ts.SyntaxKind.EqualsEqualsEqualsToken].includes(operator)) return !branch;
|
|
560
474
|
}
|
|
561
|
-
return
|
|
475
|
+
return false;
|
|
562
476
|
}
|
|
563
477
|
|
|
564
478
|
function violation(config, call, ruleId, message, extra = {}) {
|
|
@@ -687,8 +601,7 @@ function inspectKey(config, context, entry, call, identities, violations, reason
|
|
|
687
601
|
}
|
|
688
602
|
if (identity.gatesRequest) {
|
|
689
603
|
const gated = active.every(leaf => leaf.decisions.some(decision => {
|
|
690
|
-
|
|
691
|
-
return available !== null && decision.branch === available;
|
|
604
|
+
return branchMeansAvailable(context.ts, call.checker, decision.condition, identity, decision.branch);
|
|
692
605
|
}));
|
|
693
606
|
if (!gated) violations.push(violation(config, call, SWR_KEY_RULE_ID,
|
|
694
607
|
`${entry.id} must produce an explicit null key when ${identity.id} (${identity.binding}) is unavailable.`,
|
|
@@ -5,7 +5,7 @@ import { gitOutput } from '../../api/git/lib.mjs';
|
|
|
5
5
|
/**
|
|
6
6
|
* HFS check 5: the files and directories the slot manifest requires (knowledge/hfs/slots.yaml `requires`,
|
|
7
7
|
* `requiredInstances`, `minInstances`, required app kinds) must exist in the repository tree. Everything is read through the
|
|
8
|
-
* resolver of scripts/
|
|
8
|
+
* resolver of scripts/hfs/slots.mjs; nothing here names a path.
|
|
9
9
|
*
|
|
10
10
|
* BE_REQUIRED_MODULE_MISSING a backend app, feature, domain, integrations or platform instance lacks a required file or
|
|
11
11
|
* directory, or a required platform instance (config, logging, errors, primitives) is absent
|
|
@@ -60,7 +60,7 @@ export const LINT_ENFORCERS = Object.freeze([
|
|
|
60
60
|
enforcer('component-purity', FE, ['FE_COMPONENT_WORLD_OWNERSHIP', 'FE_WORLD_OWNER_RENDER_BOUNDARY', 'FE_CONNECTED_BLOCK_RENDER_PAIR', 'FE_PURE_REACHES_DATA', 'FE_PURE_WORLD_HOOK', 'FE_PURE_WORLD_IMPORT'], 'A pure component reaches no data or world state; its connected entry owns them.'),
|
|
61
61
|
enforcer('grammar-entry', FE, ['ARCH_GRAMMAR_CONTRACT_INVALID', 'ARCH_GRAMMAR_EXPORT_BYPASS'], 'Vendor grammar is reached only through its owner entry.'),
|
|
62
62
|
enforcer('i18n-keys', FE, ['FE_I18N_KEYS'], 'Translation keys read by source and keys held by catalogs agree both ways.'),
|
|
63
|
-
// The per-path judgements of the slot manifest (scripts/
|
|
63
|
+
// The per-path judgements of the slot manifest (scripts/hfs/path-findings.mjs), on a tracked TypeScript file.
|
|
64
64
|
enforcer('slot-undeclared', BOTH, ['HFS_SLOT_UNDECLARED', 'HFS_SLOT_AMBIGUOUS', 'HFS_SLOT_NOT_ENABLED'], 'A TypeScript file is owned by exactly one slot of the repository.', { origin: 'repo' }),
|
|
65
65
|
enforcer('source-suffix', BE, ['BE_SOURCE_FORM'], 'A source file name is index.ts, main.ts, a migration or <kebab-name>.<suffix>.ts with a suffix of the closed list.', { origin: 'repo' }),
|
|
66
66
|
enforcer('spec-placement', BE, ['BE_SPEC_PLACEMENT'], 'A spec file lives in one of the four test layers and nowhere else.', { origin: 'repo' }),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { treeOf } from './required-files.mjs';
|
|
2
|
-
import { allowsFile } from '
|
|
3
|
-
import { locateDeclaration } from '
|
|
2
|
+
import { allowsFile } from '../allows.mjs';
|
|
3
|
+
import { locateDeclaration } from '../slots.mjs';
|
|
4
4
|
import { DEFAULT_ENVIRONMENT, STACKS_DIRECTORY, STATEFUL_KINDS, namesOfService, readStack } from '../../lib/stack-services.mjs';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -5,7 +5,7 @@ import { canonical, isInside, slash } from './config.mjs';
|
|
|
5
5
|
import { sameOrUnder } from '../../lib/path-key.mjs';
|
|
6
6
|
import { createTypeScriptProgram, readTypeScriptProject, resolveTypeScriptModule, sharedInProgramRun, typeScriptProjectReferencePath } from '../typescript-programs.mjs';
|
|
7
7
|
import { readJsonFile } from '../../lib/json.mjs';
|
|
8
|
-
import { locateDeclaration } from '
|
|
8
|
+
import { locateDeclaration } from '../slots.mjs';
|
|
9
9
|
|
|
10
10
|
const CODE_EXTENSIONS = /\.(?:[cm]?[jt]sx?)$/i;
|
|
11
11
|
const TEST_FILE = /(?:^|[.-])(?:spec|test)\.[cm]?[jt]sx?$/i;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// manifest-shape.mjs - the shape rules of an HFS slot manifest that both manifest kinds share, and the whole shape of a
|
|
2
|
+
// manifest of kind runtime (knowledge/hfs/runtime-slots.yaml): the slot fields, the tier map, ruleParams.runtime and the
|
|
3
|
+
// `pending` allowlist. scripts/hfs/slots.mjs (loadSlotManifest) is the only reader; it adds the app kind's own rules
|
|
4
|
+
// (sides, app kinds) and refuses a manifest whole on any problem (HFS_MANIFEST_INVALID). Pure: every function takes a
|
|
5
|
+
// parsed manifest or slot and returns a list of problems in the words of modules/schemas/hfs-slots.schema.yaml.
|
|
6
|
+
import { isPlainObject } from '../../engine/plain-object.mjs';
|
|
7
|
+
|
|
8
|
+
export const SEMVER = /^(\d+)\.(\d+)\.(\d+)$/;
|
|
9
|
+
export const NAME = /^[a-z][a-z0-9-]*$/;
|
|
10
|
+
export const ENV_PREFIX = /^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$/;
|
|
11
|
+
export const SLOT_ID = /^(app|repo|be|fe)\.[a-z0-9-]+(\.[a-z0-9-]+)*$/;
|
|
12
|
+
export const RUNTIME_SLOT_ID = /^runtime\.[a-z0-9-]+(\.[a-z0-9-]+)*$/;
|
|
13
|
+
export const PRESENCE = ['required', 'optional', 'opt-in', 'forbidden'];
|
|
14
|
+
export const TRACKED = ['tracked', 'ignored', 'external'];
|
|
15
|
+
/** A runtime manifest adds `generated`: tracked, written only by the slot's generatedBy, judged by drift (RT_GENERATED_DRIFT). */
|
|
16
|
+
export const RUNTIME_TRACKED = [...TRACKED, 'generated'];
|
|
17
|
+
export const TESTS = ['unit-beside', 'e2e', 'none'];
|
|
18
|
+
export const APP_KIND = 'app';
|
|
19
|
+
/** The kind of a manifest and of a declaration that describes the StarCi runtime repository; also its one profile. */
|
|
20
|
+
export const RUNTIME_KIND = 'runtime';
|
|
21
|
+
export const MANIFEST_KINDS = [APP_KIND, RUNTIME_KIND];
|
|
22
|
+
/** The kind of a parsed manifest: `kind`, or app when absent (knowledge/hfs/slots.yaml carries none). */
|
|
23
|
+
export const manifestKind = (m) => (isPlainObject(m) && m.kind !== undefined ? m.kind : APP_KIND);
|
|
24
|
+
export const strList = (v) => Array.isArray(v) && v.every((s) => typeof s === 'string' && s.length > 0);
|
|
25
|
+
|
|
26
|
+
const APP_SLOT_KEYS = ['id', 'profiles', 'path', 'presence', 'tracked', 'tier', 'tests', 'owner', 'appKind', 'minInstances', 'requiredWhen', 'requiredInstances', 'requires', 'allows', 'forbids', 'layers', 'kinds', 'roles', 'composedBy', 'budget', 'managedBy', 'rules', 'goesTo', 'why', 'since', 'retiredIn', 'successor'];
|
|
27
|
+
/** A runtime slot has no app kind, side composition, layer or managed template; it may name the generator of a generated copy. */
|
|
28
|
+
const RUNTIME_SLOT_KEYS = ['id', 'profiles', 'path', 'presence', 'tracked', 'tier', 'tests', 'owner', 'minInstances', 'requires', 'allows', 'forbids', 'budget', 'rules', 'goesTo', 'why', 'since', 'retiredIn', 'successor', 'generatedBy'];
|
|
29
|
+
|
|
30
|
+
/** Shape problems of one slot of a manifest of `kind` (app or runtime). */
|
|
31
|
+
export function slotProblems(slot, index, kind, { appScope = 'app', scopes = [] } = {}) {
|
|
32
|
+
const bad = [];
|
|
33
|
+
const runtime = kind === RUNTIME_KIND;
|
|
34
|
+
const slotKeys = new Set(runtime ? RUNTIME_SLOT_KEYS : APP_SLOT_KEYS);
|
|
35
|
+
const tracked = runtime ? RUNTIME_TRACKED : TRACKED;
|
|
36
|
+
const at = isPlainObject(slot) && typeof slot.id === 'string' ? `slot ${slot.id}` : `slots[${index}]`;
|
|
37
|
+
if (!isPlainObject(slot)) return [`${at} is not a map`];
|
|
38
|
+
for (const key of Object.keys(slot)) if (!slotKeys.has(key)) bad.push(`${at}: unknown field ${key}`);
|
|
39
|
+
if (runtime) {
|
|
40
|
+
if (!RUNTIME_SLOT_ID.test(String(slot.id))) bad.push(`${at}: id must look like runtime.<name>`);
|
|
41
|
+
if (JSON.stringify(slot.profiles) !== JSON.stringify([RUNTIME_KIND])) bad.push(`${at}: profiles must be [runtime]`);
|
|
42
|
+
if (slot.tracked === 'generated' && (typeof slot.generatedBy !== 'string' || !slot.generatedBy || slot.generatedBy.startsWith('/') || slot.generatedBy.includes('..'))) bad.push(`${at}: a generated slot names its generator (generatedBy, a repository-relative path)`);
|
|
43
|
+
if (slot.generatedBy !== undefined && slot.tracked !== 'generated') bad.push(`${at}: generatedBy belongs to a generated slot`);
|
|
44
|
+
} else {
|
|
45
|
+
if (!SLOT_ID.test(String(slot.id))) bad.push(`${at}: id must look like be.transport.http`);
|
|
46
|
+
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(appScope) && slot.profiles.length !== 1)) bad.push(`${at}: profiles must be [app] or a unique non-empty subset of be, fe`);
|
|
47
|
+
else if ((slot.profiles[0] === appScope) !== String(slot.id).startsWith(`${appScope}.`)) bad.push(`${at}: an app-root slot has profiles [app] and an id app.<name>, and only it`);
|
|
48
|
+
}
|
|
49
|
+
if (typeof slot.path !== 'string' || !slot.path) bad.push(`${at}: path is required`);
|
|
50
|
+
if (!PRESENCE.includes(slot.presence)) bad.push(`${at}: presence must be one of ${PRESENCE.join(', ')}`);
|
|
51
|
+
if (!tracked.includes(slot.tracked)) bad.push(`${at}: tracked must be one of ${tracked.join(', ')}`);
|
|
52
|
+
if (!NAME.test(String(slot.tier))) bad.push(`${at}: tier must be a tier name, none or inherit`);
|
|
53
|
+
if (!TESTS.includes(slot.tests)) bad.push(`${at}: tests must be one of ${TESTS.join(', ')}`);
|
|
54
|
+
if (slot.owner !== undefined && typeof slot.owner !== 'boolean') bad.push(`${at}: owner must be a boolean`);
|
|
55
|
+
if (slot.appKind !== undefined && !NAME.test(String(slot.appKind))) bad.push(`${at}: appKind must be a name`);
|
|
56
|
+
if (slot.minInstances !== undefined && !(Number.isInteger(slot.minInstances) && slot.minInstances >= 1)) bad.push(`${at}: minInstances must be a positive integer`);
|
|
57
|
+
if (slot.requiredWhen !== undefined && slot.requiredWhen !== 'connections') bad.push(`${at}: requiredWhen may only be connections`);
|
|
58
|
+
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`);
|
|
59
|
+
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`);
|
|
60
|
+
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`);
|
|
61
|
+
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`);
|
|
62
|
+
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`);
|
|
63
|
+
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`);
|
|
64
|
+
if (slot.managedBy !== undefined && !NAME.test(String(slot.managedBy))) bad.push(`${at}: managedBy must be a template id`);
|
|
65
|
+
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`);
|
|
66
|
+
if (slot.since !== undefined && !SEMVER.test(String(slot.since))) bad.push(`${at}: since must be a version`);
|
|
67
|
+
if (slot.retiredIn !== undefined && !(Number.isInteger(slot.retiredIn) && slot.retiredIn >= 2)) bad.push(`${at}: retiredIn must be a major`);
|
|
68
|
+
if (slot.retiredIn !== undefined && typeof slot.successor !== 'string') bad.push(`${at}: a retired slot names its successor`);
|
|
69
|
+
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)`);
|
|
70
|
+
if (slot.presence === 'forbidden' && slot.tracked !== 'external') bad.push(`${at}: a forbidden slot is external`);
|
|
71
|
+
return bad;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The owners of an external call in a runtime manifest: an api system folder (`api/<system>`, `api/*` any system) or the DB tier. */
|
|
75
|
+
const INFRA_OWNER = /^(?:api\/(?:\*|[a-z][a-z0-9-]*)|engine\/db)$/;
|
|
76
|
+
const RUNTIME_PARAM_KEYS = ['fileLines', 'sourceRoots', 'infraOwners', 'baseWriteMembers', 'baseEnvSeams', 'apiContracts', 'sourceName', 'oneOffNames', 'sharedBasenames', 'generated', 'pinned', 'selfChecks'];
|
|
77
|
+
const PENDING_KEYS = ['path', 'rule', 'lane', 'since', 'reason'];
|
|
78
|
+
/** The chunk of the runtime migration that deletes a pending entry (C1..C8, a split chunk C2a/C2b). */
|
|
79
|
+
const PENDING_LANE = /^C[1-8][ab]?$/;
|
|
80
|
+
const DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
81
|
+
const relPath = (v) => typeof v === 'string' && v.length > 0 && !v.startsWith('/') && !v.includes('..') && !v.includes('\\');
|
|
82
|
+
|
|
83
|
+
/** Shape problems of a parsed manifest of kind runtime (knowledge/hfs/runtime-slots.yaml), in the words of modules/schemas/hfs-slots.schema.yaml. */
|
|
84
|
+
export function runtimeShapeProblems(m) {
|
|
85
|
+
const bad = [];
|
|
86
|
+
const allowed = new Set(['schema', 'kind', 'version', 'versioning', 'presenceValues', 'trackedValues', 'testValues', 'profiles', 'tiers', 'ruleParams', 'crossOwner', 'crossSystem', 'slots', 'pending', 'consumers']);
|
|
87
|
+
for (const key of Object.keys(m)) if (!allowed.has(key)) bad.push(`unknown top-level key ${key}`);
|
|
88
|
+
if (!/^starci\/runtime-slots@\d+$/.test(String(m.schema))) bad.push('schema must be starci/runtime-slots@<major>');
|
|
89
|
+
if (!SEMVER.test(String(m.version))) bad.push('version must be MAJOR.MINOR.PATCH');
|
|
90
|
+
if (!isPlainObject(m.versioning) || !['patch', 'minor', 'major', 'retire', 'pins'].every((k) => typeof m.versioning[k] === 'string')) bad.push('versioning needs patch, minor, major, retire and pins text');
|
|
91
|
+
if (JSON.stringify(m.presenceValues) !== JSON.stringify(PRESENCE)) bad.push(`presenceValues must be ${PRESENCE.join(', ')}`);
|
|
92
|
+
if (JSON.stringify(m.trackedValues) !== JSON.stringify(RUNTIME_TRACKED)) bad.push(`trackedValues must be ${RUNTIME_TRACKED.join(', ')}`);
|
|
93
|
+
if (JSON.stringify(m.testValues) !== JSON.stringify(TESTS)) bad.push(`testValues must be ${TESTS.join(', ')}`);
|
|
94
|
+
if (JSON.stringify(m.profiles) !== JSON.stringify([RUNTIME_KIND])) bad.push('profiles must be [runtime]');
|
|
95
|
+
for (const key of ['crossOwner', 'crossSystem']) if (m[key] !== undefined && typeof m[key] !== 'string') bad.push(`${key} must be text`);
|
|
96
|
+
if (!isPlainObject(m.tiers) || Object.keys(m.tiers).join() !== RUNTIME_KIND) bad.push('tiers must be a map with exactly runtime');
|
|
97
|
+
else bad.push(...tierMapProblems(RUNTIME_KIND, m.tiers.runtime));
|
|
98
|
+
const rp = m.ruleParams;
|
|
99
|
+
if (!isPlainObject(rp) || Object.keys(rp).join() !== RUNTIME_KIND || !isPlainObject(rp.runtime)) bad.push('ruleParams must be a map with exactly runtime');
|
|
100
|
+
else bad.push(...runtimeParamProblems(rp.runtime));
|
|
101
|
+
if (!Array.isArray(m.slots) || !m.slots.length) bad.push('slots must be a non-empty list');
|
|
102
|
+
else m.slots.forEach((slot, index) => bad.push(...slotProblems(slot, index, RUNTIME_KIND)));
|
|
103
|
+
if (m.pending !== undefined) {
|
|
104
|
+
if (!Array.isArray(m.pending)) bad.push('pending must be a list');
|
|
105
|
+
else m.pending.forEach((entry, index) => {
|
|
106
|
+
const at = `pending[${index}]`;
|
|
107
|
+
if (!isPlainObject(entry)) { bad.push(`${at} is not a map`); return; }
|
|
108
|
+
for (const key of Object.keys(entry)) if (!PENDING_KEYS.includes(key)) bad.push(`${at}: unknown field ${key}`);
|
|
109
|
+
if (!relPath(entry.path)) bad.push(`${at}: path must be a repository-relative glob`);
|
|
110
|
+
if (!/^[A-Z][A-Z0-9]*(_[A-Z0-9]+)+$/.test(String(entry.rule))) bad.push(`${at}: rule must be a finding code`);
|
|
111
|
+
if (!PENDING_LANE.test(String(entry.lane))) bad.push(`${at}: lane must be the chunk that removes it (C1..C8, C2a, C2b)`);
|
|
112
|
+
if (!DATE.test(String(entry.since))) bad.push(`${at}: since must be a date YYYY-MM-DD`);
|
|
113
|
+
if (typeof entry.reason !== 'string' || !entry.reason.trim()) bad.push(`${at}: reason is required`);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
if (m.consumers !== undefined && !strList(m.consumers)) bad.push('consumers must be a list of paths');
|
|
117
|
+
return bad;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Shape problems of ruleParams.runtime. */
|
|
121
|
+
function runtimeParamProblems(rp) {
|
|
122
|
+
const bad = [];
|
|
123
|
+
for (const key of Object.keys(rp)) if (!RUNTIME_PARAM_KEYS.includes(key)) bad.push(`ruleParams.runtime.${key} is not a runtime parameter`);
|
|
124
|
+
for (const key of RUNTIME_PARAM_KEYS) if (!(key in rp)) bad.push(`ruleParams.runtime.${key} is missing`);
|
|
125
|
+
const fl = rp.fileLines;
|
|
126
|
+
if (!(isPlainObject(fl) && Number.isInteger(fl.soft) && fl.soft >= 1 && typeof fl.hardGrowth === 'boolean' && Object.keys(fl).length === 2)) bad.push('ruleParams.runtime.fileLines must be {soft, hardGrowth}');
|
|
127
|
+
const owners = rp.infraOwners;
|
|
128
|
+
const ownerMap = (v) => isPlainObject(v) && Object.entries(v).every(([key, list]) => key && Array.isArray(list) && list.every((o) => INFRA_OWNER.test(String(o))) && new Set(list).size === list.length);
|
|
129
|
+
if (!isPlainObject(owners) || Object.keys(owners).sort().join() !== 'globals,modules,programs' || !['globals', 'modules', 'programs'].every((k) => ownerMap(owners[k]))) bad.push('ruleParams.runtime.infraOwners must be {modules, globals, programs}, each mapping a name to unique owners api/<system>, api/* or engine/db ([] means nowhere)');
|
|
130
|
+
for (const key of ['sourceRoots', 'baseWriteMembers', 'oneOffNames', 'sharedBasenames']) if (!(strList(rp[key]) && rp[key].length && new Set(rp[key]).size === rp[key].length)) bad.push(`ruleParams.runtime.${key} must be a non-empty list of unique strings`);
|
|
131
|
+
if (!(Array.isArray(rp.baseEnvSeams) && rp.baseEnvSeams.every(relPath))) bad.push('ruleParams.runtime.baseEnvSeams must be a list of repository-relative paths');
|
|
132
|
+
if (!(isPlainObject(rp.apiContracts) && Object.entries(rp.apiContracts).every(([system, file]) => NAME.test(system) && relPath(file)))) bad.push('ruleParams.runtime.apiContracts must map an api system to its calls contract path');
|
|
133
|
+
let sourceNameOk = typeof rp.sourceName === 'string';
|
|
134
|
+
try { if (sourceNameOk) new RegExp(rp.sourceName); } catch { sourceNameOk = false; }
|
|
135
|
+
if (!sourceNameOk) bad.push('ruleParams.runtime.sourceName must be a regular expression');
|
|
136
|
+
if (!(Array.isArray(rp.generated) && rp.generated.length && rp.generated.every((g) => isPlainObject(g) && relPath(g.root) && relPath(g.generatedBy) && Object.keys(g).length === 2))) bad.push('ruleParams.runtime.generated must be a non-empty list of {root, generatedBy}');
|
|
137
|
+
if (!(Array.isArray(rp.pinned) && rp.pinned.length && rp.pinned.every((p) => isPlainObject(p) && relPath(p.path) && typeof p.why === 'string' && p.why && Object.keys(p).length === 2))) bad.push('ruleParams.runtime.pinned must be a non-empty list of {path, why}');
|
|
138
|
+
if (!(Array.isArray(rp.selfChecks) && rp.selfChecks.length && rp.selfChecks.every((c) => isPlainObject(c) && NAME.test(String(c.id)) && relPath(c.run) && (c.args === undefined || strList(c.args)) && Object.keys(c).every((k) => ['id', 'run', 'args'].includes(k))))) bad.push('ruleParams.runtime.selfChecks must be a non-empty list of {id, run, args?}');
|
|
139
|
+
return bad;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Shape problems of the tier map of one profile. */
|
|
143
|
+
export function tierMapProblems(profile, tiers) {
|
|
144
|
+
const bad = [];
|
|
145
|
+
if (!isPlainObject(tiers) || !Object.keys(tiers).length) return [`tiers.${profile} must be a non-empty map`];
|
|
146
|
+
for (const [tier, def] of Object.entries(tiers)) {
|
|
147
|
+
if (!NAME.test(tier)) bad.push(`tiers.${profile}.${tier} is not a tier name`);
|
|
148
|
+
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`);
|
|
149
|
+
for (const key of Object.keys(def ?? {})) if (!['mayImport', 'acyclic', 'lowerLayerOnly'].includes(key)) bad.push(`tiers.${profile}.${tier}.${key} is not a tier field`);
|
|
150
|
+
}
|
|
151
|
+
return bad;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** The semantic rules of a runtime manifest beyond the slot rules both kinds share: tier names, the schema major, unique pending entries. */
|
|
155
|
+
export function runtimeSemanticProblems(m) {
|
|
156
|
+
const bad = [];
|
|
157
|
+
const tiers = m.tiers.runtime;
|
|
158
|
+
for (const [tier, def] of Object.entries(tiers)) {
|
|
159
|
+
for (const target of def.mayImport) if (!(target in tiers)) bad.push(`tiers.runtime.${tier}.mayImport names ${target}, which is not a runtime tier`);
|
|
160
|
+
}
|
|
161
|
+
const seen = new Set();
|
|
162
|
+
for (const entry of m.pending ?? []) {
|
|
163
|
+
const key = `${entry.path}\0${entry.rule}`;
|
|
164
|
+
if (seen.has(key)) bad.push(`pending names ${entry.path} for ${entry.rule} twice`);
|
|
165
|
+
seen.add(key);
|
|
166
|
+
}
|
|
167
|
+
const major = Number(m.version.split('.')[0]);
|
|
168
|
+
if (String(m.schema) !== `starci/runtime-slots@${major}`) bad.push(`schema ${m.schema} does not carry the major of version ${m.version}`);
|
|
169
|
+
return bad;
|
|
170
|
+
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
// hfs-path-findings.mjs - the per-path judgements of the slot manifest: what a tracked path is. One function, two surfaces: `hfs check`
|
|
2
2
|
// (checkRepo) runs it over the tracked tree and keeps the findings that have no TypeScript file to sit on; the lint canon's project graph
|
|
3
|
-
// (scripts/
|
|
3
|
+
// (scripts/hfs/project-graph.mjs) runs it over the same tree and serves the findings on a TypeScript file as ESLint reports.
|
|
4
4
|
// HFS_SLOT_UNDECLARED / HFS_SLOT_AMBIGUOUS / HFS_SLOT_NOT_ENABLED / HFS_FORBIDDEN_PRESENT / HFS_TOOL_CONFIG_LOCAL / HFS_TRACKED_MUST_BE_IGNORED (R01, R16)
|
|
5
5
|
// BE_SOURCE_FORM (R89, the file name), BE_SPEC_PLACEMENT (R102)
|
|
6
6
|
import path from 'node:path';
|
|
7
|
-
import { allowsFile } from './
|
|
8
|
-
import { isFeTestPath } from './
|
|
9
|
-
import { slotOwnsSecrets } from './
|
|
10
|
-
import { specPlacementFindings } from './
|
|
11
|
-
import { APP_SCOPE } from './
|
|
7
|
+
import { allowsFile } from './allows.mjs';
|
|
8
|
+
import { isFeTestPath } from './rules/fe-no-tests.mjs';
|
|
9
|
+
import { slotOwnsSecrets } from './rules/secrets.mjs';
|
|
10
|
+
import { specPlacementFindings } from './rules/spec-placement.mjs';
|
|
11
|
+
import { APP_SCOPE } from './slots.mjs';
|
|
12
12
|
|
|
13
13
|
const KEBAB = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
14
14
|
const SOURCE_ROOT = /^(?:src|apps)\//;
|
|
Binary file
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// project-rule.mjs - the factory of the lint rules that read the project graph (scripts/
|
|
1
|
+
// project-rule.mjs - the factory of the lint rules that read the project graph (scripts/hfs/project-graph.mjs).
|
|
2
2
|
//
|
|
3
3
|
// A project rule judges nothing itself: the graph (the architecture machine's module graph and slot manifest, built once per
|
|
4
4
|
// process) holds the findings of its codes, and the rule reports those of the file it is visiting, on the line the machine named.
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
// Both lint canons ship a byte copy of this file in runtime/ and build their rules with it.
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import { projectGraph } from './project-graph.mjs';
|
|
9
|
-
import { posixPath } from '
|
|
10
|
-
import { belongsTo, codeOf } from '
|
|
9
|
+
import { posixPath } from '../lib/path-key.mjs';
|
|
10
|
+
import { belongsTo, codeOf } from './architecture/surface.mjs';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* A rule reporting the graph findings of `codes` on the linted file.
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
// - a tracked `.enc` that is not a sops envelope (an encrypted-looking file that holds plaintext);
|
|
6
6
|
// - a line of any other tracked text file that matches a secret pattern.
|
|
7
7
|
// A finding names the file, the line and the pattern, never the value.
|
|
8
|
-
import { FORBIDDEN_FILES, secretHits } from '
|
|
9
|
-
import { isSopsEnvelope } from '
|
|
8
|
+
import { FORBIDDEN_FILES, secretHits } from '../../lib/secret-patterns.mjs';
|
|
9
|
+
import { isSopsEnvelope } from '../../lib/test-secrets.mjs';
|
|
10
10
|
import { found, readText } from './read.mjs';
|
|
11
11
|
|
|
12
12
|
export const PLAINTEXT_SECRET = 'HFS_PLAINTEXT_SECRET';
|