@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
@@ -3,89 +3,96 @@
3
3
  import { RUNTIME_KIND } from './manifest-shape.mjs';
4
4
  import { classifyIn, ownerIn, tierIn } from './slot-classify.mjs';
5
5
  import { braceVariants, globExpression } from '../lib/glob.mjs';
6
-
7
6
  const isEntryFile = (name) => name === 'index.ts' || name === 'index.tsx';
8
-
9
7
  function declaredEntry(slot, relative) {
10
- return (slot.entries ?? []).some(entry => braceVariants(entry).some(pattern => globExpression(pattern).test(relative)));
8
+ return (slot.entries ?? []).some((entry) => braceVariants(entry).some((pattern) => globExpression(pattern).test(relative)));
11
9
  }
12
-
13
10
  function unownedPathProblem(from, to) {
14
- for (const side of [from, to]) {
15
- if (side.status === 'no-slot') return { allowed: false, reason: 'unowned', code: side.code, path: side.path, nearest: side.nearest };
16
- }
17
- return null;
11
+ for (const side of [from, to]) {
12
+ if (side.status === 'no-slot')
13
+ return { allowed: false, reason: 'unowned', code: side.code, path: side.path, nearest: side.nearest };
14
+ }
15
+ return null;
18
16
  }
19
-
20
17
  function unavailableSlotProblem(from, to) {
21
- for (const side of [from, to]) {
22
- if (side.status !== 'owned') return { allowed: false, reason: `slot${side.status[0].toUpperCase()}${side.status.slice(1).replace(/-(.)/g, (_, c) => c.toUpperCase())}`, path: side.path, slot: side.slot };
23
- }
24
- return null;
18
+ for (const side of [from, to]) {
19
+ if (side.status !== 'owned')
20
+ return { allowed: false, reason: `slot${side.status[0].toUpperCase()}${side.status.slice(1).replace(/-(.)/g, (_, c) => c.toUpperCase())}`, path: side.path, slot: side.slot };
21
+ }
22
+ return null;
25
23
  }
26
-
27
24
  function sameOwnerUnit(side, owner) {
28
- return owner ? `${owner.slot}:${owner.root}` : `${side.slot}:${side.root}`;
25
+ return owner ? `${owner.slot}:${owner.root}` : `${side.slot}:${side.root}`;
29
26
  }
30
-
31
27
  const layerIndex = (slot, root) => (slot.layers ? slot.layers.findIndex((layer) => root.split('/').includes(layer)) : -1);
32
-
33
28
  function lowerLayerProblem({ manifest, profile, byId }, from, to, fromTier, toTier, fromOwner, toOwner) {
34
- if (manifest.tiers[profile][fromTier].lowerLayerOnly && fromTier === toTier) {
35
- const fromSlot = byId.get((fromOwner ?? from).slot);
36
- const a = layerIndex(fromSlot, fromOwner?.root ?? from.root);
37
- const b = layerIndex(byId.get((toOwner ?? to).slot), toOwner?.root ?? to.root);
38
- if (a >= 0 && b >= 0 && b <= a) return { allowed: false, reason: 'layerOrder', fromLayer: fromSlot.layers[a], toLayer: fromSlot.layers[b] };
39
- }
40
- return null;
29
+ if (manifest.tiers[profile][fromTier].lowerLayerOnly && fromTier === toTier) {
30
+ const fromSlot = byId.get((fromOwner ?? from).slot);
31
+ const a = layerIndex(fromSlot, fromOwner?.root ?? from.root);
32
+ const b = layerIndex(byId.get((toOwner ?? to).slot), toOwner?.root ?? to.root);
33
+ if (a >= 0 && b >= 0 && b <= a)
34
+ return { allowed: false, reason: 'layerOrder', fromLayer: fromSlot.layers[a], toLayer: fromSlot.layers[b] };
35
+ }
36
+ return null;
41
37
  }
42
-
43
38
  function privateEntryProblem({ repo, byId }, to, toOwner) {
44
- if (toOwner && repo.kind !== RUNTIME_KIND) {
45
- const relative = to.path === toOwner.root ? '' : to.path.slice(toOwner.root.length + 1);
46
- const ownerSlot = byId.get(toOwner.slot);
47
- const entry = isEntryFile(relative) || declaredEntry(byId.get(to.slot), to.path.slice(to.root.length + 1)) || (ownerSlot.tier === 'package' && relative === 'src/index.ts') || (ownerSlot.tier === 'app' && relative === 'app.module.ts');
48
- if (!entry) return { allowed: false, reason: 'notPublicEntry', owner: toOwner.root, path: to.path };
49
- }
50
- return null;
39
+ if (toOwner && repo.kind !== RUNTIME_KIND) {
40
+ const relative = to.path === toOwner.root ? '' : to.path.slice(toOwner.root.length + 1);
41
+ const ownerSlot = byId.get(toOwner.slot);
42
+ const entry = isEntryFile(relative) || declaredEntry(byId.get(to.slot), to.path.slice(to.root.length + 1)) || (ownerSlot.tier === 'package' && relative === 'src/index.ts') || (ownerSlot.tier === 'app' && relative === 'app.module.ts');
43
+ if (!entry)
44
+ return { allowed: false, reason: 'notPublicEntry', owner: toOwner.root, path: to.path };
45
+ }
46
+ return null;
51
47
  }
