@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.
Files changed (63) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +1 -1
  3. package/dist/index.js +4 -0
  4. package/dist/lib/why.js +15 -0
  5. package/dist/mascot.d.ts +28 -0
  6. package/dist/mascot.js +166 -0
  7. package/dist/surface-depth.d.ts +7 -0
  8. package/dist/surface-depth.js +92 -0
  9. package/docs/mascot.md +60 -0
  10. package/docs/surface-depth.md +40 -0
  11. package/package.json +2 -2
  12. package/runtime/bundle.json +74 -0
  13. package/runtime/engine/by-code-unit.mjs +6 -6
  14. package/runtime/engine/model-config.mjs +98 -76
  15. package/runtime/engine/orca-config.mjs +22 -17
  16. package/runtime/engine/release-config.mjs +22 -18
  17. package/runtime/engine/removed-vocabulary.mjs +21 -20
  18. package/runtime/engine/resources-config.mjs +42 -35
  19. package/runtime/engine/runtime-root.mjs +11 -13
  20. package/runtime/engine/secrets.mjs +170 -102
  21. package/runtime/engine/sonar-config.mjs +16 -13
  22. package/runtime/knowledge/hfs/canon-pins.yaml +10 -10
  23. package/runtime/knowledge/hfs/rules.yaml +95 -6
  24. package/runtime/knowledge/hfs/slots.yaml +2 -2
  25. package/runtime/knowledge/patterns/fe/ui.yaml +13 -1
  26. package/runtime/scripts/hfs/allows.mjs +37 -38
  27. package/runtime/scripts/hfs/architecture/config.mjs +302 -268
  28. package/runtime/scripts/hfs/architecture/side-boundary.mjs +2 -5
  29. package/runtime/scripts/hfs/architecture/type-context.mjs +255 -250
  30. package/runtime/scripts/hfs/architecture/typescript.mjs +358 -318
  31. package/runtime/scripts/hfs/declaration-shape.mjs +140 -103
  32. package/runtime/scripts/hfs/declaration-slots.mjs +64 -61
  33. package/runtime/scripts/hfs/edition-slots.mjs +43 -40
  34. package/runtime/scripts/hfs/param-names.mjs +6 -11
  35. package/runtime/scripts/hfs/rule-catalog.mjs +353 -257
  36. package/runtime/scripts/hfs/rule-params-shape.mjs +60 -47
  37. package/runtime/scripts/hfs/slot-app-view.mjs +69 -74
  38. package/runtime/scripts/hfs/slot-classify.mjs +42 -38
  39. package/runtime/scripts/hfs/slot-errors.mjs +8 -7
  40. package/runtime/scripts/hfs/slot-imports.mjs +70 -63
  41. package/runtime/scripts/hfs/slot-manifest-shape.mjs +74 -52
  42. package/runtime/scripts/hfs/slot-match.mjs +122 -111
  43. package/runtime/scripts/hfs/slot-path.mjs +0 -2
  44. package/runtime/scripts/hfs/slot-required.mjs +65 -59
  45. package/runtime/scripts/hfs/slot-semantic-problems.mjs +138 -112
  46. package/runtime/scripts/hfs/slot-side-problems.mjs +23 -19
  47. package/runtime/scripts/hfs/slots.mjs +175 -152
  48. package/runtime/scripts/hfs/typescript-programs.mjs +66 -69
  49. package/runtime/scripts/hfs/view.mjs +38 -40
  50. package/runtime/scripts/lib/glob.mjs +23 -14
  51. package/runtime/scripts/lib/grammar-lint.mjs +33 -37
  52. package/runtime/scripts/lib/i18n.mjs +32 -35
  53. package/runtime/scripts/lib/json-clone.mjs +45 -36
  54. package/runtime/scripts/lib/json.mjs +26 -14
  55. package/runtime/scripts/lib/list.mjs +9 -14
  56. package/runtime/scripts/lib/path-key.mjs +53 -38
  57. package/runtime/scripts/lib/read-text.mjs +30 -17
  58. package/runtime/scripts/lib/regex.mjs +1 -2
  59. package/runtime/scripts/lib/source-phrases.mjs +13 -13
  60. package/runtime/scripts/lib/ts-analysis.mjs +28 -26
  61. package/runtime/scripts/lib/ts-ast.mjs +36 -42
  62. package/runtime/scripts/lib/walk.mjs +33 -27
  63. package/runtime/scripts/lib/yaml-cached.mjs +4 -4
@@ -1,85 +1,98 @@
1
- // rule-params-shape.mjs - the shape of `ruleParams` of an app slot manifest (knowledge/hfs/slots.yaml): the parameters both sides
2
- // share (`common`), the back end's own (`be`) and the front end's overrides (`fe`). Pure: a parsed manifest in, a list of
3
- // problems out, in the words of modules/schemas/hfs-slots.schema.yaml. scripts/hfs/slots.mjs (loadSlotManifest) is the reader.
4
1
  import { isPlainObject } from '../../engine/plain-object.mjs';
5
2
  import { paramNamesOk } from './param-names.mjs';
6
3
  import { kindParamProblems, scenarioProblem } from './declaration-slots.mjs';
7
4
  import { NAME, namedMapValid, PACKAGE_NAME, roleListProblems, SCHEMA_AUTHORITIES, unitRolesProblems } from './manifest-shape.mjs';
