@starci/eslint-grammar-offset-pop 0.12.13 → 0.13.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 +10 -0
- package/README.md +4 -4
- package/config.mjs +2 -2
- package/index.d.ts +4 -4
- package/index.mjs +1 -1
- package/package.json +23 -15
- package/runtime/engine/by-code-unit.mjs +12 -0
- package/runtime/engine/config.mjs +461 -0
- package/runtime/engine/invalid-config.mjs +15 -0
- package/runtime/engine/model-config.mjs +90 -0
- package/runtime/engine/orca-config.mjs +29 -0
- package/runtime/engine/release-config.mjs +28 -0
- package/runtime/engine/removed-vocabulary.mjs +26 -0
- package/runtime/engine/resources-config.mjs +52 -0
- package/runtime/engine/secrets.mjs +134 -0
- package/runtime/engine/sonar-config.mjs +21 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +19 -20
- package/runtime/modules/kernel/removed-vocabulary.yaml +458 -0
- package/runtime/modules/models/registry.yaml +221 -0
- package/runtime/modules/models/runtimes.yaml +556 -0
- package/runtime/modules/models/tiers.yaml +66 -0
- package/runtime/scripts/hfs/declaration-shape.mjs +157 -0
- package/runtime/scripts/hfs/declaration-slots.mjs +86 -0
- package/runtime/scripts/hfs/edition-slots.mjs +77 -0
- package/runtime/scripts/hfs/manifest-shape.mjs +435 -0
- package/runtime/scripts/hfs/param-names.mjs +18 -0
- package/runtime/scripts/hfs/rule-catalog.mjs +362 -0
- package/runtime/scripts/hfs/rule-params-shape.mjs +80 -0
- package/runtime/scripts/hfs/slot-app-view.mjs +102 -0
- package/runtime/scripts/hfs/slot-classify.mjs +65 -0
- package/runtime/scripts/hfs/slot-errors.mjs +11 -0
- package/runtime/scripts/hfs/slot-imports.mjs +91 -0
- package/runtime/scripts/hfs/slot-manifest-shape.mjs +73 -0
- package/runtime/scripts/hfs/slot-match.mjs +153 -0
- package/runtime/scripts/hfs/slot-path.mjs +8 -0
- package/runtime/scripts/hfs/slot-required.mjs +89 -0
- package/runtime/scripts/hfs/slot-semantic-problems.mjs +149 -0
- package/runtime/scripts/hfs/slot-side-problems.mjs +28 -0
- package/runtime/scripts/lib/i18n.mjs +85 -0
- package/runtime/scripts/lib/json-clone.mjs +61 -0
- package/runtime/scripts/lib/list.mjs +31 -0
- package/runtime/scripts/lib/regex.mjs +4 -0
- package/runtime/scripts/lib/yaml-cached.mjs +14 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// declaration-shape.mjs - the dependency-free hfs.json shape validation used by the HFS slot loader. Semantic checks
|
|
2
|
+
// against a loaded manifest remain in slots.mjs; this module mirrors modules/schemas/hfs-repo.schema.yaml only.
|
|
3
|
+
import { isPlainObject } from '../../engine/plain-object.mjs';
|
|
4
|
+
import { APP_KIND, connectionShapeProblems, NAME, PACKAGE_NAME, RUNTIME_KIND, SLOT_ID } from './manifest-shape.mjs';
|
|
5
|
+
import { kindShapeProblems, patternShapeProblems } from './declaration-slots.mjs';
|
|
6
|
+
import { byCodeUnit } from '../lib/list.mjs';
|
|
7
|
+
|
|
8
|
+
/** Shape problems of the optional hfs.json `supabase` block whose auth posture DB_CONFIG_POLICY checks. */
|
|
9
|
+
const isSupabaseStringList = (value) => Array.isArray(value) && value.every((entry) => typeof entry === 'string' && entry.trim());
|
|
10
|
+
|
|
11
|
+
function supabaseValueProblems(block) {
|
|
12
|
+
const bad = [];
|
|
13
|
+
if (block.enableSignup !== undefined && typeof block.enableSignup !== 'boolean') bad.push('supabase.enableSignup must be true or false');
|
|
14
|
+
if (block.jwtExpiry !== undefined && !(Number.isInteger(block.jwtExpiry) && block.jwtExpiry > 0 && block.jwtExpiry <= 3600)) bad.push('supabase.jwtExpiry must be an integer of seconds, 3600 at most');
|
|
15
|
+
if (block.siteUrl !== undefined && (typeof block.siteUrl !== 'string' || !block.siteUrl.trim())) bad.push('supabase.siteUrl must be a URL string');
|
|
16
|
+
for (const key of ['redirectUrls', 'forceRls']) if (block[key] !== undefined && !isSupabaseStringList(block[key])) bad.push(`supabase.${key} must be a list of strings`);
|
|
17
|
+
return bad;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function supabaseBlockProblems(block) {
|
|
21
|
+
if (block === undefined) return [];
|
|
22
|
+
if (!isPlainObject(block)) return ['supabase must be an object'];
|
|
23
|
+
const bad = [];
|
|
24
|
+
for (const key of Object.keys(block)) if (!['enableSignup', 'jwtExpiry', 'siteUrl', 'redirectUrls', 'forceRls'].includes(key)) bad.push(`supabase has unknown key ${key}`);
|
|
25
|
+
bad.push(...supabaseValueProblems(block));
|
|
26
|
+
return bad;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function appShapeProblems(apps, at, side, names) {
|
|
30
|
+
const bad = [];
|
|
31
|
+
if (!Array.isArray(apps) || !apps.length) {
|
|
32
|
+
bad.push(`${at}.apps must list every ${side}/apps/<name> with its kind`);
|
|
33
|
+
return bad;
|
|
34
|
+
}
|
|
35
|
+
apps.forEach((app, index) => {
|
|
36
|
+
if (!isPlainObject(app) || !NAME.test(String(app.name)) || !NAME.test(String(app.kind)) || Object.keys(app).some((key) => key !== 'name' && key !== 'kind')) bad.push(`${at}.apps[${index}] must be {name, kind}`);
|
|
37
|
+
else if (names.has(app.name)) bad.push(`app ${app.name} is declared twice (${names.get(app.name)} and ${side})`);
|
|
38
|
+
else names.set(app.name, side);
|
|
39
|
+
});
|
|
40
|
+
return bad;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function connectionProblems(side, value) {
|
|
44
|
+
const bad = [];
|
|
45
|
+
if (side !== 'be') {
|
|
46
|
+
bad.push('connections belong to the be side');
|
|
47
|
+
return bad;
|
|
48
|
+
}
|
|
49
|
+
const list = Array.isArray(value.connections) ? value.connections : null;
|
|
50
|
+
const shape = connectionShapeProblems(list, value.apps);
|
|
51
|
+
if (shape.length) {
|
|
52
|
+
bad.push(...shape);
|
|
53
|
+
return bad;
|
|
54
|
+
}
|
|
55
|
+
if (new Set(list.map((connection) => connection.name)).size !== list.length) bad.push('connections names must be unique');
|
|
56
|
+
for (const a of list) for (const b of list) if (a !== b && `${b.envPrefix}_`.startsWith(`${a.envPrefix}_`)) bad.push(`connections ${a.name} and ${b.name} share env keys (${a.envPrefix}_ covers ${b.envPrefix}_)`);
|
|
57
|
+
return bad;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Whether a declared list is present but not a list of distinct entries that all satisfy `valid`. */
|
|
61
|
+
const isBadUniqueList = (list, valid) => list !== undefined && (!Array.isArray(list) || !list.every((entry) => valid(entry)) || new Set(list).size !== list.length);
|
|
62
|
+
|
|
63
|
+
function validExtraPackage(entry) {
|
|
64
|
+
return isPlainObject(entry) && Object.keys(entry).every(key => key === 'name' || key === 'reason')
|
|
65
|
+
&& typeof entry.name === 'string' && entry.name.length <= 214 && PACKAGE_NAME.test(entry.name)
|
|
66
|
+
&& typeof entry.reason === 'string' && entry.reason.trim().length > 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function extraPackageProblems(side, entries) {
|
|
70
|
+
if (entries === undefined) return [];
|
|
71
|
+
if (side !== 'fe') return ['extraPackages belong to the fe side'];
|
|
72
|
+
if (!Array.isArray(entries) || !entries.every(validExtraPackage)) return ['sides.fe.extraPackages must be a list of {name, reason} with a package name and a nonempty reason'];
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** `sides.be.queueOutbox`: the app's own queue outbox ({table, enqueue, queueParameter}), which a pg_cron tick may enqueue through (R258); it needs the redis backing the relay hands rows to. */
|
|
77
|
+
function queueOutboxProblems(side, value) {
|
|
78
|
+
const declared = value.queueOutbox;
|
|
79
|
+
if (declared === undefined) return [];
|
|
80
|
+
if (side !== 'be') return ['queueOutbox belongs to the be side'];
|
|
81
|
+
const name = (item) => typeof item === 'string' && /^[a-z_][a-z0-9_]*\.[a-z_][a-z0-9_]*$/.test(item);
|
|
82
|
+
const bad = [];
|
|
83
|
+
if (!isPlainObject(declared) || Object.keys(declared).sort().join() !== 'enqueue,queueParameter,table' || !name(declared.table) || !name(declared.enqueue) || !/^[a-z_][a-z0-9_]*$/.test(String(declared.queueParameter))) bad.push('sides.be.queueOutbox must be {table: <schema>.<table>, enqueue: <schema>.<function>, queueParameter: <parameter name>}');
|
|
84
|
+
if (!(value.backing ?? []).includes('redis')) bad.push('sides.be.queueOutbox needs sides.be.backing to declare redis, which the relay hands its rows to');
|
|
85
|
+
return bad;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function grammarProblems(side, grammar) {
|
|
89
|
+
if (grammar === undefined) return [];
|
|
90
|
+
if (side !== 'fe') return ['grammar belongs to the fe side'];
|
|
91
|
+
return typeof grammar === 'string' && NAME.test(grammar) ? [] : ['sides.fe.grammar must name a Grammar family'];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function declarationSideProblems(side, value, names) {
|
|
95
|
+
const bad = [];
|
|
96
|
+
const at = `sides.${side}`;
|
|
97
|
+
if (!isPlainObject(value)) return [`${at} must be an object`];
|
|
98
|
+
for (const key of Object.keys(value)) if (!['apps', 'optionalSlots', 'patterns', 'kinds', 'connections', 'reads', 'extraPackages', 'grammar', 'backing', 'queueOutbox'].includes(key)) bad.push(`${at} has unknown key ${key}`);
|
|
99
|
+
bad.push(...appShapeProblems(value.apps, at, side, names));
|
|
100
|
+
if (isBadUniqueList(value.optionalSlots, (slot) => SLOT_ID.test(String(slot)))) bad.push(`${at}.optionalSlots must be a unique list of slot ids`);
|
|
101
|
+
if (value.backing !== undefined && side !== 'be') bad.push('backing belongs to the be side');
|
|
102
|
+
else if (isBadUniqueList(value.backing, (name) => NAME.test(String(name)))) bad.push(`${at}.backing must be a unique list of backing service names`);
|
|
103
|
+
bad.push(...patternShapeProblems(value, at, NAME), ...kindShapeProblems(value, at, NAME));
|
|
104
|
+
if (isBadUniqueList(value.reads, (read) => typeof read === 'string' && read.length > 0)) bad.push(`${at}.reads must be a unique list of paths`);
|
|
105
|
+
if (value.connections !== undefined) bad.push(...connectionProblems(side, value));
|
|
106
|
+
bad.push(...extraPackageProblems(side, value.extraPackages), ...grammarProblems(side, value.grammar), ...queueOutboxProblems(side, value));
|
|
107
|
+
return bad;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Product vocabulary is a declaration, never a switch for a rule. */
|
|
111
|
+
function lexiconBlockProblems(lexicon) {
|
|
112
|
+
if (lexicon === undefined) return [];
|
|
113
|
+
if (!isPlainObject(lexicon)) return ['lexicon must be an object'];
|
|
114
|
+
const bad = [];
|
|
115
|
+
for (const key of Object.keys(lexicon)) if (key !== 'removed') bad.push(`lexicon has unknown key ${key}`);
|
|
116
|
+
if (isBadUniqueList(lexicon.removed, (word) => typeof word === 'string' && word.trim().length > 0)) bad.push('lexicon.removed must be a unique list of nonempty words');
|
|
117
|
+
return bad;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Front-end error reporting remains optional in both editions. */
|
|
121
|
+
function observeBlockProblems(observe) {
|
|
122
|
+
if (observe === undefined) return [];
|
|
123
|
+
if (!isPlainObject(observe)) return ['observe must be an object'];
|
|
124
|
+
const bad = [];
|
|
125
|
+
for (const key of Object.keys(observe)) if (key !== 'reporter') bad.push(`observe has unknown key ${key}`);
|
|
126
|
+
if (observe.reporter !== undefined && (typeof observe.reporter !== 'string' || !observe.reporter.trim())) bad.push('observe.reporter must name the optional error reporter');
|
|
127
|
+
return bad;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const DECLARATION_KEYS = new Set(['hfs', 'kind', 'project', 'edition', 'supabase', 'sides', 'browser', 'lexicon', 'observe']);
|
|
131
|
+
const RUNTIME_DECLARATION_KEYS = new Set(['hfs', 'kind', 'project']);
|
|
132
|
+
|
|
133
|
+
function productDeclarationProblems(declaration, profiles) {
|
|
134
|
+
const bad = [];
|
|
135
|
+
for (const key of Object.keys(declaration)) if (!DECLARATION_KEYS.has(key)) bad.push(`unknown key ${key}`);
|
|
136
|
+
bad.push(...supabaseBlockProblems(declaration.supabase), ...lexiconBlockProblems(declaration.lexicon), ...observeBlockProblems(declaration.observe));
|
|
137
|
+
if (declaration.browser !== undefined && declaration.browser !== true) bad.push('browser is `true` when the app has a browser journey (slot app.browser), and is left out otherwise');
|
|
138
|
+
if (declaration.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)`);
|
|
139
|
+
if (!isPlainObject(declaration.sides) || Object.keys(declaration.sides).sort(byCodeUnit).join() !== profiles.join()) { bad.push('sides must declare exactly be and fe'); return bad; }
|
|
140
|
+
const names = new Map();
|
|
141
|
+
for (const side of profiles) bad.push(...declarationSideProblems(side, declaration.sides[side], names));
|
|
142
|
+
return bad;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Shape problems of a parsed hfs.json, in the words of modules/schemas/hfs-repo.schema.yaml. */
|
|
146
|
+
export function declarationShapeProblems(declaration, profiles) {
|
|
147
|
+
const bad = [];
|
|
148
|
+
if (!isPlainObject(declaration)) return ['hfs.json is not an object'];
|
|
149
|
+
if (!(Number.isInteger(declaration.hfs) && declaration.hfs >= 1)) bad.push('hfs must be the pinned manifest major (an integer, 1 or more)');
|
|
150
|
+
if (!NAME.test(String(declaration.project))) bad.push('project must be a project name');
|
|
151
|
+
if (declaration.kind === RUNTIME_KIND) {
|
|
152
|
+
for (const key of Object.keys(declaration)) if (!RUNTIME_DECLARATION_KEYS.has(key)) bad.push(`unknown key ${key} (a runtime declaration is {hfs, kind, project})`);
|
|
153
|
+
return bad;
|
|
154
|
+
}
|
|
155
|
+
bad.push(...productDeclarationProblems(declaration, profiles));
|
|
156
|
+
return bad;
|
|
157
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// declaration-slots.mjs - how an app declaration (hfs.json) switches on the opt-in slots of a side: by name (`optionalSlots`), by app kind
|
|
2
|
+
// (a slot with `appKind`) and by declared pattern (a slot with `pattern: <name>` is enabled by `sides.<side>.patterns: [<name>]`, one
|
|
3
|
+
// mechanism for pattern slots). A cross-cutting pattern can instead be owned by the side's canonical proof scenario catalog.
|
|
4
|
+
// slots.mjs asks this file to judge a side's lists and to say whether a slot is enabled for a repository.
|
|
5
|
+
import { judgedInEdition } from './edition-slots.mjs';
|
|
6
|
+
|
|
7
|
+
/** The problems of a side's `optionalSlots` and `patterns` against the manifest. */
|
|
8
|
+
export function optionalSlotProblems(manifest, side, s) {
|
|
9
|
+
const bad = [];
|
|
10
|
+
for (const id of s.optionalSlots ?? []) {
|
|
11
|
+
const slot = manifest.slots.find((candidate) => candidate.id === id);
|
|
12
|
+
if (!slot?.profiles.includes(side)) bad.push(`sides.${side}.optionalSlots names ${id}, which is not a ${side} slot`);
|
|
13
|
+
else if (slot.presence !== 'opt-in') bad.push(`sides.${side}.optionalSlots names ${id}, which is ${slot.presence}, not opt-in`);
|
|
14
|
+
else if (slot.appKind !== undefined) bad.push(`sides.${side}.optionalSlots names ${id}; an app of kind ${slot.appKind} enables it`);
|
|
15
|
+
}
|
|
16
|
+
for (const name of s.patterns ?? []) {
|
|
17
|
+
const ownsSlot = manifest.slots.some((slot) => slot.profiles.includes(side) && slot.pattern === name);
|
|
18
|
+
const ownsProof = Object.hasOwn(manifest.ruleParams?.[side]?.patternScenarios ?? {}, name);
|
|
19
|
+
if (!ownsSlot && !ownsProof) bad.push(`sides.${side}.patterns names ${name}, which neither a ${side} slot nor its proof scenario catalog declares`);
|
|
20
|
+
}
|
|
21
|
+
if (s.kinds !== undefined && side !== 'be') bad.push(`sides.${side}.kinds belongs to the be side`);
|
|
22
|
+
for (const kind of s.kinds ?? []) {
|
|
23
|
+
if (!(manifest.triggerKinds ?? []).includes(kind)) bad.push(`sides.${side}.kinds names ${kind}, which is not one of triggerKinds (${(manifest.triggerKinds ?? []).join(', ')})`);
|
|
24
|
+
}
|
|
25
|
+
return bad;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The problems of a side's `backing` list: every name is admitted by ruleParams.be.backing in this edition. */
|
|
29
|
+
export function backingProblems(manifest, side, s, edition = 'full') {
|
|
30
|
+
const services = Object.entries(manifest.ruleParams?.be?.backing ?? {})
|
|
31
|
+
.filter(([, entry]) => judgedInEdition(entry, edition)).map(([name]) => name);
|
|
32
|
+
return (s.backing ?? []).filter((name) => !services.includes(name))
|
|
33
|
+
.map((name) => `sides.${side}.backing names ${name}, which is not admitted in edition ${edition} by ruleParams.be.backing (${services.join(', ')})`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The problems of the shape of a side's `patterns` list (a unique list of pattern names). */
|
|
37
|
+
export function patternShapeProblems(s, at, namePattern) {
|
|
38
|
+
const list = s.patterns;
|
|
39
|
+
if (list === undefined) return [];
|
|
40
|
+
return Array.isArray(list) && list.every((value) => namePattern.test(String(value))) && new Set(list).size === list.length ? [] : [`${at}.patterns must be a unique list of pattern names`];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The problems of the shape of a side's `kinds` list (a unique list of trigger kind names). */
|
|
44
|
+
export function kindShapeProblems(s, at, namePattern) {
|
|
45
|
+
const list = s.kinds;
|
|
46
|
+
if (list === undefined) return [];
|
|
47
|
+
return Array.isArray(list) && list.every((value) => namePattern.test(String(value))) && new Set(list).size === list.length ? [] : [`${at}.kinds must be a unique list of trigger kind names`];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The problems of `ruleParams.be.kindPatterns` (trigger kind -> patterns) and `ruleParams.be.addKinds` (starci app add noun -> {topic, variable, patterns, trigger?, needs?, also?, defaults?, wire?}). */
|
|
51
|
+
export function kindParamProblems(be, triggerKinds) {
|
|
52
|
+
const kebab = /^[a-z][a-z0-9-]*$/;
|
|
53
|
+
const names = (list) => Array.isArray(list) && list.every((value) => kebab.test(String(value))) && new Set(list).size === list.length;
|
|
54
|
+
const bad = [];
|
|
55
|
+
const patterns = be.kindPatterns;
|
|
56
|
+
if (patterns === null || typeof patterns !== 'object' || Array.isArray(patterns) || !Object.entries(patterns).every(([kind, list]) => (triggerKinds ?? []).includes(kind) && names(list))) bad.push('ruleParams.be.kindPatterns must map a trigger kind to a unique list of pattern names');
|
|
57
|
+
const nouns = be.addKinds;
|
|
58
|
+
const nounOk = (spec) => spec !== null && typeof spec === 'object' && kebab.test(String(spec.topic)) && kebab.test(String(spec.variable)) && names(spec.patterns) && (spec.trigger === undefined || kebab.test(String(spec.trigger))) && (spec.needs === undefined || names(spec.needs)) && (spec.also === undefined || names(spec.also)) && (spec.defaults === undefined || (typeof spec.defaults === 'object' && !Array.isArray(spec.defaults))) && (spec.wire === undefined || (spec.wire !== null && typeof spec.wire === 'object' && ['file', 'import', 'symbol'].every((key) => typeof spec.wire[key] === 'string' && spec.wire[key] !== '')));
|
|
59
|
+
if (nouns === null || typeof nouns !== 'object' || Array.isArray(nouns) || !Object.entries(nouns).every(([noun, spec]) => kebab.test(noun) && nounOk(spec))) bad.push('ruleParams.be.addKinds must map a noun to {topic, variable, patterns, trigger?, needs?, also?, defaults?}');
|
|
60
|
+
return bad;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The problems of `ruleParams.be.patternScenarios`: a pattern name to a non-empty list of unique kebab-case scenario ids. */
|
|
64
|
+
export function scenarioProblem(scenarios) {
|
|
65
|
+
const kebab = /^[a-z][a-z0-9-]*$/;
|
|
66
|
+
const ok = scenarios !== null && typeof scenarios === 'object' && !Array.isArray(scenarios) && Object.entries(scenarios).every(([name, list]) => kebab.test(name) && Array.isArray(list) && list.length > 0 && list.every((id) => kebab.test(String(id))) && new Set(list).size === list.length);
|
|
67
|
+
return ok ? [] : ['ruleParams.be.patternScenarios must map a pattern name to a non-empty list of unique kebab-case scenario ids'];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The problems of the `trigger` of a slot against the manifest's `triggerKinds`: a known kind, on a feature-tier slot only. */
|
|
71
|
+
export function triggerProblems(slot, triggerKinds) {
|
|
72
|
+
if (slot.trigger === undefined) return [];
|
|
73
|
+
const bad = [];
|
|
74
|
+
if (!(triggerKinds ?? []).includes(slot.trigger)) bad.push(`slot ${slot.id}: trigger ${slot.trigger} is not one of triggerKinds`);
|
|
75
|
+
if (slot.tier !== 'feature') bad.push(`slot ${slot.id}: a trigger belongs to a feature-tier slot`);
|
|
76
|
+
return bad;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Whether an opt-in slot is enabled for the repository: by its app kind, by a connection's provider, by name in optionalSlots, or by its declared pattern. */
|
|
80
|
+
export function declaredSlotEnabled(slot, repo) {
|
|
81
|
+
if (slot.appKind !== undefined) return repo.apps.some((app) => app.kind === slot.appKind);
|
|
82
|
+
// A provider declared on any connection (be owns them all today) enables the provider slots of every profile: repo.providers
|
|
83
|
+
// is the app-wide union the declaration resolver computed; a hand-built repo falls back to its own connections.
|
|
84
|
+
if (slot.provider !== undefined) return (repo.providers ?? (repo.connections ?? []).map((connection) => connection.provider)).includes(slot.provider);
|
|
85
|
+
return repo.optionalSlots.includes(slot.id) || (slot.pattern !== undefined && (repo.patterns ?? []).includes(slot.pattern));
|
|
86
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// edition.mjs - the one edition filter shared by the HFS slot resolver, declaration validation, rule catalog and
|
|
2
|
+
// edition-specific rule parameters. Full is the default; lite changes data views, never the checks that consume them.
|
|
3
|
+
// Lite uses provider adapters and declares Docker Swarm production infrastructure (owner:lite-production-swarm, ruling:R212/case-4).
|
|
4
|
+
// Its overlays exclude the e2e world, and every slot keeps its own `tests`.
|
|
5
|
+
import { isPlainObject } from '../../engine/plain-object.mjs';
|
|
6
|
+
import { EDITIONS } from './manifest-shape.mjs';
|
|
7
|
+
|
|
8
|
+
/** The presence of `slot` under edition lite for `profile`: `litePresence`, else the slot's own presence. */
|
|
9
|
+
export function litePresenceOf(slot, profile) {
|
|
10
|
+
const lp = slot.litePresence;
|
|
11
|
+
if (lp === undefined) return slot.presence;
|
|
12
|
+
return isPlainObject(lp) ? (lp[profile] ?? slot.presence) : lp;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Whether `slot` exists under `edition` of `manifest` (default: the manifest's own editions). */
|
|
16
|
+
export function slotInEdition(manifest, slot, edition) {
|
|
17
|
+
return (slot.editions ?? manifest.editions ?? EDITIONS).includes(edition);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The slot as `edition` sees it: lite overlays its data, presence and tracking; `tests` stays the slot's own (lite keeps the unit specs). */
|
|
21
|
+
export function effectiveSlot(slot, profile, edition) {
|
|
22
|
+
if (edition !== 'lite') return slot;
|
|
23
|
+
const { lite, litePresence, ...base } = slot;
|
|
24
|
+
const presence = litePresenceOf(slot, profile);
|
|
25
|
+
let tracked = slot.tracked;
|
|
26
|
+
if (presence === 'forbidden') tracked = 'external';
|
|
27
|
+
else if (tracked === 'external') tracked = 'tracked';
|
|
28
|
+
return { ...base, ...lite, presence, tracked };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The template group starci app sync renders `slot` from under `edition`: `liteManagedBy` (else `managedBy`) in lite, `managedBy` otherwise; undefined when unmanaged. */
|
|
32
|
+
export function managedGroupOf(slot, edition) {
|
|
33
|
+
return edition === 'lite' ? (slot.liteManagedBy ?? slot.managedBy) : slot.managedBy;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The declared edition and the vocabulary against which it is validated. */
|
|
37
|
+
export function declarationEdition(manifest, declaration) {
|
|
38
|
+
const edition = declaration.edition === undefined ? 'full' : declaration.edition;
|
|
39
|
+
const known = manifest.editions ?? EDITIONS;
|
|
40
|
+
return { edition, known, valid: known.includes(edition) };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Problems of an optional rule-catalog `editions` list, in the catalog schema's words. */
|
|
44
|
+
export function ruleEditionProblems(rule, label) {
|
|
45
|
+
if (rule.editions === undefined) return [];
|
|
46
|
+
const bad = [];
|
|
47
|
+
if (!Array.isArray(rule.editions) || !rule.editions.length) return [`${label}.editions must be a non-empty list`];
|
|
48
|
+
for (const value of rule.editions) if (!EDITIONS.includes(value)) bad.push(`${label}.editions has ${JSON.stringify(value)}, not one of ${EDITIONS.join(', ')}`);
|
|
49
|
+
if (new Set(rule.editions).size !== rule.editions.length) bad.push(`${label}.editions repeats a value`);
|
|
50
|
+
return bad;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Whether a catalogued rule owner judges its finding under `edition`; uncatalogued codes always remain judged. */
|
|
54
|
+
export function judgedInEdition(owner, edition = 'full') {
|
|
55
|
+
return !owner?.editions || owner.editions.includes(edition);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Whether the enforcer `kind:id` of the catalog `rules` runs under `edition`: every rule listing it is judged there, and the enforcer row's own `editions` (a rule with several enforcers where only one is full-only) agrees. A lint rule no rule lists runs everywhere. */
|
|
59
|
+
export function enforcerJudgedInEdition(rules, kind, id, edition = 'full') {
|
|
60
|
+
const owners = rules.filter((rule) => rule.enforcers.some((enforcer) => enforcer.kind === kind && enforcer.id === id));
|
|
61
|
+
return owners.every((rule) => judgedInEdition(rule, edition) && rule.enforcers.filter((enforcer) => enforcer.kind === kind && enforcer.id === id).every((enforcer) => judgedInEdition(enforcer, edition)));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A frozen copy of one profile's parameters, with its lite overrides applied only in the lite edition. A lite key whose base
|
|
66
|
+
* value and override are both maps merges one level deep (lite.infraOwners restates only the specifiers whose owner changes);
|
|
67
|
+
* any other value replaces the base.
|
|
68
|
+
*/
|
|
69
|
+
export function editionRuleParams(params, edition) {
|
|
70
|
+
const { lite, ...base } = params;
|
|
71
|
+
const merged = edition === 'lite' && lite ? { ...base } : base;
|
|
72
|
+
if (edition === 'lite' && lite) {
|
|
73
|
+
for (const [key, value] of Object.entries(lite)) merged[key] = isPlainObject(value) && isPlainObject(base[key]) ? { ...base[key], ...value } : value;
|
|
74
|
+
}
|
|
75
|
+
const deepFreeze = (value) => { if (value && typeof value === 'object') { Object.values(value).forEach(deepFreeze); } return Object.freeze(value); };
|
|
76
|
+
return deepFreeze(structuredClone(merged));
|
|
77
|
+
}
|