52
-
53
48
  function tierImportProblem(scope, from, to, fromTier, toTier) {
54
- const { mayImport, mayImportKinds, mayImportSlots } = scope.manifest.tiers[scope.profile][fromTier] ?? {};
55
- if (!mayImport?.includes(toTier)) return { allowed: false, reason: 'tierDirection', fromTier, toTier, mayImport: mayImport ?? [] };
56
- const kindRestriction = mayImportKinds?.[toTier];
57
- if (kindRestriction && (!kindRestriction.from.includes(from.kind) || !kindRestriction.to.includes(to.kind))) return { allowed: false, reason: 'tierDirection', fromTier, toTier, mayImport, kindRestriction, fromKind: from.kind ?? null, toKind: to.kind ?? null };
58
- const slotRestriction = mayImportSlots?.[toTier];
59
- if (slotRestriction && !slotRestriction.from.includes(from.slot)) return { allowed: false, reason: 'tierDirection', fromTier, toTier, mayImport, slotRestriction, fromSlot: from.slot };
60
- return null;
49
+ const { mayImport, mayImportKinds, mayImportSlots } = scope.manifest.tiers[scope.profile][fromTier] ?? {};
50
+ if (!mayImport?.includes(toTier))
51
+ return { allowed: false, reason: 'tierDirection', fromTier, toTier, mayImport: mayImport ?? [] };
52
+ const kindRestriction = mayImportKinds?.[toTier];
53
+ if (kindRestriction && (!kindRestriction.from.includes(from.kind) || !kindRestriction.to.includes(to.kind)))
54
+ return { allowed: false, reason: 'tierDirection', fromTier, toTier, mayImport, kindRestriction, fromKind: from.kind ?? null, toKind: to.kind ?? null };
55
+ const slotRestriction = mayImportSlots?.[toTier];
56
+ if (slotRestriction && !slotRestriction.from.includes(from.slot))
57
+ return { allowed: false, reason: 'tierDirection', fromTier, toTier, mayImport, slotRestriction, fromSlot: from.slot };
58
+ return null;
61
59
  }
62
-
63
60
  /**
64
61
  * Whether `fromPath` may import `toPath`: {allowed, reason, ...}. Reasons: sameOwner, untiered, crossApp,
65
62
  * tierDirection, layerOrder, notPublicEntry, allowed, slotForbidden, slotNotEnabled, slotAmbiguous, and unowned (HFS_SLOT_UNDECLARED for the path no slot owns).
66
63
  * Cycles are a graph property and belong to the architecture check, not to one edge.
67
64
  */