8
5
  import { byCodeUnit, nonemptyUniqueList } from '../lib/list.mjs';
9
6
  import { ruleEditionProblems } from './edition-slots.mjs';
10
-
11
7
  const PROFILES = ['be', 'fe'];
12
8
  const BE_OWN = ['infraOwners', 'eventBus', 'specDoubles', 'paramNames', 'suffixes', 'bannedSuffixes', 'contractShape', 'unitRoles', 'logicRoles', 'valueRoles', 'thinRoles', 'patternScenarios', 'kindPatterns', 'addKinds', 'backing'];
13
9
  const backingEntryOk = (entry) => isPlainObject(entry) && Object.keys(entry).every((key) => ['clients', 'images', 'patterns', 'editions'].includes(key))
14
- && nonemptyUniqueList(entry.clients, (name) => PACKAGE_NAME.test(String(name)))
15
- && Array.isArray(entry.patterns) && entry.patterns.every((name) => NAME.test(String(name))) && new Set(entry.patterns).size === entry.patterns.length
16
- && nonemptyUniqueList(entry.images, (name) => PACKAGE_NAME.test(String(name)))
17
- && ruleEditionProblems(entry, 'backing service').length === 0;
10
+ && nonemptyUniqueList(entry.clients, (name) => PACKAGE_NAME.test(String(name)))
11
+ && Array.isArray(entry.patterns) && entry.patterns.every((name) => NAME.test(String(name))) && new Set(entry.patterns).size === entry.patterns.length
12
+ && nonemptyUniqueList(entry.images, (name) => PACKAGE_NAME.test(String(name)))
13
+ && ruleEditionProblems(entry, 'backing service').length === 0;
18
14
  const FORM_NAMES = new Set(['call', 'new', 'curried', 'object', 'primitive', 'array']);
19
15
  const blockOk = (v) => isPlainObject(v) && Number.isInteger(v.lines) && v.lines >= 2 && Number.isInteger(v.tokens) && v.tokens >= 1 && Object.keys(v).length === 2;
20
16
  const fileLinesOk = (v) => isPlainObject(v) && Number.isInteger(v.soft) && v.soft >= 1 && typeof v.hardGrowth === 'boolean' && Object.keys(v).length === 2;
21
17
  const SHARED_PARAMS = {
22
- fileLines: fileLinesOk,
23
- duplicateBlock: blockOk,
24
- functionLines: v => Number.isInteger(v) && v >= 1,
25
- featureFiles: v => Number.isInteger(v) && v >= 1,
18
+ fileLines: fileLinesOk,
19
+ duplicateBlock: blockOk,
20
+ functionLines: (v) => Number.isInteger(v) && v >= 1,
21
+ featureFiles: (v) => Number.isInteger(v) && v >= 1,
26
22
  };
27
23
  const SHARED_KEYS = Object.keys(SHARED_PARAMS);
28
24
  const sharedParam = (key, v) => Object.hasOwn(SHARED_PARAMS, key) && SHARED_PARAMS[key](v);
29
25
  const sideOk = (side, own, extra = []) => own.every((k) => side[k] !== undefined) && Object.entries(side).every(([k, v]) => own.includes(k) || extra.includes(k) || sharedParam(k, v));
30
26
  const liteKeyOk = (lite, key, keys) => key !== 'lite' && keys.includes(key)
31
- && (!['functionLines', 'featureFiles'].includes(key) || sharedParam(key, lite[key]));
27
+ && (!['functionLines', 'featureFiles'].includes(key) || sharedParam(key, lite[key]));
32
28
  const liteOk = (lite, keys) => lite === undefined || (isPlainObject(lite) && Object.keys(lite).length > 0 && Object.keys(lite).every((k) => liteKeyOk(lite, k, keys)));
33
29
  const memberMap = (v, allowEmpty) => isPlainObject(v) && Object.keys(v).length > 0 && Object.values(v).every((list) => Array.isArray(list) && (allowEmpty || list.length > 0) && list.every((x) => /^[A-Z][A-Za-z0-9]*$/.test(String(x))) && new Set(list).size === list.length);
34
30
  const roleList = (v) => nonemptyUniqueList(v, (role) => NAME.test(String(role)));
35
31
  const formsOk = (v) => Array.isArray(v) && v.length > 0 && v.every((x) => FORM_NAMES.has(x)) && new Set(v).size === v.length;
36
32
  const doubleOk = (v) => isPlainObject(v) && /^[A-Za-z][A-Za-z0-9]*$/.test(String(v.double)) && formsOk(v.forms);
37
- const regexOk = (v) => { try { return typeof v === 'string' && v.length > 0 && Boolean(new RegExp(v)); } catch { return false; } };
33
+ const regexOk = (v) => { try {
34
+ return typeof v === 'string' && v.length > 0 && Boolean(new RegExp(v));
35
+ }
36
+ catch {
37
+ return false;
38
+ } };
38
39
  const dbExtensionsOk = (value) => Array.isArray(value) && value.every((name) => typeof name === 'string' && name.length > 0) && new Set(value).size === value.length;
39
40
  const DB_PARAMS = { extensions: dbExtensionsOk };
