@starci/eslint-canon-be 1.8.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +95 -5
- package/README.md +41 -52
- package/async-discipline.mjs +2 -2
- package/authorization.mjs +38 -88
- package/builders.mjs +305 -0
- package/comments.mjs +19 -34
- package/config-owner.mjs +127 -125
- package/connections.mjs +254 -0
- package/cqrs.mjs +426 -135
- package/data-access.mjs +320 -391
- package/default-deny.mjs +14 -10
- package/e2e-flow.mjs +208 -180
- package/error-handling.mjs +254 -128
- package/idempotency.mjs +98 -67
- package/index.mjs +32 -9
- package/infra-owner.mjs +96 -0
- package/injection.mjs +602 -0
- package/input-bounds.mjs +166 -66
- package/lib/config.mjs +70 -33
- package/lib/db-writes.mjs +58 -0
- package/lib/declared.mjs +154 -0
- package/lib/hfs.mjs +61 -0
- package/lib/import-source.mjs +104 -0
- package/lib/persistence.mjs +160 -0
- package/lib/ports.mjs +138 -0
- package/lib/transport-slots.mjs +14 -0
- package/lib/types.mjs +133 -0
- package/lib/unit-spec.mjs +33 -0
- package/log-safety.mjs +94 -26
- package/module-layering.mjs +180 -297
- package/module-shape.mjs +296 -59
- package/naming.mjs +44 -12
- package/observability.mjs +57 -91
- package/operations.mjs +201 -0
- package/package.json +10 -7
- package/query-safety.mjs +189 -223
- package/resilience.mjs +126 -31
- package/runtime/engine/plain-object.mjs +5 -0
- package/runtime/engine/runtime-root.mjs +32 -0
- package/runtime/engine/yaml.mjs +161 -0
- package/runtime/knowledge/hfs/slots.yaml +1111 -0
- package/runtime/scripts/lib/git.mjs +53 -0
- package/runtime/scripts/lib/glob.mjs +23 -0
- package/runtime/scripts/lib/hfs-allows.mjs +57 -0
- package/runtime/scripts/lib/hfs-slots.mjs +729 -0
- package/runtime/scripts/lib/hfs-view.mjs +68 -0
- package/runtime/scripts/lib/language.mjs +107 -0
- package/runtime/scripts/lib/path-key.mjs +17 -0
- package/runtime/scripts/lib/recorded-lines.mjs +27 -0
- package/schema-authority.mjs +273 -91
- package/size-budget.mjs +19 -61
- package/spec-no-skip.mjs +84 -0
- package/spec-quality.mjs +76 -35
- package/suppression.mjs +34 -16
- package/temporal.mjs +101 -36
- package/test-world.mjs +204 -0
- package/testing.mjs +99 -251
- package/transport.mjs +433 -142
- package/type-safety.mjs +50 -278
- package/unit-spec.mjs +452 -0
- package/user-copy.mjs +56 -37
- package/lib/slots.mjs +0 -83
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// hfs-view.mjs - the frozen view of one repository's HFS slots that the lint factories hand their rules
|
|
2
|
+
// (`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 (packages/hfs/scripts/sync-runtime.mjs), so a path-scoped rule in either package asks the SAME resolver the
|
|
4
|
+
// architecture machine and `hfs check` use, and never tests a path with a regular expression of its own.
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { allowsFile } from './hfs-allows.mjs';
|
|
7
|
+
import { openHfs } from './hfs-slots.mjs';
|
|
8
|
+
|
|
9
|
+
const posix = (p) => String(p).replace(/\\/g, '/');
|
|
10
|
+
const isAbsoluteAny = (text) => path.isAbsolute(text) || /^[A-Za-z]:\//.test(text);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The view rules read, over an opened resolver.
|
|
14
|
+
*
|
|
15
|
+
* @param {object} opened - The result of `openHfs` (manifest, repo and resolver methods).
|
|
16
|
+
* @param {string} repoRoot - The absolute repository root.
|
|
17
|
+
* @returns {object} The frozen view: profile, apps, connections, ruleParams, relative, classify, slotOf, tierOf, ownerOf, slot.
|
|
18
|
+
*/
|
|
19
|
+
export function hfsView(opened, repoRoot) {
|
|
20
|
+
const rel = (file) => {
|
|
21
|
+
const text = posix(file);
|
|
22
|
+
if (!isAbsoluteAny(text)) return text.replace(/^\.\//, '');
|
|
23
|
+
return posix(path.relative(repoRoot, file));
|
|
24
|
+
};
|
|
25
|
+
const slotCache = new Map();
|
|
26
|
+
const classify = (file) => {
|
|
27
|
+
const key = rel(file);
|
|
28
|
+
if (!slotCache.has(key)) slotCache.set(key, opened.classifyPath(key));
|
|
29
|
+
return slotCache.get(key);
|
|
30
|
+
};
|
|
31
|
+
return Object.freeze({
|
|
32
|
+
repoRoot,
|
|
33
|
+
profile: opened.repo.profile,
|
|
34
|
+
apps: opened.repo.apps,
|
|
35
|
+
connections: opened.repo.connections,
|
|
36
|
+
ruleParams: opened.ruleParams(),
|
|
37
|
+
/** The repository-relative, forward-slash form of a linted filename. */
|
|
38
|
+
relative: rel,
|
|
39
|
+
/** The classification of a file: `{ slot, tier, owner?, bindings? ... }` or a no-slot status. */
|
|
40
|
+
classify,
|
|
41
|
+
/** The slot id that owns a file, or null when no slot does. */
|
|
42
|
+
slotOf: (file) => classify(file).slot ?? null,
|
|
43
|
+
/** The tier of a file, or null. */
|
|
44
|
+
tierOf: (file) => opened.tierOf(rel(file)) ?? null,
|
|
45
|
+
/** The owner root of a file, or null. */
|
|
46
|
+
ownerOf: (file) => opened.ownerOf(rel(file))?.root ?? null,
|
|
47
|
+
/** What the slot that owns a file says about it: `{ slot, root, relative, allowed, entry?, forbiddenBy?, allows }`, or null when the slot names no `allows`. */
|
|
48
|
+
allows: (file) => allowsFile(opened, rel(file)),
|
|
49
|
+
/** A slot definition by id. */
|
|
50
|
+
slot: (id) => opened.slot(id),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The view of the repository on disk whose root is `repoRoot`, against the manifest under `runtimeRoot`.
|
|
56
|
+
*
|
|
57
|
+
* @param {{ runtimeRoot: string, repoRoot: string }} input - Where the manifest copy lives and the repository root.
|
|
58
|
+
* @returns {object} The frozen view.
|
|
59
|
+
*/
|
|
60
|
+
export const openHfsView = ({ runtimeRoot, repoRoot }) => hfsView(openHfs({ root: runtimeRoot, repoRoot }), repoRoot);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The view of an in-memory declaration (rule tests): no file is read except the manifest.
|
|
64
|
+
*
|
|
65
|
+
* @param {{ runtimeRoot: string, declaration: object, repoRoot: string }} input - Manifest root, hfs.json object, linted root.
|
|
66
|
+
* @returns {object} The frozen view.
|
|
67
|
+
*/
|
|
68
|
+
export const declaredHfsView = ({ runtimeRoot, declaration, repoRoot }) => hfsView(openHfs({ root: runtimeRoot, declaration }), repoRoot);
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// language.mjs - the one home of "is this text English?" for source, comments, tests and docs (HFS_LANGUAGE_NOT_ENGLISH,
|
|
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 packages/hfs/scripts/sync-runtime.mjs).
|
|
4
|
+
//
|
|
5
|
+
// Detection is structural on characters, never a word list: the letters Vietnamese adds to the Latin alphabet (a-breve, a-circumflex,
|
|
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
|
|
7
|
+
// decomposed spelling (base letter + combining mark) is caught exactly like the precomposed one, and a loanword such as
|
|
8
|
+
// `naive`, `facade` or `Muller` (no tone mark, no Vietnamese letter) is never a hit.
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
|
|
11
|
+
/** One Vietnamese letter, precomposed (NFC): the Vietnamese-specific base letters, tone-marked vowels and the Latin Extended Additional block. */
|
|
12
|
+
export const SECOND_LANGUAGE_LETTER = /[À-ÃÈ-ÊÌÍÒ-ÕÙÚÝà-ãè-êìíò-õùúýĂăĐđĨĩŨũƠơƯưẠ-ỿ]/;
|
|
13
|
+
|
|
14
|
+
/** Whether `text` holds a Vietnamese letter, in NFC or NFD spelling. */
|
|
15
|
+
export const hasSecondLanguage = (text) => typeof text === 'string' && SECOND_LANGUAGE_LETTER.test(text.normalize('NFC'));
|
|
16
|
+
|
|
17
|
+
/** Every line of `text` with a Vietnamese letter: `[{ line, column }]`, both 1-based. */
|
|
18
|
+
export function secondLanguageHits(text) {
|
|
19
|
+
const hits = [];
|
|
20
|
+
String(text).split(/\r?\n/).forEach((raw, index) => {
|
|
21
|
+
const match = SECOND_LANGUAGE_LETTER.exec(raw.normalize('NFC'));
|
|
22
|
+
if (match) hits.push({ line: index + 1, column: match.index + 1 });
|
|
23
|
+
});
|
|
24
|
+
return hits;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
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.
|
|
30
|
+
*/
|
|
31
|
+
export const FAILURE_CODE_VIETNAMESE_FIELDS = Object.freeze(['title_vi', 'meaning_vi', 'nextStep_vi', 'causes_vi']);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The fields of a document that carry deliberate Vietnamese text, keyed by file: a declared field-level exception of that file's
|
|
35
|
+
* typed content, never a path pattern over content. `fields` are block keys (the key's own value and its list items are exempt);
|
|
36
|
+
* `flowFields` are keys inside an inline flow map (`{ vi: ..., en: ... }`), where only that key's value is exempt. A Vietnamese
|
|
37
|
+
* value under any other key of the same file is still a finding.
|
|
38
|
+
* - modules/kernel/failure-codes.yaml: the operator text the owner mandated for the failure-code catalog.
|
|
39
|
+
* - modules/ops/_labels.yaml: the op-label catalogue, one `{ vi, en }` pair per op (the `vi` field is the localized label).
|
|
40
|
+
* - modules/goal/archetypes.yaml: the Vietnamese phrase lexicons matched against owner input (the signal phrase sets, and the
|
|
41
|
+
* phrase lists of the archetype recognisers).
|
|
42
|
+
*/
|
|
43
|
+
export const DECLARED_VIETNAMESE_FIELDS = Object.freeze({
|
|
44
|
+
'modules/kernel/failure-codes.yaml': Object.freeze({ fields: FAILURE_CODE_VIETNAMESE_FIELDS }),
|
|
45
|
+
'modules/ops/_labels.yaml': Object.freeze({ flowFields: Object.freeze(['vi']) }),
|
|
46
|
+
'modules/goal/archetypes.yaml': Object.freeze({
|
|
47
|
+
fields: Object.freeze([
|
|
48
|
+
'buildIntent', 'canonIntent', 'e2eIntent', 'uatIntent', 'proofNegation', 'integrationIntent', 'integrationNegation', 'brandIntent',
|
|
49
|
+
'phrases', 'requires', 'excludes', 'backend', 'frontend', 'package',
|
|
50
|
+
]),
|
|
51
|
+
}),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The slots whose files may carry another language, and why: a catalog is product copy in two languages, and an i18n
|
|
56
|
+
* fixture reproduces a real localized string a parser or formatter must accept. Placement is the whole marker - there is no
|
|
57
|
+
* comment pragma and no path pattern. The slots are declared in knowledge/hfs/slots.yaml.
|
|
58
|
+
*/
|
|
59
|
+
export const LOCALIZED_TEXT_SLOTS = Object.freeze([
|
|
60
|
+
'be.domain.messages', 'be.feature.messages', 'be.tests.fixtures.i18n',
|
|
61
|
+
'fe.modules.i18n', 'fe.package.i18n',
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
/** Whether a document (Markdown or YAML) of `slot` is exempt: only catalogue and fixture DATA (YAML) is; Markdown is prose and never is. */
|
|
65
|
+
export const isLocalizedDataFile = (rel, slot) => LOCALIZED_TEXT_SLOTS.includes(slot) && /\.ya?ml$/.test(rel);
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The key each line of a YAML text belongs to: a `key:` line owns itself, and every deeper-indented line after it (a list item,
|
|
69
|
+
* a folded block, a wrapped scalar) belongs to the same key. A line at or above the key's indent that is not a key belongs to none.
|
|
70
|
+
*/
|
|
71
|
+
export function yamlKeyOfEachLine(text) {
|
|
72
|
+
let key = null;
|
|
73
|
+
let keyIndent = -1;
|
|
74
|
+
return String(text).split(/\r?\n/).map((raw) => {
|
|
75
|
+
if (raw.trim() === '') return key;
|
|
76
|
+
const indent = raw.length - raw.trimStart().length;
|
|
77
|
+
const own = /^(\s*(?:-\s+)?)([A-Za-z_][\w-]*)\s*:(?:\s|$)/.exec(raw);
|
|
78
|
+
if (own) { key = own[2]; keyIndent = own[1].length; return key; }
|
|
79
|
+
// a list item may sit at the key's own indent (`key:` then `- item` at the same column)
|
|
80
|
+
if (indent > keyIndent || (indent === keyIndent && /^\s*-\s/.test(raw))) return key;
|
|
81
|
+
key = null;
|
|
82
|
+
keyIndent = -1;
|
|
83
|
+
return null;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The Vietnamese hits of a document, minus the declared field-level exceptions of `rel` (a repository-relative POSIX path). */
|
|
88
|
+
export function documentLanguageHits(rel, text) {
|
|
89
|
+
const declared = DECLARED_VIETNAMESE_FIELDS[rel];
|
|
90
|
+
const hits = secondLanguageHits(text);
|
|
91
|
+
if (!declared) return hits;
|
|
92
|
+
const keys = yamlKeyOfEachLine(text);
|
|
93
|
+
const lines = String(text).split(/\r?\n/);
|
|
94
|
+
return hits.filter((hit) => {
|
|
95
|
+
if (declared.fields?.includes(keys[hit.line - 1])) return false;
|
|
96
|
+
if (!declared.flowFields) return true;
|
|
97
|
+
// an inline flow map: drop the declared fields' values and judge what is left of the line
|
|
98
|
+
const rest = declared.flowFields.reduce((line, field) => line.replace(new RegExp(`\\b${field}\\s*:\\s*[^,}]*`, 'g'), ''), lines[hit.line - 1]);
|
|
99
|
+
return hasSecondLanguage(rest);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The file extensions of a prose document (Markdown and YAML). */
|
|
104
|
+
export const DOCUMENT_EXTENSIONS = Object.freeze(['.md', '.yaml', '.yml']);
|
|
105
|
+
|
|
106
|
+
/** Whether `rel` is a document a language check reads. */
|
|
107
|
+
export const isDocument = (rel) => DOCUMENT_EXTENSIONS.includes(path.posix.extname(rel));
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// path-key.mjs — spelling and comparing paths the way this host's filesystem does.
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const WIN = process.platform === 'win32';
|
|
5
|
+
|
|
6
|
+
/** `p` with every backslash turned into a forward slash; null and undefined read as ''. */
|
|
7
|
+
export const slash = (p) => String(p ?? '').replaceAll('\\', '/');
|
|
8
|
+
/** A relative path in git's spelling: forward slashes, no leading './'. */
|
|
9
|
+
export const posixPath = (p) => slash(p).replace(/^\.\//, '');
|
|
10
|
+
/** Whether posix `file` is `prefix` itself or under it (`file === prefix || file` starts with `prefix/`). */
|
|
11
|
+
export const sameOrUnder = (file, prefix) => file === prefix || file.startsWith(`${prefix}/`);
|
|
12
|
+
/** `p` case-folded where the filesystem ignores case (Windows). */
|
|
13
|
+
export const foldCase = (p) => (WIN ? p.toLowerCase() : p);
|
|
14
|
+
/** Whether two spellings name the same path on this host's filesystem. */
|
|
15
|
+
export const samePath = (a, b) => foldCase(a) === foldCase(b);
|
|
16
|
+
/** A path's comparable identity: absolute, forward slashes, no trailing slash, case-folded on Windows. */
|
|
17
|
+
export const pathKey = (p) => foldCase(slash(path.resolve(p)).replace(/\/+$/, ''));
|
|
@@ -0,0 +1,27 @@
|
|
|
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 './git.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
|
+
};
|