68
65
  export function importAllowedIn(scope, fromPath, toPath) {
69
- const from = classifyIn(scope, fromPath);
70
- const to = classifyIn(scope, toPath);
71
- const unowned = unownedPathProblem(from, to);
72
- if (unowned) return unowned;
73
- const unavailable = unavailableSlotProblem(from, to);
74
- if (unavailable) return unavailable;
75
- const fromTier = tierIn(scope, from.path);
76
- const toTier = tierIn(scope, to.path);
77
- if (fromTier === 'none' || toTier === 'none') return { allowed: true, reason: 'untiered' };
78
- if (from.bindings.app !== undefined && to.bindings.app !== undefined && from.bindings.app !== to.bindings.app)
79
- return { allowed: false, reason: 'crossApp', from: from.bindings.app, to: to.bindings.app };
80
- const fromOwner = ownerIn(scope, from.path);
81
- const toOwner = ownerIn(scope, to.path);
82
- if (sameOwnerUnit(from, fromOwner) === sameOwnerUnit(to, toOwner)) return { allowed: true, reason: 'sameOwner' };
83
- const tierProblem = tierImportProblem(scope, from, to, fromTier, toTier);
84
- if (tierProblem) return tierProblem;
85
- const layerProblem = lowerLayerProblem(scope, from, to, fromTier, toTier, fromOwner, toOwner);
86
- if (layerProblem) return layerProblem;
87
- // A runtime owner is imported file by file (crossOwner of knowledge/hfs/runtime-slots.yaml): no public entry.
88
- const privateEntry = privateEntryProblem(scope, to, toOwner);
89
- if (privateEntry) return privateEntry;
90
- return { allowed: true, reason: 'allowed', fromTier, toTier };
66
+ const fromClassified = classifyIn(scope, fromPath);
67
+ const toClassified = classifyIn(scope, toPath);
68
+ const unowned = unownedPathProblem(fromClassified, toClassified);
69
+ if (unowned)
70
+ return unowned;
71
+ const unavailable = unavailableSlotProblem(fromClassified, toClassified);
72
+ if (unavailable)
73
+ return unavailable;
74
+ // reason: unavailableSlotProblem answers for every status but owned, so both paths are placed from here on
75
+ const from = fromClassified;
76
+ const to = toClassified;
77
+ const fromTier = tierIn(scope, from.path);
78
+ const toTier = tierIn(scope, to.path);
79
+ if (fromTier === 'none' || toTier === 'none')
80
+ return { allowed: true, reason: 'untiered' };
81
+ if (from.bindings.app !== undefined && to.bindings.app !== undefined && from.bindings.app !== to.bindings.app)
82
+ return { allowed: false, reason: 'crossApp', from: from.bindings.app, to: to.bindings.app };
83
+ const fromOwner = ownerIn(scope, from.path);
84
+ const toOwner = ownerIn(scope, to.path);
85
+ if (sameOwnerUnit(from, fromOwner) === sameOwnerUnit(to, toOwner))
86
+ return { allowed: true, reason: 'sameOwner' };
87
+ const tierProblem = tierImportProblem(scope, from, to, fromTier, toTier);
88
+ if (tierProblem)
89
+ return tierProblem;
90
+ const layerProblem = lowerLayerProblem(scope, from, to, fromTier, toTier, fromOwner, toOwner);
91
+ if (layerProblem)
92
+ return layerProblem;
93
+ // A runtime owner is imported file by file (crossOwner of knowledge/hfs/runtime-slots.yaml): no public entry.
94
+ const privateEntry = privateEntryProblem(scope, to, toOwner);
95
+ if (privateEntry)
96
+ return privateEntry;
97
+ return { allowed: true, reason: 'allowed', fromTier, toTier };
91
98
  }
@@ -1,74 +1,96 @@
1
- // slot-manifest-shape.mjs - the product slot manifest's schema-shaped validation.
2
1
  import { isPlainObject } from '../../engine/plain-object.mjs';
3
2
  import { byCodeUnit } from '../lib/list.mjs';
4
3
  import { ruleParamsProblems } from './rule-params-shape.mjs';
5
4
  import { APP_KIND, EDITIONS, MANIFEST_KINDS, NAME, PRESENCE, RUNTIME_KIND, SEMVER, TESTS, TRACKED, manifestKind, runtimeShapeProblems, slotProblems, tierMapProblems } from './manifest-shape.mjs';
6
-
7
5
  export const PROFILES = ['be', 'fe'];
6
+ /** Whether `value` names a side of an app. */
7
+ export const isSide = (value) => PROFILES.includes(value);
8
8
  export const APP_SCOPE = 'app';
9
9
  export const SCOPES = [APP_SCOPE, ...PROFILES];
10
-
11
10
  /** Shape problems of a parsed manifest, in the words of modules/schemas/hfs-slots.schema.yaml. */