40
41
  const dbParamsOk = (db) => db === undefined || (isPlainObject(db) && Object.entries(db).every(([key, value]) => Object.hasOwn(DB_PARAMS, key) && DB_PARAMS[key](value)));
41
-
42
42
  function beSpecificProblems(be, manifest) {
43
- const bad = [];
44
- bad.push(...scenarioProblem(be.patternScenarios), ...kindParamProblems(be, manifest.triggerKinds));
45
- if (!isPlainObject(be.contractShape) || Object.keys(be.contractShape).length !== 1 || !/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(String(be.contractShape.helper))) bad.push('ruleParams.be.contractShape must be {helper: <identifier>}');
46
- const owners = be.infraOwners;
47
- const ownerId = /^(platform|integrations)\/[a-z][a-z0-9-]*$/;
48
- if (!isPlainObject(owners) || !Object.keys(owners).length || !Object.entries(owners).every(([key, list]) => key && Array.isArray(list) && list.every((o) => ownerId.test(String(o))) && new Set(list).size === list.length)) bad.push('ruleParams.be.infraOwners must map a non-empty specifier to a list of unique platform/<capability> or integrations/<provider> owners ([] means nowhere)');
49
- const eventBus = be.eventBus;
50
- if (!isPlainObject(eventBus) || Object.keys(eventBus).sort(byCodeUnit).join() !== 'classes,imports' || !memberMap(eventBus.imports, true) || !memberMap(eventBus.classes, false)) bad.push('ruleParams.be.eventBus must be {imports: {<module>: [unique PascalCase members]}, classes: {<module>: [non-empty unique PascalCase classes]}}');
51
- if (!paramNamesOk(be.paramNames)) bad.push('ruleParams.be.paramNames must be a non-empty list of unique {type | typeSuffix, names, nameSuffix?} entries');
52
- if (!namedMapValid(be.backing, backingEntryOk)) bad.push('ruleParams.be.backing must map a backing service name to {clients: [unique package names], patterns: [unique pattern names], images: [unique image repositories], editions?: [unique known editions]}');
53
- if (!roleList(be.suffixes)) bad.push('ruleParams.be.suffixes must be a non-empty list of unique kebab-case role suffixes');
54
- if (!roleList(be.bannedSuffixes)) bad.push('ruleParams.be.bannedSuffixes must be a non-empty list of unique kebab-case suffixes');
55
- else if (roleList(be.suffixes) && be.suffixes.some((x) => be.bannedSuffixes.includes(x))) bad.push('ruleParams.be.suffixes and bannedSuffixes must be disjoint');
56
- const sd = be.specDoubles;
57
- if (!isPlainObject(sd) || Object.keys(sd).sort(byCodeUnit).join() !== 'doubles,fallback,kit' || typeof sd.kit !== 'string' || !sd.kit || !Array.isArray(sd.doubles) || !sd.doubles.length || !sd.doubles.every((e) => doubleOk(e) && regexOk(e.token) && (e.from === undefined || regexOk(e.from)) && Object.keys(e).length === (e.from === undefined ? 3 : 4)) || !doubleOk(sd.fallback) || Object.keys(sd.fallback).length !== 2) bad.push('ruleParams.be.specDoubles must be {kit, doubles: [{token: regex, double, forms}], fallback: {double, forms}} with forms drawn from call, new, curried, object, primitive, array');
58
- return bad;
43
+ const bad = [];
44
+ bad.push(...scenarioProblem(be.patternScenarios), ...kindParamProblems(be, manifest.triggerKinds));
45
+ if (!isPlainObject(be.contractShape) || Object.keys(be.contractShape).length !== 1 || !/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(String(be.contractShape.helper)))
46
+ bad.push('ruleParams.be.contractShape must be {helper: <identifier>}');
47
+ const owners = be.infraOwners;
48
+ const ownerId = /^(platform|integrations)\/[a-z][a-z0-9-]*$/;
49
+ if (!isPlainObject(owners) || !Object.keys(owners).length || !Object.entries(owners).every(([key, list]) => key && Array.isArray(list) && list.every((o) => ownerId.test(String(o))) && new Set(list).size === list.length))
50
+ bad.push('ruleParams.be.infraOwners must map a non-empty specifier to a list of unique platform/<capability> or integrations/<provider> owners ([] means nowhere)');
51
+ const eventBus = be.eventBus;
52
+ if (!isPlainObject(eventBus) || Object.keys(eventBus).sort(byCodeUnit).join() !== 'classes,imports' || !memberMap(eventBus.imports, true) || !memberMap(eventBus.classes, false))
53
+ bad.push('ruleParams.be.eventBus must be {imports: {<module>: [unique PascalCase members]}, classes: {<module>: [non-empty unique PascalCase classes]}}');
54
+ if (!paramNamesOk(be.paramNames))
55
+ bad.push('ruleParams.be.paramNames must be a non-empty list of unique {type | typeSuffix, names, nameSuffix?} entries');
56
+ if (!namedMapValid(be.backing, backingEntryOk))
57
+ bad.push('ruleParams.be.backing must map a backing service name to {clients: [unique package names], patterns: [unique pattern names], images: [unique image repositories], editions?: [unique known editions]}');
58
+ if (!roleList(be.suffixes))
59
+ bad.push('ruleParams.be.suffixes must be a non-empty list of unique kebab-case role suffixes');
60
+ if (!roleList(be.bannedSuffixes))
61
+ bad.push('ruleParams.be.bannedSuffixes must be a non-empty list of unique kebab-case suffixes');
62
+ else if (roleList(be.suffixes) && be.suffixes.some((x) => be.bannedSuffixes.includes(x)))
63
+ bad.push('ruleParams.be.suffixes and bannedSuffixes must be disjoint');
64
+ const sd = be.specDoubles;
65
+ if (!isPlainObject(sd) || Object.keys(sd).sort(byCodeUnit).join() !== 'doubles,fallback,kit' || typeof sd.kit !== 'string' || !sd.kit || !Array.isArray(sd.doubles) || !sd.doubles.length || !sd.doubles.every((e) => doubleOk(e) && regexOk(e.token) && (e.from === undefined || regexOk(e.from)) && Object.keys(e).length === (e.from === undefined ? 3 : 4)) || !doubleOk(sd.fallback) || Object.keys(sd.fallback).length !== 2)
66
+ bad.push('ruleParams.be.specDoubles must be {kit, doubles: [{token: regex, double, forms}], fallback: {double, forms}} with forms drawn from call, new, curried, object, primitive, array');
67
+ return bad;
59
68
  }
