@starci/eslint-grammar-core 0.15.34 → 0.16.2
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 +14 -0
- package/README.md +1 -1
- package/dist/index.js +4 -0
- package/dist/lib/why.js +15 -0
- package/dist/mascot.d.ts +28 -0
- package/dist/mascot.js +166 -0
- package/dist/surface-depth.d.ts +7 -0
- package/dist/surface-depth.js +92 -0
- package/docs/mascot.md +60 -0
- package/docs/surface-depth.md +40 -0
- package/package.json +2 -2
- package/runtime/bundle.json +74 -0
- package/runtime/engine/by-code-unit.mjs +6 -6
- package/runtime/engine/model-config.mjs +98 -76
- package/runtime/engine/orca-config.mjs +22 -17
- package/runtime/engine/release-config.mjs +22 -18
- package/runtime/engine/removed-vocabulary.mjs +21 -20
- package/runtime/engine/resources-config.mjs +42 -35
- package/runtime/engine/runtime-root.mjs +11 -13
- package/runtime/engine/secrets.mjs +170 -102
- package/runtime/engine/sonar-config.mjs +16 -13
- package/runtime/knowledge/hfs/canon-pins.yaml +10 -10
- package/runtime/knowledge/hfs/rules.yaml +95 -6
- package/runtime/knowledge/hfs/slots.yaml +2 -2
- package/runtime/knowledge/patterns/fe/ui.yaml +13 -1
- package/runtime/scripts/hfs/allows.mjs +37 -38
- package/runtime/scripts/hfs/architecture/config.mjs +302 -268
- package/runtime/scripts/hfs/architecture/side-boundary.mjs +2 -5
- package/runtime/scripts/hfs/architecture/type-context.mjs +255 -250
- package/runtime/scripts/hfs/architecture/typescript.mjs +358 -318
- package/runtime/scripts/hfs/declaration-shape.mjs +140 -103
- package/runtime/scripts/hfs/declaration-slots.mjs +64 -61
- package/runtime/scripts/hfs/edition-slots.mjs +43 -40
- package/runtime/scripts/hfs/param-names.mjs +6 -11
- package/runtime/scripts/hfs/rule-catalog.mjs +353 -257
- package/runtime/scripts/hfs/rule-params-shape.mjs +60 -47
- package/runtime/scripts/hfs/slot-app-view.mjs +69 -74
- package/runtime/scripts/hfs/slot-classify.mjs +42 -38
- package/runtime/scripts/hfs/slot-errors.mjs +8 -7
- package/runtime/scripts/hfs/slot-imports.mjs +70 -63
- package/runtime/scripts/hfs/slot-manifest-shape.mjs +74 -52
- package/runtime/scripts/hfs/slot-match.mjs +122 -111
- package/runtime/scripts/hfs/slot-path.mjs +0 -2
- package/runtime/scripts/hfs/slot-required.mjs +65 -59
- package/runtime/scripts/hfs/slot-semantic-problems.mjs +138 -112
- package/runtime/scripts/hfs/slot-side-problems.mjs +23 -19
- package/runtime/scripts/hfs/slots.mjs +175 -152
- package/runtime/scripts/hfs/typescript-programs.mjs +66 -69
- package/runtime/scripts/hfs/view.mjs +38 -40
- package/runtime/scripts/lib/glob.mjs +23 -14
- package/runtime/scripts/lib/grammar-lint.mjs +33 -37
- package/runtime/scripts/lib/i18n.mjs +32 -35
- package/runtime/scripts/lib/json-clone.mjs +45 -36
- package/runtime/scripts/lib/json.mjs +26 -14
- package/runtime/scripts/lib/list.mjs +9 -14
- package/runtime/scripts/lib/path-key.mjs +53 -38
- package/runtime/scripts/lib/read-text.mjs +30 -17
- package/runtime/scripts/lib/regex.mjs +1 -2
- package/runtime/scripts/lib/source-phrases.mjs +13 -13
- package/runtime/scripts/lib/ts-analysis.mjs +28 -26
- package/runtime/scripts/lib/ts-ast.mjs +36 -42
- package/runtime/scripts/lib/walk.mjs +33 -27
- package/runtime/scripts/lib/yaml-cached.mjs +4 -4
|
@@ -1,142 +1,179 @@
|
|
|
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
1
|
import { isPlainObject } from '../../engine/plain-object.mjs';
|
|
4
2
|
import { APP_KIND, connectionShapeProblems, NAME, PACKAGE_NAME, RUNTIME_KIND, SLOT_ID } from './manifest-shape.mjs';
|
|
5
3
|
import { dbFunctionsOk } from './rule-params-shape.mjs';
|
|
6
4
|
import { kindShapeProblems, patternShapeProblems } from './declaration-slots.mjs';
|
|
7
5
|
import { byCodeUnit } from '../lib/list.mjs';
|
|
8
|
-
|
|
9
6
|
/** Shape problems of the optional hfs.json `db` block of a lite app: the environment variable naming its database, its image, forced RLS and its application role. */
|
|
10
7
|
const isStringList = (value) => Array.isArray(value) && value.every((entry) => typeof entry === 'string' && entry.trim());
|
|
11
|
-
|
|
12
8
|
function dbBlockProblems(block) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
9
|
+
if (block === undefined)
|
|
10
|
+
return [];
|
|
11
|
+
if (!isPlainObject(block))
|
|
12
|
+
return ['db must be an object'];
|
|
13
|
+
const bad = [];
|
|
14
|
+
for (const key of Object.keys(block))
|
|
15
|
+
if (!['urlEnv', 'image', 'forceRls', 'forceRlsExemptions', 'appRole'].includes(key))
|
|
16
|
+
bad.push(`db has unknown key ${key}`);
|
|
17
|
+
if (block.urlEnv !== undefined && (typeof block.urlEnv !== 'string' || !/^[A-Z][A-Z0-9_]*$/.test(block.urlEnv)))
|
|
18
|
+
bad.push('db.urlEnv must name an UPPER_CASE environment variable');
|
|
19
|
+
if (block.image !== undefined && (typeof block.image !== 'string' || !/@sha256:[0-9a-f]{64}$/.test(block.image)))
|
|
20
|
+
bad.push('db.image must be a digest-pinned image reference');
|
|
21
|
+
if (block.forceRls !== undefined && !isStringList(block.forceRls))
|
|
22
|
+
bad.push('db.forceRls must be a list of strings');
|
|
23
|
+
if (block.appRole !== undefined && (typeof block.appRole !== 'string' || !/^[a-z][a-z0-9_]*$/.test(block.appRole)))
|
|
24
|
+
bad.push('db.appRole must name a role');
|
|
25
|
+
if (block.forceRlsExemptions !== undefined && !Array.isArray(block.forceRlsExemptions))
|
|
26
|
+
bad.push('db.forceRlsExemptions must be a list of { table, reason }');
|
|
27
|
+
return bad;
|
|
23
28
|
}
|
|
24
|
-
|
|
25
29
|
function appShapeProblems(apps, at, side, names) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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'))
|
|
37
|
+
bad.push(`${at}.apps[${index}] must be {name, kind}`);
|
|
38
|
+
else if (names.has(app.name))
|
|
39
|
+
bad.push(`app ${app.name} is declared twice (${names.get(app.name)} and ${side})`);
|
|
40
|
+
else
|
|
41
|
+
names.set(app.name, side);
|
|
42
|
+
});
|
|
29
43
|
return bad;
|
|
30
|
-
}
|
|
31
|
-
apps.forEach((app, index) => {
|
|
32
|
-
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}`);
|
|
33
|
-
else if (names.has(app.name)) bad.push(`app ${app.name} is declared twice (${names.get(app.name)} and ${side})`);
|
|
34
|
-
else names.set(app.name, side);
|
|
35
|
-
});
|
|
36
|
-
return bad;
|
|
37
44
|
}
|
|
38
|
-
|
|
39
45
|
function connectionProblems(side, value) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
const bad = [];
|
|
47
|
+
if (side !== 'be') {
|
|
48
|
+
bad.push('connections belong to the be side');
|
|
49
|
+
return bad;
|
|
50
|
+
}
|
|
51
|
+
const list = Array.isArray(value.connections) ? value.connections : null;
|
|
52
|
+
const shape = connectionShapeProblems(list, value.apps);
|
|
53
|
+
if (shape.length) {
|
|
54
|
+
bad.push(...shape);
|
|
55
|
+
return bad;
|
|
56
|
+
}
|
|
57
|
+
if (new Set(list.map((connection) => connection.name)).size !== list.length)
|
|
58
|
+
bad.push('connections names must be unique');
|
|
59
|
+
for (const a of list)
|
|
60
|
+
for (const b of list)
|
|
61
|
+
if (a !== b && `${b.envPrefix}_`.startsWith(`${a.envPrefix}_`))
|
|
62
|
+
bad.push(`connections ${a.name} and ${b.name} share env keys (${a.envPrefix}_ covers ${b.envPrefix}_)`);
|
|
49
63
|
return bad;
|
|
50
|
-
}
|
|
51
|
-
if (new Set(list.map((connection) => connection.name)).size !== list.length) bad.push('connections names must be unique');
|
|
52
|
-
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}_)`);
|
|
53
|
-
return bad;
|
|
54
64
|
}
|
|
55
|
-
|
|
56
65
|
/** Whether a declared list is present but not a list of distinct entries that all satisfy `valid`. */
|
|
57
66
|
const isBadUniqueList = (list, valid) => list !== undefined && (!Array.isArray(list) || !list.every((entry) => valid(entry)) || new Set(list).size !== list.length);
|
|
58
|
-
|
|
59
67
|
function validExtraPackage(entry) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
68
|
+
return isPlainObject(entry) && Object.keys(entry).every(key => key === 'name' || key === 'reason')
|
|
69
|
+
&& typeof entry.name === 'string' && entry.name.length <= 214 && PACKAGE_NAME.test(entry.name)
|
|
70
|
+
&& typeof entry.reason === 'string' && entry.reason.trim().length > 0;
|
|
63
71
|
}
|
|
64
|
-
|
|
65
72
|
function extraPackageProblems(side, entries) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
73
|
+
if (entries === undefined)
|
|
74
|
+
return [];
|
|
75
|
+
if (side !== 'fe')
|
|
76
|
+
return ['extraPackages belong to the fe side'];
|
|
77
|
+
if (!Array.isArray(entries) || !entries.every(validExtraPackage))
|
|
78
|
+
return ['sides.fe.extraPackages must be a list of {name, reason} with a package name and a nonempty reason'];
|
|
79
|
+
return [];
|
|
70
80
|
}
|
|
71
|
-
|
|
72
81
|
function grammarProblems(side, grammar) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
82
|
+
if (grammar === undefined)
|
|
83
|
+
return [];
|
|
84
|
+
if (side !== 'fe')
|
|
85
|
+
return ['grammar belongs to the fe side'];
|
|
86
|
+
return typeof grammar === 'string' && NAME.test(grammar) ? [] : ['sides.fe.grammar must name a Grammar family'];
|
|
76
87
|
}
|
|
77
|
-
|
|
78
88
|
function declarationSideProblems(side, value, names) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
const bad = [];
|
|
90
|
+
const at = `sides.${side}`;
|
|
91
|
+
if (!isPlainObject(value))
|
|
92
|
+
return [`${at} must be an object`];
|
|
93
|
+
for (const key of Object.keys(value))
|
|
94
|
+
if (!['apps', 'optionalSlots', 'patterns', 'kinds', 'connections', 'reads', 'extraPackages', 'grammar', 'backing', 'dbFunctions'].includes(key))
|
|
95
|
+
bad.push(`${at} has unknown key ${key}`);
|
|
96
|
+
if (value.dbFunctions !== undefined && (side !== 'be' || !dbFunctionsOk(value.dbFunctions, { bare: true })))
|
|
97
|
+
bad.push(`${at}.dbFunctions belongs to the be side and lists {name, reason} database functions, each schema-qualified (schema.name, never pg_catalog) or a bare pg built-in of the canon's list, with unique names`);
|
|
98
|
+
bad.push(...appShapeProblems(value.apps, at, side, names));
|
|
99
|
+
if (isBadUniqueList(value.optionalSlots, (slot) => SLOT_ID.test(String(slot))))
|
|
100
|
+
bad.push(`${at}.optionalSlots must be a unique list of slot ids`);
|
|
101
|
+
if (value.backing !== undefined && side !== 'be')
|
|
102
|
+
bad.push('backing belongs to the be side');
|
|
103
|
+
else if (isBadUniqueList(value.backing, (name) => NAME.test(String(name))))
|
|
104
|
+
bad.push(`${at}.backing must be a unique list of backing service names`);
|
|
105
|
+
bad.push(...patternShapeProblems(value, at, NAME), ...kindShapeProblems(value, at, NAME));
|
|
106
|
+
if (isBadUniqueList(value.reads, (read) => typeof read === 'string' && read.length > 0))
|
|
107
|
+
bad.push(`${at}.reads must be a unique list of paths`);
|
|
108
|
+
if (value.connections !== undefined)
|
|
109
|
+
bad.push(...connectionProblems(side, value));
|
|
110
|
+
bad.push(...extraPackageProblems(side, value.extraPackages), ...grammarProblems(side, value.grammar));
|
|
111
|
+
return bad;
|
|
93
112
|
}
|
|
94
|
-
|
|
95
113
|
/** Product vocabulary is a declaration, never a switch for a rule. */
|
|
96
114
|
function lexiconBlockProblems(lexicon) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
115
|
+
if (lexicon === undefined)
|
|
116
|
+
return [];
|
|
117
|
+
if (!isPlainObject(lexicon))
|
|
118
|
+
return ['lexicon must be an object'];
|
|
119
|
+
const bad = [];
|
|
120
|
+
for (const key of Object.keys(lexicon))
|
|
121
|
+
if (key !== 'removed')
|
|
122
|
+
bad.push(`lexicon has unknown key ${key}`);
|
|
123
|
+
if (isBadUniqueList(lexicon.removed, (word) => typeof word === 'string' && word.trim().length > 0))
|
|
124
|
+
bad.push('lexicon.removed must be a unique list of nonempty words');
|
|
125
|
+
return bad;
|
|
103
126
|
}
|
|
104
|
-
|
|
105
127
|
/** Front-end error reporting remains optional in both editions. */
|
|
106
128
|
function observeBlockProblems(observe) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
129
|
+
if (observe === undefined)
|
|
130
|
+
return [];
|
|
131
|
+
if (!isPlainObject(observe))
|
|
132
|
+
return ['observe must be an object'];
|
|
133
|
+
const bad = [];
|
|
134
|
+
for (const key of Object.keys(observe))
|
|
135
|
+
if (key !== 'reporter')
|
|
136
|
+
bad.push(`observe has unknown key ${key}`);
|
|
137
|
+
if (observe.reporter !== undefined && (typeof observe.reporter !== 'string' || !observe.reporter.trim()))
|
|
138
|
+
bad.push('observe.reporter must name the optional error reporter');
|
|
139
|
+
return bad;
|
|
113
140
|
}
|
|
114
|
-
|
|
115
141
|
const DECLARATION_KEYS = new Set(['hfs', 'kind', 'project', 'edition', 'db', 'sides', 'browser', 'lexicon', 'observe']);
|
|
116
142
|
const RUNTIME_DECLARATION_KEYS = new Set(['hfs', 'kind', 'project']);
|
|
117
|
-
|
|
118
143
|
function productDeclarationProblems(declaration, profiles) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
144
|
+
const bad = [];
|
|
145
|
+
for (const key of Object.keys(declaration))
|
|
146
|
+
if (!DECLARATION_KEYS.has(key))
|
|
147
|
+
bad.push(`unknown key ${key}`);
|
|
148
|
+
bad.push(...dbBlockProblems(declaration.db), ...lexiconBlockProblems(declaration.lexicon), ...observeBlockProblems(declaration.observe));
|
|
149
|
+
if (declaration.browser !== undefined && declaration.browser !== true)
|
|
150
|
+
bad.push('browser is `true` when the app has a browser journey (slot app.browser), and is left out otherwise');
|
|
151
|
+
if (declaration.kind !== APP_KIND)
|
|
152
|
+
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)`);
|
|
153
|
+
if (!isPlainObject(declaration.sides) || Object.keys(declaration.sides).sort(byCodeUnit).join() !== profiles.join()) {
|
|
154
|
+
bad.push('sides must declare exactly be and fe');
|
|
155
|
+
return bad;
|
|
156
|
+
}
|
|
157
|
+
const names = new Map();
|
|
158
|
+
for (const side of profiles)
|
|
159
|
+
bad.push(...declarationSideProblems(side, declaration.sides[side], names));
|
|
160
|
+
return bad;
|
|
128
161
|
}
|
|
129
|
-
|
|
130
162
|
/** Shape problems of a parsed hfs.json, in the words of modules/schemas/hfs-repo.schema.yaml. */
|
|
131
163
|
export function declarationShapeProblems(declaration, profiles) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
164
|
+
const bad = [];
|
|
165
|
+
if (!isPlainObject(declaration))
|
|
166
|
+
return ['hfs.json is not an object'];
|
|
167
|
+
if (!(Number.isInteger(declaration.hfs) && declaration.hfs >= 1))
|
|
168
|
+
bad.push('hfs must be the pinned manifest major (an integer, 1 or more)');
|
|
169
|
+
if (!NAME.test(String(declaration.project)))
|
|
170
|
+
bad.push('project must be a project name');
|
|
171
|
+
if (declaration.kind === RUNTIME_KIND) {
|
|
172
|
+
for (const key of Object.keys(declaration))
|
|
173
|
+
if (!RUNTIME_DECLARATION_KEYS.has(key))
|
|
174
|
+
bad.push(`unknown key ${key} (a runtime declaration is {hfs, kind, project})`);
|
|
175
|
+
return bad;
|
|
176
|
+
}
|
|
177
|
+
bad.push(...productDeclarationProblems(declaration, profiles));
|
|
138
178
|
return bad;
|
|
139
|
-
}
|
|
140
|
-
bad.push(...productDeclarationProblems(declaration, profiles));
|
|
141
|
-
return bad;
|
|
142
179
|
}
|
|
@@ -1,86 +1,89 @@
|
|
|
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
1
|
import { judgedInEdition } from './edition-slots.mjs';
|
|
6
|
-
|
|
7
2
|
/** The problems of a side's `optionalSlots` and `patterns` against the manifest. */
|
|
8
3
|
export function optionalSlotProblems(manifest, side, s) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
4
|
+
const bad = [];
|
|
5
|
+
for (const id of s.optionalSlots ?? []) {
|
|
6
|
+
const slot = manifest.slots.find((candidate) => candidate.id === id);
|
|
7
|
+
if (!slot?.profiles.includes(side))
|
|
8
|
+
bad.push(`sides.${side}.optionalSlots names ${id}, which is not a ${side} slot`);
|
|
9
|
+
else if (slot.presence !== 'opt-in')
|
|
10
|
+
bad.push(`sides.${side}.optionalSlots names ${id}, which is ${slot.presence}, not opt-in`);
|
|
11
|
+
else if (slot.appKind !== undefined)
|
|
12
|
+
bad.push(`sides.${side}.optionalSlots names ${id}; an app of kind ${slot.appKind} enables it`);
|
|
13
|
+
}
|
|
14
|
+
for (const name of s.patterns ?? []) {
|
|
15
|
+
const ownsSlot = manifest.slots.some((slot) => slot.profiles.includes(side) && slot.pattern === name);
|
|
16
|
+
const ownsProof = Object.hasOwn(manifest.ruleParams?.[side]?.patternScenarios ?? {}, name);
|
|
17
|
+
if (!ownsSlot && !ownsProof)
|
|
18
|
+
bad.push(`sides.${side}.patterns names ${name}, which neither a ${side} slot nor its proof scenario catalog declares`);
|
|
19
|
+
}
|
|
20
|
+
if (s.kinds !== undefined && side !== 'be')
|
|
21
|
+
bad.push(`sides.${side}.kinds belongs to the be side`);
|
|
22
|
+
for (const kind of s.kinds ?? []) {
|
|
23
|
+
if (!(manifest.triggerKinds ?? []).includes(kind))
|
|
24
|
+
bad.push(`sides.${side}.kinds names ${kind}, which is not one of triggerKinds (${(manifest.triggerKinds ?? []).join(', ')})`);
|
|
25
|
+
}
|
|
26
|
+
return bad;
|
|
26
27
|
}
|
|
27
|
-
|
|
28
28
|
/** The problems of a side's `backing` list: every name is admitted by ruleParams.be.backing in this edition. */
|
|
29
29
|
export function backingProblems(manifest, side, s, edition = 'full') {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
34
|
}
|
|
35
|
-
|
|
36
35
|
/** The problems of the shape of a side's `patterns` list (a unique list of pattern names). */
|
|
37
36
|
export function patternShapeProblems(s, at, namePattern) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
const list = s.patterns;
|
|
38
|
+
if (list === undefined)
|
|
39
|
+
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
41
|
}
|
|
42
|
-
|
|
43
42
|
/** The problems of the shape of a side's `kinds` list (a unique list of trigger kind names). */
|
|
44
43
|
export function kindShapeProblems(s, at, namePattern) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
const list = s.kinds;
|
|
45
|
+
if (list === undefined)
|
|
46
|
+
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
48
|
}
|
|
49
|
-
|
|
50
49
|
/** 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
50
|
export function kindParamProblems(be, triggerKinds) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
51
|
+
const kebab = /^[a-z][a-z0-9-]*$/;
|
|
52
|
+
const names = (list) => Array.isArray(list) && list.every((value) => kebab.test(String(value))) && new Set(list).size === list.length;
|
|
53
|
+
const bad = [];
|
|
54
|
+
const patterns = be.kindPatterns;
|
|
55
|
+
if (patterns === null || typeof patterns !== 'object' || Array.isArray(patterns) || !Object.entries(patterns).every(([kind, list]) => (triggerKinds ?? []).includes(kind) && names(list)))
|
|
56
|
+
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)))
|
|
60
|
+
bad.push('ruleParams.be.addKinds must map a noun to {topic, variable, patterns, trigger?, needs?, also?, defaults?}');
|
|
61
|
+
return bad;
|
|
61
62
|
}
|
|
62
|
-
|
|
63
63
|
/** The problems of `ruleParams.be.patternScenarios`: a pattern name to a non-empty list of unique kebab-case scenario ids. */
|
|
64
64
|
export function scenarioProblem(scenarios) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
68
|
}
|
|
69
|
-
|
|
70
69
|
/** The problems of the `trigger` of a slot against the manifest's `triggerKinds`: a known kind, on a feature-tier slot only. */
|
|
71
70
|
export function triggerProblems(slot, triggerKinds) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
if (slot.trigger === undefined)
|
|
72
|
+
return [];
|
|
73
|
+
const bad = [];
|
|
74
|
+
if (!(triggerKinds ?? []).includes(slot.trigger))
|
|
75
|
+
bad.push(`slot ${slot.id}: trigger ${slot.trigger} is not one of triggerKinds`);
|
|
76
|
+
if (slot.tier !== 'feature')
|
|
77
|
+
bad.push(`slot ${slot.id}: a trigger belongs to a feature-tier slot`);
|
|
78
|
+
return bad;
|
|
77
79
|
}
|
|
78
|
-
|
|
79
80
|
/** 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
81
|
export function declaredSlotEnabled(slot, repo) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
if (slot.appKind !== undefined)
|
|
83
|
+
return repo.apps.some((app) => app.kind === slot.appKind);
|
|
84
|
+
// A provider declared on any connection (be owns them all today) enables the provider slots of every profile: repo.providers
|
|
85
|
+
// is the app-wide union the declaration resolver computed; a hand-built repo falls back to its own connections.
|
|
86
|
+
if (slot.provider !== undefined)
|
|
87
|
+
return (repo.providers ?? (repo.connections ?? []).map((connection) => connection.provider)).includes(slot.provider);
|
|
88
|
+
return repo.optionalSlots.includes(slot.id) || (slot.pattern !== undefined && (repo.patterns ?? []).includes(slot.pattern));
|
|
86
89
|
}
|
|
@@ -4,74 +4,77 @@
|
|
|
4
4
|
// Its overlays exclude the e2e world, and every slot keeps its own `tests`.
|
|
5
5
|
import { isPlainObject } from '../../engine/plain-object.mjs';
|
|
6
6
|
import { EDITIONS } from './manifest-shape.mjs';
|
|
7
|
-
|
|
8
7
|
/** The presence of `slot` under edition lite for `profile`: `litePresence`, else the slot's own presence. */
|
|
9
8
|
export function litePresenceOf(slot, profile) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
const lp = slot.litePresence;
|
|
10
|
+
if (lp === undefined)
|
|
11
|
+
return slot.presence;
|
|
12
|
+
return isPlainObject(lp) ? (lp[profile] ?? slot.presence) : lp;
|
|
13
13
|
}
|
|
14
|
-
|
|
15
14
|
/** Whether `slot` exists under `edition` of `manifest` (default: the manifest's own editions). */
|
|
16
15
|
export function slotInEdition(manifest, slot, edition) {
|
|
17
|
-
|
|
16
|
+
return (slot.editions ?? manifest.editions ?? EDITIONS).includes(edition);
|
|
18
17
|
}
|
|
19
|
-
|
|
20
18
|
/** 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
19
|
export function effectiveSlot(slot, profile, edition) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
20
|
+
if (edition !== 'lite')
|
|
21
|
+
return slot;
|
|
22
|
+
const { lite, litePresence, ...base } = slot;
|
|
23
|
+
const presence = litePresenceOf(slot, profile);
|
|
24
|
+
let tracked = slot.tracked;
|
|
25
|
+
if (presence === 'forbidden')
|
|
26
|
+
tracked = 'external';
|
|
27
|
+
else if (tracked === 'external')
|
|
28
|
+
tracked = 'tracked';
|
|
29
|
+
return { ...base, ...lite, presence, tracked };
|
|
29
30
|
}
|
|
30
|
-
|
|
31
31
|
/** The template group starci app sync renders `slot` from under `edition`: `liteManagedBy` (else `managedBy`) in lite, `managedBy` otherwise; undefined when unmanaged. */
|
|
32
32
|
export function managedGroupOf(slot, edition) {
|
|
33
|
-
|
|
33
|
+
return edition === 'lite' ? (slot.liteManagedBy ?? slot.managedBy) : slot.managedBy;
|
|
34
34
|
}
|
|
35
|
-
|
|
36
35
|
/** The declared edition and the vocabulary against which it is validated. */
|
|
37
36
|
export function declarationEdition(manifest, declaration) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
const edition = declaration.edition === undefined ? 'full' : declaration.edition;
|
|
38
|
+
const known = manifest.editions ?? EDITIONS;
|
|
39
|
+
return { edition, known, valid: known.includes(edition) };
|
|
41
40
|
}
|
|
42
|
-
|
|
43
41
|
/** Problems of an optional rule-catalog `editions` list, in the catalog schema's words. */
|
|
44
42
|
export function ruleEditionProblems(rule, label) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
43
|
+
if (rule.editions === undefined)
|
|
44
|
+
return [];
|
|
45
|
+
const bad = [];
|
|
46
|
+
if (!Array.isArray(rule.editions) || !rule.editions.length)
|
|
47
|
+
return [`${label}.editions must be a non-empty list`];
|
|
48
|
+
for (const value of rule.editions)
|
|
49
|
+
if (!EDITIONS.includes(value))
|
|
50
|
+
bad.push(`${label}.editions has ${JSON.stringify(value)}, not one of ${EDITIONS.join(', ')}`);
|
|
51
|
+
if (new Set(rule.editions).size !== rule.editions.length)
|
|
52
|
+
bad.push(`${label}.editions repeats a value`);
|
|
53
|
+
return bad;
|
|
51
54
|
}
|
|
52
|
-
|
|
53
55
|
/** Whether a catalogued rule owner judges its finding under `edition`; uncatalogued codes always remain judged. */
|
|
54
56
|
export function judgedInEdition(owner, edition = 'full') {
|
|
55
|
-
|
|
57
|
+
return !owner?.editions || owner.editions.includes(edition);
|
|
56
58
|
}
|
|
57
|
-
|
|
58
59
|
/** 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
60
|
export function enforcerJudgedInEdition(rules, kind, id, edition = 'full') {
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
const owners = rules.filter((rule) => rule.enforcers.some((enforcer) => enforcer.kind === kind && enforcer.id === id));
|
|
62
|
+
return owners.every((rule) => judgedInEdition(rule, edition) && rule.enforcers.filter((enforcer) => enforcer.kind === kind && enforcer.id === id).every((enforcer) => judgedInEdition(enforcer, edition)));
|
|
62
63
|
}
|
|
63
|
-
|
|
64
64
|
/**
|
|
65
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
66
|
* value and override are both maps merges one level deep (lite.infraOwners restates only the specifiers whose owner changes);
|
|
67
67
|
* any other value replaces the base.
|
|
68
68
|
*/
|
|
69
69
|
export function editionRuleParams(params, edition) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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))
|
|
74
|
+
merged[key] = isPlainObject(value) && isPlainObject(base[key]) ? { ...base[key], ...value } : value;
|
|
75
|
+
}
|
|
76
|
+
const deepFreeze = (value) => { if (value && typeof value === 'object') {
|
|
77
|
+
Object.values(value).forEach(deepFreeze);
|
|
78
|
+
} return Object.freeze(value); };
|
|
79
|
+
return deepFreeze(structuredClone(merged));
|
|
77
80
|
}
|
|
@@ -1,18 +1,13 @@
|
|
|
1
|
-
// param-names.mjs - the one shape of `ruleParams.be.paramNames` of knowledge/hfs/slots.yaml (R85 injected-param-name): a non-empty list of unique
|
|
2
|
-
// entries `{type | typeSuffix, names, nameSuffix?}`. The loader of scripts/hfs/slots.mjs calls it; the schema (modules/schemas/hfs-slots.schema.yaml)
|
|
3
|
-
// states the same shape.
|
|
4
1
|
import { isPlainObject } from '../../engine/plain-object.mjs';
|
|
5
|
-
|
|
6
2
|
const UPPER = /^[A-Z][A-Za-z0-9]*$/u;
|
|
7
3
|
const LOWER = /^[a-z][A-Za-z0-9]*$/u;
|
|
8
|
-
|
|
9
4
|
const entryOk = (e) => isPlainObject(e) && ((e.type === undefined) !== (e.typeSuffix === undefined)) && UPPER.test(String(e.type ?? e.typeSuffix))
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
&& Array.isArray(e.names) && e.names.length > 0 && new Set(e.names).size === e.names.length && e.names.every((n) => LOWER.test(String(n)))
|
|
6
|
+
&& (e.nameSuffix === undefined || UPPER.test(String(e.nameSuffix))) && Object.keys(e).every((k) => ['type', 'typeSuffix', 'names', 'nameSuffix'].includes(k));
|
|
13
7
|
/** True when `list` is a well-formed paramNames table. */
|
|
14
8
|
export function paramNamesOk(list) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
9
|
+
if (!Array.isArray(list) || list.length === 0 || !list.every(entryOk))
|
|
10
|
+
return false;
|
|
11
|
+
const keys = list.map((e) => (e.type !== undefined ? `type:${e.type}` : `typeSuffix:${e.typeSuffix}`));
|
|
12
|
+
return new Set(keys).size === keys.length;
|
|
18
13
|
}
|