12
11
  function checkCoreManifestFields(m, bad) {
13
- const allowed = new Set(['schema', 'kind', 'version', 'versioning', 'presenceValues', 'trackedValues', 'testValues', 'editions', 'sides', 'appKinds', 'triggerKinds', 'tiers', 'ruleParams', 'crossOwner', 'crossApp', 'slots', 'consumers', 'naming']);
14
- for (const key of Object.keys(m)) if (!allowed.has(key)) bad.push(`unknown top-level key ${key}`);
15
- if (!/^starci\/hfs-slots@\d+$/.test(String(m.schema))) bad.push('schema must be starci/hfs-slots@<major>');
16
- if (!SEMVER.test(String(m.version))) bad.push('version must be MAJOR.MINOR.PATCH');
17
- if (!isPlainObject(m.versioning) || !['patch', 'minor', 'major', 'pins'].every((k) => typeof m.versioning[k] === 'string')) bad.push('versioning needs patch, minor, major and pins text');
18
- if (JSON.stringify(m.presenceValues) !== JSON.stringify(PRESENCE)) bad.push(`presenceValues must be ${PRESENCE.join(', ')}`);
19
- if (JSON.stringify(m.trackedValues) !== JSON.stringify(TRACKED)) bad.push(`trackedValues must be ${TRACKED.join(', ')}`);
20
- if (JSON.stringify(m.testValues) !== JSON.stringify(TESTS)) bad.push(`testValues must be ${TESTS.join(', ')}`);
21
- if (JSON.stringify(m.editions) !== JSON.stringify(EDITIONS)) bad.push(`editions must be ${EDITIONS.join(', ')}`);
12
+ const allowed = new Set(['schema', 'kind', 'version', 'versioning', 'presenceValues', 'trackedValues', 'testValues', 'editions', 'sides', 'appKinds', 'triggerKinds', 'tiers', 'ruleParams', 'crossOwner', 'crossApp', 'slots', 'consumers', 'naming']);
13
+ for (const key of Object.keys(m))
14
+ if (!allowed.has(key))
15
+ bad.push(`unknown top-level key ${key}`);
16
+ if (!/^starci\/hfs-slots@\d+$/.test(String(m.schema)))
17
+ bad.push('schema must be starci/hfs-slots@<major>');
18
+ if (!SEMVER.test(String(m.version)))
19
+ bad.push('version must be MAJOR.MINOR.PATCH');
20
+ if (!isPlainObject(m.versioning) || !['patch', 'minor', 'major', 'pins'].every((k) => typeof m.versioning[k] === 'string'))
21
+ bad.push('versioning needs patch, minor, major and pins text');
22
+ if (JSON.stringify(m.presenceValues) !== JSON.stringify(PRESENCE))
23
+ bad.push(`presenceValues must be ${PRESENCE.join(', ')}`);
24
+ if (JSON.stringify(m.trackedValues) !== JSON.stringify(TRACKED))
25
+ bad.push(`trackedValues must be ${TRACKED.join(', ')}`);
26
+ if (JSON.stringify(m.testValues) !== JSON.stringify(TESTS))
27
+ bad.push(`testValues must be ${TESTS.join(', ')}`);
28
+ if (JSON.stringify(m.editions) !== JSON.stringify(EDITIONS))
29
+ bad.push(`editions must be ${EDITIONS.join(', ')}`);
22
30
  }
23
-
24
31
  function checkSideShapes(m, bad) {
25
- if (!isPlainObject(m.sides) || Object.keys(m.sides).sort(byCodeUnit).join() !== PROFILES.join()) bad.push('sides must be a map with exactly be and fe');
26
- else for (const side of PROFILES) {
27
- const def = m.sides[side];
28
- if (!isPlainObject(def) || Object.keys(def).join() !== 'reads' || !Array.isArray(def.reads) || !def.reads.every((r) => typeof r === 'string' && /^[a-z][a-z0-9-]*\/([^/]+\/)+$/.test(r)) || new Set(def.reads).size !== def.reads.length) bad.push(`sides.${side} must be {reads: [unique <owner>/<dir>/ paths]}`);
29
- }
32
+ if (!isPlainObject(m.sides) || Object.keys(m.sides).sort(byCodeUnit).join() !== PROFILES.join())
33
+ bad.push('sides must be a map with exactly be and fe');
34
+ else
35
+ for (const side of PROFILES) {
36
+ const def = m.sides[side];
37
+ if (!isPlainObject(def) || Object.keys(def).join() !== 'reads' || !Array.isArray(def.reads) || !def.reads.every((r) => typeof r === 'string' && /^[a-z][a-z0-9-]*\/([^/]+\/)+$/.test(r)) || new Set(def.reads).size !== def.reads.length)
38
+ bad.push(`sides.${side} must be {reads: [unique <owner>/<dir>/ paths]}`);
39
+ }
30
40
  }
31
-
32
41
  function checkTriggerKinds(m, bad) {
33
- if (m.triggerKinds !== undefined && (!Array.isArray(m.triggerKinds) || !m.triggerKinds.length || !m.triggerKinds.every((k) => NAME.test(String(k))) || new Set(m.triggerKinds).size !== m.triggerKinds.length)) bad.push('triggerKinds must be a non-empty list of unique names');
42
+ if (m.triggerKinds !== undefined && (!Array.isArray(m.triggerKinds) || !m.triggerKinds.length || !m.triggerKinds.every((k) => NAME.test(String(k))) || new Set(m.triggerKinds).size !== m.triggerKinds.length))
43
+ bad.push('triggerKinds must be a non-empty list of unique names');
34
44
  }