60
-
61
69
  /** A database function name (R334): `schema.name` outside pg_catalog, or, where `bare` admits it, a pg built-in named alone. */
62
70
  const dbFunctionNameOk = (name, bare) => typeof name === 'string' && (/^[a-z_][a-z0-9_]*\.[a-z_][a-z0-9_]*$/.test(name) ? !name.startsWith('pg_catalog.') : bare && /^[a-z_][a-z0-9_]*$/.test(name));
63
-
64
71
  /**
65
72
  * True for an absent list or a list of {name, reason} database functions, names unique and schema-qualified; `bare` also admits
66
73
  * a bare name, which only the canon's list (ruleParams.be.dbFunctions) uses, for the pg built-ins its helpers call.
67
74
  */
68
75
  export const dbFunctionsOk = (list, { bare = false } = {}) => list === undefined || (Array.isArray(list) && list.every((entry) => isPlainObject(entry) && Object.keys(entry).sort().join() === 'name,reason' && dbFunctionNameOk(entry.name, bare) && typeof entry.reason === 'string' && entry.reason.trim().length > 0) && new Set(list.map((entry) => entry.name)).size === list.length);
69
-
70
76
  /** The shape problems of `m.ruleParams`. */
71
77
  export function ruleParamsProblems(m) {
72
- const bad = [];
73
- const rp = m.ruleParams;
74
- if (!isPlainObject(rp) || Object.keys(rp).some((k) => ![...PROFILES, 'common', 'db'].includes(k)) || !isPlainObject(rp.common) || !isPlainObject(rp.be) || (rp.fe !== undefined && !isPlainObject(rp.fe))) return ['ruleParams must be a map with common, be and optionally fe and db'];
75
- else {
76
- // common holds the parameters both sides share; a side may restate one of them (a valid value) as an override, nothing else.
77
- // Edition lite: a side's optional `lite` map overrides keys of the same side (never `lite` itself) when the app declares edition lite.
78
- if (!sideOk(rp.common, []) || !fileLinesOk(rp.common.fileLines) || !blockOk(rp.common.duplicateBlock)) bad.push('ruleParams.common needs fileLines {soft, hardGrowth} and duplicateBlock {lines >= 2, tokens >= 1}; optional functionLines and featureFiles must be positive integers');
79
- if (!sideOk(rp.be, BE_OWN, ['schemaAuthority', 'lite', 'dbFunctions']) || (rp.be.schemaAuthority !== undefined && !SCHEMA_AUTHORITIES.includes(rp.be.schemaAuthority)) || !dbFunctionsOk(rp.be.dbFunctions, { bare: true }) || !liteOk(rp.be.lite, [...BE_OWN, ...SHARED_KEYS, 'schemaAuthority', 'dbFunctions'])) bad.push('ruleParams.be needs infraOwners, eventBus, specDoubles, paramNames, suffixes, bannedSuffixes, contractShape {helper}, unitRoles, logicRoles, thinRoles, patternScenarios, kindPatterns, addKinds and backing (shared parameters live in ruleParams.common, a side may override them); optional: schemaAuthority (' + SCHEMA_AUTHORITIES.join(' | ') + '), dbFunctions ([{name, reason}]: a pg built-in bare, every other function schema.name) and lite (overrides of the same keys)'); else bad.push(...unitRolesProblems(rp.be), ...roleListProblems(rp.be));
80
- bad.push(...beSpecificProblems(rp.be, m));
81
- if (rp.fe !== undefined && (!sideOk(rp.fe, [], ['lite']) || !liteOk(rp.fe.lite, SHARED_KEYS))) bad.push('ruleParams.fe may only restate the shared parameters of ruleParams.common, and carry a lite override of them');
82
- if (!dbParamsOk(rp.db)) bad.push('ruleParams.db may contain only extensions (a list of unique non-empty extension names)');
83
- }
84
- return bad;
78
+ const bad = [];
79
+ const rp = m.ruleParams;
80
+ if (!isPlainObject(rp) || Object.keys(rp).some((k) => ![...PROFILES, 'common', 'db'].includes(k)) || !isPlainObject(rp.common) || !isPlainObject(rp.be) || (rp.fe !== undefined && !isPlainObject(rp.fe)))
81
+ return ['ruleParams must be a map with common, be and optionally fe and db'];
82
+ else {
83
+ // common holds the parameters both sides share; a side may restate one of them (a valid value) as an override, nothing else.
84
+ // Edition lite: a side's optional `lite` map overrides keys of the same side (never `lite` itself) when the app declares edition lite.
85
+ if (!sideOk(rp.common, []) || !fileLinesOk(rp.common.fileLines) || !blockOk(rp.common.duplicateBlock))
86
+ bad.push('ruleParams.common needs fileLines {soft, hardGrowth} and duplicateBlock {lines >= 2, tokens >= 1}; optional functionLines and featureFiles must be positive integers');
87
+ if (!sideOk(rp.be, BE_OWN, ['schemaAuthority', 'lite', 'dbFunctions']) || (rp.be.schemaAuthority !== undefined && !SCHEMA_AUTHORITIES.includes(rp.be.schemaAuthority)) || !dbFunctionsOk(rp.be.dbFunctions, { bare: true }) || !liteOk(rp.be.lite, [...BE_OWN, ...SHARED_KEYS, 'schemaAuthority', 'dbFunctions']))
88
+ bad.push('ruleParams.be needs infraOwners, eventBus, specDoubles, paramNames, suffixes, bannedSuffixes, contractShape {helper}, unitRoles, logicRoles, thinRoles, patternScenarios, kindPatterns, addKinds and backing (shared parameters live in ruleParams.common, a side may override them); optional: schemaAuthority (' + SCHEMA_AUTHORITIES.join(' | ') + '), dbFunctions ([{name, reason}]: a pg built-in bare, every other function schema.name) and lite (overrides of the same keys)');
89
+ else
90
+ bad.push(...unitRolesProblems(rp.be), ...roleListProblems(rp.be));
91
+ bad.push(...beSpecificProblems(rp.be, m));
92
+ if (rp.fe !== undefined && (!sideOk(rp.fe, [], ['lite']) || !liteOk(rp.fe.lite, SHARED_KEYS)))
93
+ bad.push('ruleParams.fe may only restate the shared parameters of ruleParams.common, and carry a lite override of them');
94
+ if (!dbParamsOk(rp.db))
95
+ bad.push('ruleParams.db may contain only extensions (a list of unique non-empty extension names)');
96
+ }
97
+ return bad;
85
98
  }
@@ -1,102 +1,97 @@
1
1
  // slot-app-view.mjs - the resolver of a whole app: a root path is answered by the scope of the app root, a path below a side folder
2
2
  // (be/, fe/) by that side's scope with the side prefixed back onto every path and root it returns. Only the declared `reads` cross sides.
3
3
  // slots.mjs builds the scopes (`root` and `sides`, each a resolver of one scope) and hands them to appResolver.
4
- import { APP_SCOPE, PROFILES } from './slot-manifest-shape.mjs';
4
+ import { APP_SCOPE, isSide, PROFILES } from './slot-manifest-shape.mjs';
5
5
  import { cleanSlotPath } from './slot-path.mjs';
6
-
6
+ /** True for the view of a whole app (profile app), the one resolveRepoDeclaration builds with both sides. */
7
+ export const isAppRepo = (repo) => repo.sides !== undefined;
7
8
  /** { side, rest } when `p` lies below a side folder, else null. */
8
9
  function splitSide(input) {
9
- const p = cleanSlotPath(input);
10
- const slash = p.indexOf('/');
11
- const head = slash < 0 ? p : p.slice(0, slash);
12
- return PROFILES.includes(head) && slash > 0 ? { side: head, rest: p.slice(slash + 1) } : null;
10
+ const p = cleanSlotPath(input);
11
+ const slash = p.indexOf('/');
12
+ const head = slash < 0 ? p : p.slice(0, slash);
13
+ return isSide(head) && slash > 0 ? { side: head, rest: p.slice(slash + 1) } : null;
13
14
  }
14
-
15
15
  const under = (side, rel) => (rel ? `${side}/${rel}` : side);
16
-
17
16
  function prefixed(side, c) {
18
- return {
19
- ...c,
20
- path: under(side, c.path),
21
- side,
22
- ...(c.root !== undefined ? { root: under(side, c.root) } : {}),
23
- ...(c.nearest ? { nearest: { ...c.nearest, matchedPrefix: under(side, c.nearest.matchedPrefix), matchedDepth: c.nearest.matchedDepth + 1 } } : {}),
24
- };
17
+ return {
18
+ ...c,
19
+ path: under(side, c.path),
20
+ side,
21
+ ...(c.root !== undefined ? { root: under(side, c.root) } : {}),
22
+ ...(c.nearest ? { nearest: { ...c.nearest, matchedPrefix: under(side, c.nearest.matchedPrefix), matchedDepth: c.nearest.matchedDepth + 1 } } : {}),
23
+ };
25
24
  }