35
-
36
45
  function checkProfileMaps(m, bad) {
37
- for (const key of ['appKinds', 'tiers']) {
38
- if (!isPlainObject(m[key])) { bad.push(`${key} must be a map with be and fe`); continue; }
39
- for (const extra of Object.keys(m[key])) if (!PROFILES.includes(extra)) bad.push(`${key}.${extra} is not a profile`);
40
- for (const profile of PROFILES) if (!(profile in m[key])) bad.push(`${key}.${profile} is missing`);
41
- }
46
+ for (const key of ['appKinds', 'tiers']) {
47
+ if (!isPlainObject(m[key])) {
48
+ bad.push(`${key} must be a map with be and fe`);
49
+ continue;
50
+ }
51
+ for (const extra of Object.keys(m[key]))
52
+ if (!isSide(extra))
53
+ bad.push(`${key}.${extra} is not a profile`);
54
+ for (const profile of PROFILES)
55
+ if (!(profile in m[key]))
56
+ bad.push(`${key}.${profile} is missing`);
57
+ }
42
58
  }
43
-
44
59
  function checkAppKindAndTierShapes(m, bad) {
45
- for (const profile of PROFILES) {
46
- const kinds = m.appKinds?.[profile];
47
- if (kinds !== undefined && (!Array.isArray(kinds) || !kinds.length || !kinds.every((k) => NAME.test(String(k))) || new Set(kinds).size !== kinds.length)) bad.push(`appKinds.${profile} must be a non-empty list of unique names`);
48
- const tiers = m.tiers?.[profile];
49
- if (tiers === undefined) continue;
50
- bad.push(...tierMapProblems(profile, tiers));
51
- }
60
+ for (const profile of PROFILES) {
61
+ const kinds = m.appKinds?.[profile];
62
+ if (kinds !== undefined && (!Array.isArray(kinds) || !kinds.length || !kinds.every((k) => NAME.test(String(k))) || new Set(kinds).size !== kinds.length))
63
+ bad.push(`appKinds.${profile} must be a non-empty list of unique names`);
64
+ const tiers = m.tiers?.[profile];
65
+ if (tiers === undefined)
66
+ continue;
67
+ bad.push(...tierMapProblems(profile, tiers));
68
+ }
52
69
  }
53
-
54
70
  function checkSlotShapes(m, bad) {
55
- if (!Array.isArray(m.slots) || !m.slots.length) { bad.push('slots must be a non-empty list'); return false; }
56
- const logicRoles = Array.isArray(m.ruleParams?.be?.logicRoles) ? m.ruleParams.be.logicRoles : [];
57
- m.slots.forEach((slot, index) => bad.push(...slotProblems(slot, index, APP_KIND, { appScope: APP_SCOPE, scopes: SCOPES, logicRoles })));
58
- return true;
71
+ if (!Array.isArray(m.slots) || !m.slots.length) {
72
+ bad.push('slots must be a non-empty list');
73
+ return false;
74
+ }
75
+ const logicRoles = Array.isArray(m.ruleParams?.be?.logicRoles) ? m.ruleParams.be.logicRoles : [];
76
+ m.slots.forEach((slot, index) => bad.push(...slotProblems(slot, index, APP_KIND, { appScope: APP_SCOPE, scopes: SCOPES, logicRoles })));
77
+ return true;
59
78
  }
60
-
61
79
  export function manifestShapeProblems(m) {
62
- const bad = [];
63
- if (!isPlainObject(m)) return ['the manifest is not a map'];
64
- if (!MANIFEST_KINDS.includes(manifestKind(m))) return [`kind must be one of ${MANIFEST_KINDS.join(', ')}`];
65
- if (manifestKind(m) === RUNTIME_KIND) return runtimeShapeProblems(m);
66
- checkCoreManifestFields(m, bad);
67
- checkSideShapes(m, bad);
68
- checkTriggerKinds(m, bad);
69
- checkProfileMaps(m, bad);
70
- checkAppKindAndTierShapes(m, bad);
71
- bad.push(...ruleParamsProblems(m));
72
- if (!checkSlotShapes(m, bad)) return bad;
73
- return bad;
80
+ const bad = [];
81
+ if (!isPlainObject(m))
82
+ return ['the manifest is not a map'];
83
+ if (!MANIFEST_KINDS.includes(manifestKind(m)))
84
+ return [`kind must be one of ${MANIFEST_KINDS.join(', ')}`];
85
+ if (manifestKind(m) === RUNTIME_KIND)
86
+ return runtimeShapeProblems(m);
87
+ checkCoreManifestFields(m, bad);
88
+ checkSideShapes(m, bad);
89
+ checkTriggerKinds(m, bad);
90
+ checkProfileMaps(m, bad);
91
+ checkAppKindAndTierShapes(m, bad);
92
+ bad.push(...ruleParamsProblems(m));
93
+ if (!checkSlotShapes(m, bad))
94
+ return bad;
95
+ return bad;
74
96
  }
@@ -3,151 +3,162 @@
3
3
  import path from 'node:path';
4
4
  import { braceVariants } from '../lib/glob.mjs';
5
5
  import { captureNames } from '../lib/i18n.mjs';
6
-
7
6
  const VAR = /<([A-Za-z][A-Za-z0-9-]*)>/g;
8
7
  export const varsOf = (text) => captureNames(text, VAR);
9
8
  const hasWildcard = (segment) => /[*?]/.test(segment);
10
-
11
9
  /** `text` with the `<name>` variables filled from `bindings` (an unbound one stays as written). */
12
10
  export function fillVars(text, bindings) {
13
- const bound = (whole, name) => bindings[name] ?? whole;
14
- return `${text}`.replace(VAR, bound);
11
+ const bound = (whole, name) => bindings[name] ?? whole;
12
+ return `${text}`.replace(VAR, bound);
15
13
  }
16
-
17
14
  /** Weight of one pattern segment: literal 4, literal mixed with a variable 3, a bare variable 2, a wildcard 1, `**` 0. */
18
15
  function segmentWeight(segment) {
19
- if (segment === '**') return 0;
20
- if (hasWildcard(segment)) return 1;
21
- if (/^<[^>]+>$/.test(segment)) return 2;
22
- if (segment.includes('<')) return 3;
23
- return 4;
16
+ if (segment === '**')
17
+ return 0;
18
+ if (hasWildcard(segment))
19
+ return 1;
20
+ if (/^<[^>]+>$/.test(segment))
21
+ return 2;
22
+ if (segment.includes('<'))
23
+ return 3;
24
+ return 4;
24
25
  }
25
-
26
26
  /** The RegExp source of one pattern segment (no separators); variables capture, and are listed in `names`. */
27
27
  function segmentSource(segment, names) {
28
- let source = '', variableEnd = -1;
29
- for (let i = 0; i < segment.length; i += 1) {
30
- const c = segment[i];
31
- if (i <= variableEnd) continue;
32
- if (c === '<') {
33
- variableEnd = segment.indexOf('>', i);
34
- names.push(segment.slice(i + 1, variableEnd));
35
- source += '([^/]+?)';
36
- } else if (c === '*') source += '[^/]*';
37
- else if (c === '?') source += '[^/]';
38
- else source += /[.+^${}()|[\]\\]/.test(c) ? `\\${c}` : c;
39
- }
40
- return source;
28
+ let source = '', variableEnd = -1;
29
+ for (let i = 0; i < segment.length; i += 1) {
30
+ const c = segment[i];
31
+ if (i <= variableEnd)
32
+ continue;
33
+ if (c === '<') {
34
+ variableEnd = segment.indexOf('>', i);
35
+ names.push(segment.slice(i + 1, variableEnd));
36
+ source += '([^/]+?)';
37
+ }
38
+ else if (c === '*')
39
+ source += '[^/]*';
40
+ else if (c === '?')
41
+ source += '[^/]';
42
+ else
43
+ source += /[.+^${}()|[\]\\]/.test(c) ? `\\${c}` : c;
44
+ }
45
+ return source;
41
46
  }
42
-
43
47
  /** One brace-free pattern compiled: `dir` patterns (trailing /) own everything below their root. */
44
48
  export function compileVariant(slot, pattern) {
45
- const dir = pattern.endsWith('/');
46
- const body = dir ? pattern.slice(0, -1) : pattern;
47
- const segments = body.split('/');
48
- const names = [];
49
- let source = '';
50
- segments.forEach((segment, index) => {
51
- const last = index === segments.length - 1;
52
- if (segment === '**') source += last ? '.*' : '(?:.*/)?';
53
- else source += segmentSource(segment, names) + (last ? '' : '/');
54
- });
55
- return {
56
- slot,
57
- pattern,
58
- dir,
59
- segments,
60
- names,
61
- score: segments.reduce((sum, s) => sum + segmentWeight(s), 0),
62
- wildcards: segments.filter(hasWildcard).length,
63
- regex: new RegExp(`^(${source})${dir ? '(?:/.*)?' : ''}$`),
64
- segmentRegexes: segments.map((s) => (s === '**' ? null : new RegExp(`^${segmentSource(s, [])}$`))),
65
- };
49
+ const dir = pattern.endsWith('/');
50
+ const body = dir ? pattern.slice(0, -1) : pattern;
51
+ const segments = body.split('/');
52
+ const names = [];
53
+ let source = '';
54
+ segments.forEach((segment, index) => {
55
+ const last = index === segments.length - 1;
56
+ if (segment === '**')
57
+ source += last ? '.*' : '(?:.*/)?';
58
+ else
59
+ source += segmentSource(segment, names) + (last ? '' : '/');
60
+ });
61
+ return {
62
+ slot,
63
+ pattern,
64
+ dir,
65
+ segments,
66
+ names,
67
+ score: segments.reduce((sum, s) => sum + segmentWeight(s), 0),
68
+ wildcards: segments.filter(hasWildcard).length,
69
+ regex: new RegExp(`^(${source})${dir ? '(?:/.*)?' : ''}$`),
70
+ segmentRegexes: segments.map((s) => (s === '**' ? null : new RegExp(`^${segmentSource(s, [])}$`))),
71
+ };
66
72
  }