26
-
27
25
  function classifyApp({ root, sides }, input) {
28
- const at = splitSide(input);
29
- return at ? prefixed(at.side, sides[at.side].classifyPath(at.rest)) : root.classifyPath(input);
26
+ const at = splitSide(input);
27
+ return at ? prefixed(at.side, sides[at.side].classifyPath(at.rest)) : root.classifyPath(input);
30
28
  }
31
-
32
29
  function ownerApp({ root, sides }, input) {
33
- const at = splitSide(input);
34
- if (!at) return root.ownerOf(input);
35
- const owner = sides[at.side].ownerOf(at.rest);
36
- return owner ? { ...owner, root: under(at.side, owner.root), side: at.side } : null;
30
+ const at = splitSide(input);
31
+ if (!at)
32
+ return root.ownerOf(input);
33
+ const owner = sides[at.side].ownerOf(at.rest);
34
+ return owner ? { ...owner, root: under(at.side, owner.root), side: at.side } : null;
37
35
  }
38
-
39
36
  /** Whether `toPath`, of the other side, lies below a path `fromSide` declares it reads. */
40
37
  const readsPath = (repo, fromSide, toPath) => repo.sides[fromSide].reads.some((read) => `${cleanSlotPath(toPath)}/`.startsWith(read));
41
-
42
38
  /** The verdict of an import from a side file to a path of the other side (or of the app root, `toSide` null): allowed only through a declared read. */
43
39
  function sideCrossing(repo, fromSide, toSide, toPath) {
44
- return readsPath(repo, fromSide, toPath)
45
- ? { allowed: true, reason: 'sideRead', fromSide, toSide }
46
- : { allowed: false, reason: 'crossSide', fromSide, toSide, reads: repo.sides[fromSide].reads };
40
+ return readsPath(repo, fromSide, toPath)
41
+ ? { allowed: true, reason: 'sideRead', fromSide, toSide }
42
+ : { allowed: false, reason: 'crossSide', fromSide, toSide, reads: repo.sides[fromSide].reads };
47
43
  }
48
-
49
44
  function importAllowedApp({ repo, root, sides }, fromPath, toPath) {
50
- const from = splitSide(fromPath);
51
- const to = splitSide(toPath);
52
- // A side file may read an app-root path (db/types/) only through a declared read, exactly as it reads the other side.
53
- if (from && from.side !== to?.side) return sideCrossing(repo, from.side, to?.side ?? null, toPath);
54
- if (from && to) return sides[from.side].importAllowed(from.rest, to.rest);
55
- return root.importAllowed(fromPath, toPath);
45
+ const from = splitSide(fromPath);
46
+ const to = splitSide(toPath);
47
+ // A side file may read an app-root path (db/types/) only through a declared read, exactly as it reads the other side.
48
+ if (from && from.side !== to?.side)
49
+ return sideCrossing(repo, from.side, to?.side ?? null, toPath);
50
+ if (from && to)
51
+ return sides[from.side].importAllowed(from.rest, to.rest);
52
+ return root.importAllowed(fromPath, toPath);
56
53
  }
57
-
58
54
  function requiredPathsApp({ root, sides }) {
59
- const own = root.requiredPaths();
60
- const paths = [...own.paths];
61
- const minimums = [...own.minimums];
62
- for (const side of PROFILES) {
63
- const required = sides[side].requiredPaths();
64
- paths.push(...required.paths.map((entry) => ({ ...entry, path: under(side, entry.path), side })));
65
- minimums.push(...required.minimums.map((entry) => ({ ...entry, side })));
66
- }
67
- return { paths, minimums };
55
+ const own = root.requiredPaths();
56
+ const paths = [...own.paths];
57
+ const minimums = [...own.minimums];
58
+ for (const side of PROFILES) {
59
+ const required = sides[side].requiredPaths();
60
+ paths.push(...required.paths.map((entry) => ({ ...entry, path: under(side, entry.path), side })));
61
+ minimums.push(...required.minimums.map((entry) => ({ ...entry, side })));
62
+ }
63
+ return { paths, minimums };
68
64
  }
69
-
70
65
  function slotEnabledApp({ root, sides }, slot) {
71
- if (slot.profiles.includes(APP_SCOPE)) return root.slotEnabled(slot);
72
- return PROFILES.some((side) => slot.profiles.includes(side) && sides[side].slotEnabled(slot));
66
+ if (slot.profiles.includes(APP_SCOPE))
67
+ return root.slotEnabled(slot);
68
+ return PROFILES.some((side) => slot.profiles.includes(side) && sides[side].slotEnabled(slot));
73
69
  }
74
-
75
70
  /** The resolver of the app `repo`: `root` answers its root paths and `sides[side]` the paths below that side's folder. */