67
-
68
73
  export const variantsOf = (slot) => braceVariants(slot.path).map((pattern) => compileVariant(slot, pattern));
69
-
70
74
  const levenshtein = (a, b) => {
71
- const row = Array.from({ length: b.length + 1 }, (_, i) => i);
72
- for (let i = 1; i <= a.length; i += 1) {
73
- let prev = row[0];
74
- row[0] = i;
75
- for (let j = 1; j <= b.length; j += 1) {
76
- const held = row[j];
77
- row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
78
- prev = held;
75
+ const row = Array.from({ length: b.length + 1 }, (_, i) => i);
76
+ for (let i = 1; i <= a.length; i += 1) {
77
+ let prev = row[0];
78
+ row[0] = i;
79
+ for (let j = 1; j <= b.length; j += 1) {
80
+ const held = row[j];
81
+ row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
82
+ prev = held;
83
+ }
79
84
  }
80
- }
81
- return row[b.length];
85
+ return row[b.length];
82
86
  };
83
-
84
87
  /** The match of `p` against one variant: its slot, root and bindings, or null (not matching, filtered out, or an app-kind slot of another kind). */
85
88
  function matchVariant(variant, p, only, appKind) {
86
- if (only && !only(variant.slot)) return null;
87
- const m = variant.regex.exec(p);
88
- if (!m) return null;
89
- const bindings = {};
90
- variant.names.forEach((name, i) => { bindings[name] = m[i + 2]; });
91
- if (variant.slot.appKind !== undefined && appKind.get(bindings.app) !== variant.slot.appKind) return null;
92
- const root = variant.dir ? m[1] : path.posix.dirname(m[1]);
93
- return { variant, slot: variant.slot, root: root === '.' ? '' : root, bindings };
89
+ if (only && !only(variant.slot))
90
+ return null;
91
+ const m = variant.regex.exec(p);
92
+ if (!m)
93
+ return null;
94
+ const bindings = {};
95
+ variant.names.forEach((name, i) => { bindings[name] = m[i + 2]; });
96
+ if (variant.slot.appKind !== undefined && appKind.get(bindings.app) !== variant.slot.appKind)
97
+ return null;
98
+ const root = variant.dir ? m[1] : path.posix.dirname(m[1]);
99
+ return { variant, slot: variant.slot, root: root === '.' ? '' : root, bindings };
94
100
  }
95
-
96
101
  /** Every variant matching `p`, with its root and bindings, minus app-kind slots of another kind, the most specific first. */
97
102
  export function matchVariants({ variants, appKind }, p, only) {
98
- const found = [];
99
- for (const variant of variants) {
100
- const hit = matchVariant(variant, p, only, appKind);
101
- if (hit) found.push(hit);
102
- }
103
- return found.sort((a, b) => b.variant.score - a.variant.score || a.variant.wildcards - b.variant.wildcards);
103
+ const found = [];
104
+ for (const variant of variants) {
105
+ const hit = matchVariant(variant, p, only, appKind);
106
+ if (hit)
107
+ found.push(hit);
108
+ }
109
+ return found.sort((a, b) => b.variant.score - a.variant.score || a.variant.wildcards - b.variant.wildcards);
104
110
  }
105
-
106
111
  /** The single most specific match of a sorted list: {hit, ambiguous}; two slots of equal specificity are ambiguous. */
107
112
  export function bestMatch(found) {
108
- if (!found.length) return { hit: null, ambiguous: [] };
109
- const top = found.filter((f) => f.variant.score === found[0].variant.score && f.variant.wildcards === found[0].variant.wildcards);
110
- const distinct = [...new Set(top.map((f) => f.slot.id))];
111
- return distinct.length > 1 ? { hit: null, ambiguous: distinct } : { hit: top[0], ambiguous: [] };
113
+ if (!found.length)
114
+ return { hit: null, ambiguous: [] };
115
+ const first = found[0];
116
+ const top = found.filter((f) => f.variant.score === first.variant.score && f.variant.wildcards === first.variant.wildcards);
117
+ const distinct = [...new Set(top.map((f) => f.slot.id))];
118
+ return distinct.length > 1 ? { hit: null, ambiguous: distinct } : { hit: top[0], ambiguous: [] };
112
119
  }
113
-
114
120
  /** How many leading segments of `parts` a variant accepts. */
115
121
  function acceptedDepth(variant, parts) {
116
- let depth = 0;
117
- while (depth < parts.length && depth < variant.segments.length) {
118
- const rx = variant.segmentRegexes[depth];
119
- if (rx === null || !rx.test(parts[depth])) break;
120
- depth += 1;
121
- }
122
- return depth;
122
+ let depth = 0;
123
+ while (depth < parts.length && depth < variant.segments.length) {
124
+ const rx = variant.segmentRegexes[depth];
125
+ if (rx === null || !rx.test(parts[depth]))
126
+ break;
127
+ depth += 1;
128
+ }
129
+ return depth;
123
130
  }
124
-
125
131
  /** Whether `cand` is nearer to the path than `winner`: deeper, then closer in shape, then in spelling, then more specific. */
126
132
  function nearerCandidate(cand, winner) {
127
- if (!winner) return true;
128
- if (cand.depth !== winner.depth) return cand.depth > winner.depth;
129
- if (cand.shape !== winner.shape) return cand.shape < winner.shape;
130
- if (cand.distance !== winner.distance) return cand.distance < winner.distance;
131
- return cand.variant.score > winner.variant.score;
133
+ if (!winner)
134
+ return true;
135
+ if (cand.depth !== winner.depth)
136
+ return cand.depth > winner.depth;
137
+ if (cand.shape !== winner.shape)
138
+ return cand.shape < winner.shape;
139
+ if (cand.distance !== winner.distance)
140
+ return cand.distance < winner.distance;
141
+ return cand.variant.score > winner.variant.score;
132
142
  }
133
-
134
143
  /** The leading segments of `p` a variant accepts, and how far it got; how the nearest slot of an unknown path is found. */
135
144
  export function nearestSlot(variants, p) {
136
- const parts = p.split('/');
137
- let winner = null;
138
- for (const variant of variants) {
139
- const depth = acceptedDepth(variant, parts);
140
- const expected = variant.segments[depth] ?? '';
141
- const distance = levenshtein(parts[depth] ?? '', expected.replace(VAR, ''));
142
- const cand = { variant, depth, shape: Math.abs(variant.segments.length - parts.length), distance };
143
- if (nearerCandidate(cand, winner)) winner = cand;
144
- }
145
- if (!winner) return null;
146
- return {
147
- slot: winner.variant.slot.id,
148
- pattern: winner.variant.pattern,
149
- matchedDepth: winner.depth,
150
- matchedPrefix: parts.slice(0, winner.depth).join('/'),
151
- expectedNext: winner.variant.segments[winner.depth] ?? null,
152
- };
145
+ const parts = p.split('/');
146
+ let winner = null;
147
+ for (const variant of variants) {
148
+ const depth = acceptedDepth(variant, parts);
149
+ const expected = variant.segments[depth] ?? '';
150
+ const distance = levenshtein(parts[depth] ?? '', expected.replace(VAR, ''));
151
+ const cand = { variant, depth, shape: Math.abs(variant.segments.length - parts.length), distance };
152
+ if (nearerCandidate(cand, winner))
153
+ winner = cand;
154
+ }
155
+ if (!winner)
156
+ return null;
157
+ return {
158
+ slot: winner.variant.slot.id,
159
+ pattern: winner.variant.pattern,
160
+ matchedDepth: winner.depth,
161
+ matchedPrefix: parts.slice(0, winner.depth).join('/'),
162
+ expectedNext: winner.variant.segments[winner.depth] ?? null,
163
+ };
153
164
  }
@@ -1,8 +1,6 @@
1
1
  // slot-path.mjs - normalize a slot-relative path for resolver comparisons.
2
2
  import { posixPath } from '../lib/path-key.mjs';
3
-
4
3
  const SLASH = '/';
5
4
  const END = '$';
6
5
  const TRAILING_SLASHES = new RegExp(`${SLASH}+${END}`);
7
-
8
6
  export const cleanSlotPath = (value) => posixPath(value).replace(TRAILING_SLASHES, '');