76
71
  export function appResolver(repo, root, sides) {
77
- const view = { repo, root, sides };
78
- const allSlots = [...new Map([...root.slots(), ...PROFILES.flatMap((side) => sides[side].slots())].map((s) => [s.id, s])).values()];
79
- const byId = new Map(allSlots.map((s) => [s.id, s]));
80
- const classifyPath = (input) => classifyApp(view, input);
81
- return Object.freeze({
82
- repo,
83
- sides: Object.freeze(sides),
84
- /** The side of a path (be, fe) or null for a root path. */
85
- sideOf: (input) => splitSide(input)?.side ?? null,
86
- slot: (id) => byId.get(id) ?? null,
87
- slots: () => allSlots,
88
- slotEnabled: (slot) => slotEnabledApp(view, slot),
89
- classifyPath,
90
- slotOf: (p) => { const c = classifyPath(p); return c.slot ? byId.get(c.slot) : null; },
91
- ownerOf: (input) => ownerApp(view, input),
92
- tierOf: (input) => { const at = splitSide(input); return at ? sides[at.side].tierOf(at.rest) : root.tierOf(input); },
93
- importAllowed: (fromPath, toPath) => importAllowedApp(view, fromPath, toPath),
94
- requiredFiles: (input) => { const at = splitSide(input); return at ? sides[at.side].requiredFiles(at.rest).map((p) => under(at.side, p)) : root.requiredFiles(input); },
95
- requiredPaths: () => requiredPathsApp(view),
96
- trackingOf: (p) => classifyPath(p).tracking ?? null,
97
- isTracked: (p) => classifyPath(p).tracking === 'tracked',
98
- // The root has no tier and no rule parameters of its own; each side has them (sides.<side>.ruleParams()).
99
- ruleParams: () => null,
100
- allowedImports: () => null,
101
- });
72
+ const view = { repo, root, sides };
73
+ const allSlots = [...new Map([...root.slots(), ...PROFILES.flatMap((side) => sides[side].slots())].map((s) => [s.id, s])).values()];
74
+ const byId = new Map(allSlots.map((s) => [s.id, s]));
75
+ const classifyPath = (input) => classifyApp(view, input);
76
+ return Object.freeze({
77
+ repo,
78
+ sides: Object.freeze(sides),
79
+ /** The side of a path (be, fe) or null for a root path. */
80
+ sideOf: (input) => splitSide(input)?.side ?? null,
81
+ slot: (id) => byId.get(id) ?? null,
82
+ slots: () => allSlots,
83
+ slotEnabled: (slot) => slotEnabledApp(view, slot),
84
+ classifyPath,
85
+ slotOf: (p) => { const c = classifyPath(p); return c.slot ? byId.get(c.slot) : null; },
86
+ ownerOf: (input) => ownerApp(view, input),
87
+ tierOf: (input) => { const at = splitSide(input); return at ? sides[at.side].tierOf(at.rest) : root.tierOf(input); },
88
+ importAllowed: (fromPath, toPath) => importAllowedApp(view, fromPath, toPath),
89
+ requiredFiles: (input) => { const at = splitSide(input); return at ? sides[at.side].requiredFiles(at.rest).map((p) => under(at.side, p)) : root.requiredFiles(input); },
90
+ requiredPaths: () => requiredPathsApp(view),
91
+ trackingOf: (p) => classifyPath(p).tracking ?? null,
92
+ isTracked: (p) => classifyPath(p).tracking === 'tracked',
93
+ // The root has no tier and no rule parameters of its own; each side has them (sides.<side>.ruleParams()).
94
+ ruleParams: () => null,
95
+ allowedImports: () => null,
96
+ });
102
97
  }
@@ -4,62 +4,66 @@ import { globExpression } from '../lib/glob.mjs';
4
4
  import { declaredSlotEnabled } from './declaration-slots.mjs';
5
5
  import { bestMatch, fillVars, matchVariants, nearestSlot } from './slot-match.mjs';
6
6
  import { cleanSlotPath } from './slot-path.mjs';
7
-
8
7
  /** Whether the repository of the scope holds the slot. */
9
8
  export function slotEnabled(scope, slot) {
10
- // A provider slot is gated by its provider in every edition: litePresence may lift it to required, but it still
11
- // exists for this repository only while a connection declares the provider.
12
- if (slot.provider !== undefined) return declaredSlotEnabled(slot, scope.repo);
13
- if (slot.presence !== 'opt-in') return true;
14
- return declaredSlotEnabled(slot, scope.repo);
9
+ // A provider slot is gated by its provider in every edition: litePresence may lift it to required, but it still
10
+ // exists for this repository only while a connection declares the provider.
11
+ if (slot.provider !== undefined)
12
+ return declaredSlotEnabled(slot, scope.repo);
13
+ if (slot.presence !== 'opt-in')
14
+ return true;
15
+ return declaredSlotEnabled(slot, scope.repo);
15
16
  }
16
-
17
17
  /** The folder kind of a file in its slot, from a named layer or kind folder. */
18
18
  function kindOf({ variant, slot, root }, p) {
19
- const names = [...(slot.layers ?? []), ...(slot.kinds ?? [])];
20
- if (!names.length) return null;
21
- const literal = variant.segments.find((segment) => names.includes(segment));
22
- if (literal) return literal;
23
- const below = root ? p.slice(root.length + 1) : p;
24
- return below.split('/').slice(0, -1).find((segment) => names.includes(segment)) ?? null;
19
+ const names = [...(slot.layers ?? []), ...(slot.kinds ?? [])];
20
+ if (!names.length)
21
+ return null;
22
+ const literal = variant.segments.find((segment) => names.includes(segment));
23
+ if (literal)
24
+ return literal;
25
+ const below = root ? p.slice(root.length + 1) : p;
26
+ return below.split('/').slice(0, -1).find((segment) => names.includes(segment)) ?? null;
25
27
  }
26
-
27
28
  /** The role of a file in its slot: the entry of `roles` whose file name (variables filled from the path, `*` a wildcard inside the name) matches the file's name. */
28
29
  function roleOf(slot, bindings, p) {
29
- const name = p.split('/').pop();
30
- for (const [role, file] of Object.entries(slot.roles ?? {})) if (globExpression(fillVars(file, bindings)).test(name)) return role;
31
- return null;
30
+ const name = p.split('/').pop();
31
+ for (const [role, file] of Object.entries(slot.roles ?? {}))
32
+ if (globExpression(fillVars(file, bindings)).test(name))
33
+ return role;
34
+ return null;
32
35
  }
33
-
34
36
  /**
35
37
  * status: owned | forbidden (external slot) | not-enabled (opt-in slot the repository did not declare) | ambiguous
36
38
  * (two slots of equal specificity; a manifest gap) | no-slot (code HFS_SLOT_UNDECLARED, with the nearest slot).
37
39
  */
38
40
  export function classifyIn(scope, input) {
39
- const p = cleanSlotPath(input);
40
- const { hit, ambiguous } = bestMatch(matchVariants(scope, p));
41
- if (ambiguous.length) return { path: p, status: 'ambiguous', candidates: ambiguous };
42
- if (!hit) return { path: p, status: 'no-slot', code: 'HFS_SLOT_UNDECLARED', nearest: nearestSlot(scope.variants, p) };
43
- const { slot, root, bindings } = hit;
44
- const status = (slot.presence === 'forbidden' && 'forbidden') || (slotEnabled(scope, slot) ? 'owned' : 'not-enabled');
45
- const kind = kindOf(hit, p);
46
- const role = roleOf(slot, bindings, p);
47
- return { path: p, status, slot: slot.id, root, bindings, ...(kind ? { kind } : {}), ...(role ? { role } : {}), presence: slot.presence, tracking: slot.tracked, ...(status === 'forbidden' ? { goesTo: slot.goesTo } : {}) };
41
+ const p = cleanSlotPath(input);
42
+ const { hit, ambiguous } = bestMatch(matchVariants(scope, p));
43
+ if (ambiguous.length)
44
+ return { path: p, status: 'ambiguous', candidates: ambiguous };
45
+ if (!hit)
46
+ return { path: p, status: 'no-slot', code: 'HFS_SLOT_UNDECLARED', nearest: nearestSlot(scope.variants, p) };
47
+ const { slot, root, bindings } = hit;
48
+ const status = (slot.presence === 'forbidden' && 'forbidden') || (slotEnabled(scope, slot) ? 'owned' : 'not-enabled');
49
+ const kind = kindOf(hit, p);
50
+ const role = roleOf(slot, bindings, p);
51
+ return { path: p, status, slot: slot.id, root, bindings, ...(kind ? { kind } : {}), ...(role ? { role } : {}), presence: slot.presence, tracking: slot.tracked, ...(status === 'forbidden' ? { goesTo: slot.goesTo } : {}) };
48
52
  }
49
-
50
53
  /** The owner unit of `p`: the most specific owner slot instance containing it (a slot's own root when it owns nothing). */
51
54
  export function ownerIn(scope, input) {
52
- const p = cleanSlotPath(input);
53
- const { hit } = bestMatch(matchVariants(scope, p, (s) => s.owner === true));
54
- return hit ? { slot: hit.slot.id, root: hit.root, bindings: hit.bindings } : null;
55
+ const p = cleanSlotPath(input);
56
+ const { hit } = bestMatch(matchVariants(scope, p, (s) => s.owner === true));
57
+ return hit ? { slot: hit.slot.id, root: hit.root, bindings: hit.bindings } : null;
55
58
  }
56
-
57
59
  /** The tier of `p` in the direction matrix: its slot's tier, the owner's for an inheriting slot, none for an untiered one; null when no slot owns it. */
58
60
  export function tierIn(scope, input) {
59
- const c = classifyIn(scope, input);
60
- if (!c.slot) return null;
61
- const tier = scope.byId.get(c.slot).tier;
62
- if (tier !== 'inherit') return tier;
63
- const owner = ownerIn(scope, input);
64
- return owner ? scope.byId.get(owner.slot).tier : 'none';
61
+ const c = classifyIn(scope, input);
62
+ if (!c.slot)
63
+ return null;
64
+ const tier = scope.byId.get(c.slot).tier;
65
+ if (tier !== 'inherit')
66
+ return tier;
67
+ const owner = ownerIn(scope, input);
68
+ return owner ? scope.byId.get(owner.slot).tier : 'none';
65
69
  }
@@ -1,11 +1,12 @@
1
1
  // slot-errors.mjs - the shared refusal type for HFS manifest and declaration loaders.
2
2
  export class HfsSlotsError extends Error {
3
- constructor(code, message, details = {}) {
4
- super(`${code}: ${message}`);
5
- this.name = 'HfsSlotsError';
6
- this.code = code;
7
- this.details = details;
8
- }
3
+ code;
4
+ details;
5
+ constructor(code, message, details = {}) {
6
+ super(`${code}: ${message}`);
7
+ this.name = 'HfsSlotsError';
8
+ this.code = code;
9
+ this.details = details;
10
+ }
9
11
  }
10
-
11
12
  export const fail = (code, message, details) => { throw new HfsSlotsError(code, message, details); };