@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
@@ -24,13 +24,13 @@ import { skillRoot } from '../../engine/runtime-root.mjs';
24
24
  import { parseYamlCached } from '../lib/yaml-cached.mjs';
25
25
  import { braceVariants } from '../lib/glob.mjs';
26
26
  import { APP_KIND, RUNTIME_KIND, manifestKind } from './manifest-shape.mjs';
27
- import { APP_SCOPE, manifestShapeProblems, PROFILES } from './slot-manifest-shape.mjs';
27
+ import { APP_SCOPE, isSide, manifestShapeProblems, PROFILES } from './slot-manifest-shape.mjs';
28
28
  import { manifestSemanticProblems } from './slot-semantic-problems.mjs';
29
29
  import { classifyIn, ownerIn, slotEnabled, tierIn } from './slot-classify.mjs';
30
30
  import { compileVariant, varsOf, variantsOf } from './slot-match.mjs';
31
31
  import { importAllowedIn } from './slot-imports.mjs';
32
32
  import { requiredFilesIn, requiredPathsIn } from './slot-required.mjs';
33
- import { appResolver } from './slot-app-view.mjs';
33
+ import { appResolver, isAppRepo } from './slot-app-view.mjs';
34
34
  import { sideProblems } from './slot-side-problems.mjs';
35
35
  import { declarationShapeProblems } from './declaration-shape.mjs';
36
36
  import { declarationEdition, editionRuleParams, effectiveSlot, slotInEdition } from './edition-slots.mjs';
@@ -46,7 +46,6 @@ const HFS_MANIFEST_FILE = 'knowledge/hfs/slots.yaml';
46
46
  export const RUNTIME_MANIFEST_FILE = 'knowledge/hfs/runtime-slots.yaml';
47
47
  export const HFS_DECLARATION_FILE = 'hfs.json';
48
48
  export const SIDES = Object.freeze([...PROFILES]);
49
-
50
49
  /**
51
50
  * The rewriter of a side's finding messages: `(message) => message` with its paths made app-relative like the finding's own path.
52
51
  * Every check and rule of a side judges the side folder as its root, so the paths it names (`apps/web/src/...`, `src/modules/...`,
@@ -55,85 +54,88 @@ export const SIDES = Object.freeze([...PROFILES]);
55
54
  * `../x`) or a path already app-relative is left alone.
56
55
  */
57
56
  export function appRelativeMessages(side, sideRoot) {
58
- let entries = [];
59
- try { entries = fs.readdirSync(sideRoot).filter((name) => name !== 'node_modules' && !name.startsWith('.git')); } catch { /* no side folder: nothing to rewrite */ }
60
- if (!entries.length) return (message) => message;
61
- const escaped = entries.sort((a, b) => b.length - a.length).map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`));
62
- const token = new RegExp(`(^|[\\s'"\`(\\[{,;=<>])((?:${escaped.join('|')})(?=/|[\\s'"\`)\\]},;:!?<>]|\\.(?:\\s|$)|$))`, 'g');
63
- return (message) => (typeof message === 'string' && message
64
- ? message.replace(token, (whole, before, entry, offset) => (message.startsWith('/', offset + whole.length) || entry.includes('.') ? `${before}${side}/${entry}` : whole))
65
- : message);
57
+ let entries = [];
58
+ try {
59
+ entries = fs.readdirSync(sideRoot).filter((name) => name !== 'node_modules' && !name.startsWith('.git'));
60
+ }
61
+ catch { /* no side folder: nothing to rewrite */ }
62
+ if (!entries.length)
63
+ return (message) => message;
64
+ const escaped = entries.sort((a, b) => b.length - a.length).map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, String.raw `\$&`));
65
+ const token = new RegExp(`(^|[\\s'"\`(\\[{,;=<>])((?:${escaped.join('|')})(?=/|[\\s'"\`)\\]},;:!?<>]|\\.(?:\\s|$)|$))`, 'g');
66
+ return (message) => (typeof message === 'string' && message
67
+ ? message.replace(token, (whole, before, entry, offset) => (message.startsWith('/', offset + whole.length) || entry.includes('.') ? `${before}${side}/${entry}` : whole))
68
+ : message);
66
69
  }
67
-
68
70
  // ------------------------------------------------------------------------------------------- manifest
69
-
70
71
  /**
71
72
  * The parsed and validated manifest. `text` (or `file`, or `root`) selects the source; the default is the runtime's own
72
73
  * knowledge/hfs/slots.yaml. A manifest that fails its shape or a semantic rule is refused whole (HFS_MANIFEST_INVALID).
73
74
  */
74
75
  export function loadSlotManifest({ root = skillRoot, file = path.join(root, HFS_MANIFEST_FILE), text } = {}) {
75
- let doc;
76
- try { doc = parseYamlCached(text ?? fs.readFileSync(file, 'utf8')); } catch (error) { fail('HFS_MANIFEST_INVALID', `the slot manifest cannot be read (${String(error?.message ?? error).split('\n')[0]})`, { file }); }
77
- const problems = manifestShapeProblems(doc);
78
- if (!problems.length) problems.push(...manifestSemanticProblems(doc, { varsOf, braceVariants, compileVariant }));
79
- if (problems.length) fail('HFS_MANIFEST_INVALID', `the slot manifest breaks its schema: ${problems.slice(0, 5).join('; ')}${problems.length > 5 ? '; and ' + (problems.length - 5) + ' more' : ''}`, { file, problems });
80
- const [major, minor, patch] = doc.version.split('.').map(Number);
81
- return Object.freeze({ ...doc, major, minor, patch });
76
+ let doc;
77
+ try {
78
+ doc = parseYamlCached(text ?? fs.readFileSync(file, 'utf8'));
79
+ }
80
+ catch (error) {
81
+ fail('HFS_MANIFEST_INVALID', `the slot manifest cannot be read (${String(error?.message ?? error).split('\n')[0]})`, { file });
82
+ }
83
+ const problems = manifestShapeProblems(doc);
84
+ if (!problems.length)
85
+ problems.push(...manifestSemanticProblems(doc, { varsOf, braceVariants, compileVariant }));
86
+ if (problems.length)
87
+ fail('HFS_MANIFEST_INVALID', `the slot manifest breaks its schema: ${problems.slice(0, 5).join('; ')}${problems.length > 5 ? '; and ' + (problems.length - 5) + ' more' : ''}`, { file, problems });
88
+ const [major, minor, patch] = doc.version.split('.').map(Number);
89
+ return Object.freeze({ ...doc, major, minor, patch });
82
90
  }
83
-
84
91
  // ------------------------------------------------------------------------------------- declaration
85
-
86
92
  const declarationInvalid = (problems, file) => fail('HFS_DECLARATION_INVALID', `hfs.json is refused: ${problems.slice(0, 5).join('; ')}${problems.length > 5 ? '; and ' + (problems.length - 5) + ' more' : ''}`, { file, problems });
87
-
88
93
  function runtimeDeclaration(manifest, declaration, file, side) {
89
- if (side !== null) fail('HFS_DECLARATION_INVALID', 'a runtime repository has no sides', { file, side });
90
- return Object.freeze({
91
- hfs: declaration.hfs,
92
- kind: RUNTIME_KIND,
93
- project: declaration.project,
94
- side: null,
95
- profile: RUNTIME_KIND,
96
- apps: Object.freeze([]),
97
- optionalSlots: Object.freeze([]),
98
- connections: Object.freeze([]),
99
- reads: Object.freeze([]),
100
- manifestVersion: manifest.version,
101
- });
94
+ if (side !== null)
95
+ fail('HFS_DECLARATION_INVALID', 'a runtime repository has no sides', { file, side });
96
+ return Object.freeze({
97
+ hfs: declaration.hfs,
98
+ kind: RUNTIME_KIND,
99
+ project: declaration.project,
100
+ side: null,
101
+ profile: RUNTIME_KIND,
102
+ apps: Object.freeze([]),
103
+ optionalSlots: Object.freeze([]),
104
+ connections: Object.freeze([]),
105
+ reads: Object.freeze([]),
106
+ manifestVersion: manifest.version,
107
+ });
102
108
  }
103
-
104
109
  const connectionView = (c) => Object.freeze({ name: c.name, envPrefix: c.envPrefix, owner: c.owner, isolation: c.isolation, ...(c.provider !== undefined ? { provider: c.provider } : {}) });
105
-
106
110
  /** The declaration of one side of an app, frozen: the view a check of that side folder runs under. */
107
111
  function sideDeclaration(manifest, declaration, name, edition, providers) {
108
- const s = declaration.sides[name];
109
- return Object.freeze({
110
- hfs: declaration.hfs,
111
- kind: APP_KIND,
112
- project: declaration.project,
113
- edition,
114
- side: name,
115
- profile: name,
116
- apps: Object.freeze(s.apps.map((a) => Object.freeze({ name: a.name, kind: a.kind }))),
117
- optionalSlots: Object.freeze([...(s.optionalSlots ?? [])]),
118
- patterns: Object.freeze([...(s.patterns ?? [])]), kinds: Object.freeze([...(s.kinds ?? [])]),
119
- backing: Object.freeze([...(s.backing ?? [])]),
120
- connections: Object.freeze((s.connections ?? []).map(connectionView)),
121
- providers,
122
- reads: Object.freeze([...(s.reads ?? [])]),
123
- dbFunctions: Object.freeze((s.dbFunctions ?? []).map((entry) => Object.freeze({ ...entry }))),
124
- ...(s.grammar === undefined ? {} : { grammar: s.grammar }),
125
- ...(s.extraPackages === undefined ? {} : { extraPackages: Object.freeze(s.extraPackages.map(entry => Object.freeze({ ...entry }))) }),
126
- manifestVersion: manifest.version,
127
- });
112
+ const s = declaration.sides[name];
113
+ return Object.freeze({
114
+ hfs: declaration.hfs,
115
+ kind: APP_KIND,
116
+ project: declaration.project,
117
+ edition,
118
+ side: name,
119
+ profile: name,
120
+ apps: Object.freeze(s.apps.map((a) => Object.freeze({ name: a.name, kind: a.kind }))),
121
+ optionalSlots: Object.freeze([...(s.optionalSlots ?? [])]),
122
+ patterns: Object.freeze([...(s.patterns ?? [])]), kinds: Object.freeze([...(s.kinds ?? [])]),
123
+ backing: Object.freeze([...(s.backing ?? [])]),
124
+ connections: Object.freeze((s.connections ?? []).map(connectionView)),
125
+ providers,
126
+ reads: Object.freeze([...(s.reads ?? [])]),
127
+ dbFunctions: Object.freeze((s.dbFunctions ?? []).map((entry) => Object.freeze({ ...entry }))),
128
+ ...(s.grammar === undefined ? {} : { grammar: s.grammar }),
129
+ ...(s.extraPackages === undefined ? {} : { extraPackages: Object.freeze(s.extraPackages.map((entry) => Object.freeze({ ...entry }))) }),
130
+ manifestVersion: manifest.version,
131
+ });
128
132
  }
129
-
130
133
  /** R334: an app's bare database function name must be a pg built-in the canon lists bare (ruleParams.be.dbFunctions); any other is schema.name. */
131
134
  function bareDbFunctionProblems(manifest, be) {
132
- const builtins = new Set((manifest.ruleParams?.be?.dbFunctions ?? []).map((entry) => entry.name).filter((name) => !name.includes('.')));
133
- return (be?.dbFunctions ?? []).filter((entry) => !entry.name.includes('.') && !builtins.has(entry.name))
134
- .map((entry) => `sides.be.dbFunctions names ${entry.name} bare, but a bare name is only a pg built-in of the canon's list (ruleParams.be.dbFunctions); name a function of a migration schema-qualified (public.${entry.name})`);
135
+ const builtins = new Set((manifest.ruleParams?.be?.dbFunctions ?? []).map((entry) => entry.name).filter((name) => !name.includes('.')));
136
+ return (be?.dbFunctions ?? []).filter((entry) => !entry.name.includes('.') && !builtins.has(entry.name))
137
+ .map((entry) => `sides.be.dbFunctions names ${entry.name} bare, but a bare name is only a pg built-in of the canon's list (ruleParams.be.dbFunctions); name a function of a migration schema-qualified (public.${entry.name})`);
135
138
  }
136
-
137
139
  /**
138
140
  * A declaration checked against the manifest: kind app, the pinned major the manifest's (HFS_MANIFEST_MAJOR_MISMATCH otherwise,
139
141
  * and there is no compatibility window), and per side: app kinds of that profile, optionalSlots naming only opt-in slots of the
@@ -141,100 +143,119 @@ function bareDbFunctionProblems(manifest, be) {
141
143
  * its two sides under `sides`), or with `side` that side's view: the declaration a check of the side folder runs under.
142
144
  */
143
145
  export function resolveRepoDeclaration(manifest, declaration, { file = HFS_DECLARATION_FILE, side = null } = {}) {
144
- const problems = declarationShapeProblems(declaration, PROFILES);
145
- if (problems.length) declarationInvalid(problems, file);
146
- if (declaration.kind !== manifestKind(manifest)) declarationInvalid([`hfs.json is of kind ${declaration.kind}, but the manifest it is judged by is of kind ${manifestKind(manifest)}`], file);
147
- if (declaration.hfs !== manifest.major)
148
- fail('HFS_MANIFEST_MAJOR_MISMATCH', `hfs.json pins manifest major ${declaration.hfs} but the manifest is ${manifest.version}`, { pinned: declaration.hfs, manifest: manifest.version, manifestMajor: manifest.major, file });
149
- if (declaration.kind === RUNTIME_KIND) return runtimeDeclaration(manifest, declaration, file, side);
150
- const { edition, known, valid } = declarationEdition(manifest, declaration);
151
- if (!valid) fail('HFS_EDITION_INVALID', `hfs.json edition is ${JSON.stringify(declaration.edition)}; the editions this manifest knows are ${known.join(', ')} (absent means full)`, { file, edition: declaration.edition });
152
- const bad = [...PROFILES.flatMap((name) => sideProblems(manifest, name, declaration.sides[name], edition)), ...bareDbFunctionProblems(manifest, declaration.sides.be)];
153
- if (bad.length) declarationInvalid(bad, file);
154
- if (side !== null && !PROFILES.includes(side)) fail('HFS_DECLARATION_INVALID', `${side} is not a side of an app (be, fe)`, { file, side });
155
- // A provider declared on any connection enables the provider slots of every profile (a fe side declares none): each view carries the union.
156
- const providers = Object.freeze([...new Set(PROFILES.flatMap((name) => (declaration.sides[name].connections ?? []).map((c) => c.provider).filter((p) => p !== undefined)))]);
157
- const sides = Object.fromEntries(PROFILES.map((name) => [name, sideDeclaration(manifest, declaration, name, edition, providers)]));
158
- if (side !== null) return sides[side];
159
- return Object.freeze({
160
- hfs: declaration.hfs,
161
- kind: APP_KIND,
162
- project: declaration.project,
163
- edition,
164
- side: null,
165
- profile: APP_SCOPE,
166
- // The root declares no app or connection of its own (each side does); its opt-in slots are the browser journey and the managed
167
- // tsconfig.json that type-checks it, both switched on by `browser: true`.
168
- apps: Object.freeze([]),
169
- optionalSlots: Object.freeze(declaration.browser === true ? ['app.browser', 'app.browser-config'] : []),
170
- connections: Object.freeze([]),
171
- providers,
172
- reads: Object.freeze([]),
173
- sides: Object.freeze(sides),
174
- manifestVersion: manifest.version,
175
- });
146
+ const problems = declarationShapeProblems(declaration, PROFILES);
147
+ if (problems.length)
148
+ declarationInvalid(problems, file);
149
+ if (declaration.kind !== manifestKind(manifest))
150
+ declarationInvalid([`hfs.json is of kind ${declaration.kind}, but the manifest it is judged by is of kind ${manifestKind(manifest)}`], file);
151
+ if (declaration.hfs !== manifest.major)
152
+ fail('HFS_MANIFEST_MAJOR_MISMATCH', `hfs.json pins manifest major ${declaration.hfs} but the manifest is ${manifest.version}`, { pinned: declaration.hfs, manifest: manifest.version, manifestMajor: manifest.major, file });
153
+ if (declaration.kind === RUNTIME_KIND)
154
+ return runtimeDeclaration(manifest, declaration, file, side);
155
+ const { edition, known, valid } = declarationEdition(manifest, declaration);
156
+ if (!valid)
157
+ fail('HFS_EDITION_INVALID', `hfs.json edition is ${JSON.stringify(declaration.edition)}; the editions this manifest knows are ${known.join(', ')} (absent means full)`, { file, edition: declaration.edition });
158
+ const bad = [...PROFILES.flatMap((name) => sideProblems(manifest, name, declaration.sides[name], edition)), ...bareDbFunctionProblems(manifest, declaration.sides.be)];
159
+ if (bad.length)
160
+ declarationInvalid(bad, file);
161
+ if (side !== null && !isSide(side))
162
+ fail('HFS_DECLARATION_INVALID', `${side} is not a side of an app (be, fe)`, { file, side });
163
+ // A provider declared on any connection enables the provider slots of every profile (a fe side declares none): each view carries the union.
164
+ const providers = Object.freeze([...new Set(PROFILES.flatMap((name) => (declaration.sides[name].connections ?? []).map((c) => c.provider).filter((p) => p !== undefined)))]);
165
+ const sides = Object.fromEntries(PROFILES.map((name) => [name, sideDeclaration(manifest, declaration, name, edition, providers)]));
166
+ if (side !== null)
167
+ return sides[side];
168
+ return Object.freeze({
169
+ hfs: declaration.hfs,
170
+ kind: APP_KIND,
171
+ project: declaration.project,
172
+ edition,
173
+ side: null,
174
+ profile: APP_SCOPE,
175
+ // The root declares no app or connection of its own (each side does); its opt-in slots are the browser journey and the managed
176
+ // tsconfig.json that type-checks it, both switched on by `browser: true`.
177
+ apps: Object.freeze([]),
178
+ optionalSlots: Object.freeze(declaration.browser === true ? ['app.browser', 'app.browser-config'] : []),
179
+ connections: Object.freeze([]),
180
+ providers,
181
+ reads: Object.freeze([]),
182
+ sides: Object.freeze(sides),
183
+ manifestVersion: manifest.version,
184
+ });
185
+ }
186
+ /**
187
+ * The view of a whole app (profile app, both sides) that resolveRepoDeclaration builds for an app declaration; a declaration that
188
+ * resolves to a view without sides (a runtime repository) is refused HFS_DECLARATION_INVALID.
189
+ */
190
+ export function resolveAppDeclaration(manifest, declaration, { file = HFS_DECLARATION_FILE } = {}) {
191
+ const view = resolveRepoDeclaration(manifest, declaration, { file });
192
+ if (!isAppRepo(view))
193
+ declarationInvalid([`a ${view.kind} declaration has no sides`], file);
194
+ return view;
176
195
  }
177
-
178
196
  /**
179
197
  * Where the declaration of `dir` lives: `dir` itself when it holds hfs.json (the app root), else its parent when `dir` is a side
180
198
  * folder (be/ or fe/) of an app (the side view). `{ file, appRoot, side }`; side is null at the app root.
181
199
  */
182
200
  export function locateDeclaration(dir) {
183
- const own = path.join(dir, HFS_DECLARATION_FILE);
184
- if (fs.existsSync(own)) return { file: own, appRoot: dir, side: null };
185
- const parent = path.dirname(dir);
186
- const side = path.basename(dir);
187
- const up = path.join(parent, HFS_DECLARATION_FILE);
188
- if (PROFILES.includes(side) && fs.existsSync(up)) return { file: up, appRoot: parent, side };
189
- return { file: own, appRoot: dir, side: null };
201
+ const own = path.join(dir, HFS_DECLARATION_FILE);
202
+ if (fs.existsSync(own))
203
+ return { file: own, appRoot: dir, side: null };
204
+ const parent = path.dirname(dir);
205
+ const side = path.basename(dir);
206
+ const up = path.join(parent, HFS_DECLARATION_FILE);
207
+ if (isSide(side) && fs.existsSync(up))
208
+ return { file: up, appRoot: parent, side };
209
+ return { file: own, appRoot: dir, side: null };
190
210
  }
191
-
192
211
  /**
193
212
  * hfs.json of the app at `repoRoot` (the app itself), or of the app whose side folder `repoRoot` is (that side's view), parsed and
194
213
  * resolved; a missing or unreadable file is a refusal, never "unavailable".
195
214
  */
196
215
  export function readRepoDeclaration(manifest, repoRoot) {
197
- const { file, side } = locateDeclaration(repoRoot);
198
- let declaration;
199
- try { declaration = JSON.parse(fs.readFileSync(file, 'utf8')); } catch (error) { declarationInvalid([`hfs.json cannot be read (${String(error?.code ?? error?.message ?? error).split('\n')[0]})`], file); }
200
- return resolveRepoDeclaration(manifest, declaration, { file, side });
216
+ const { file, side } = locateDeclaration(repoRoot);
217
+ let declaration;
218
+ try {
219
+ declaration = JSON.parse(fs.readFileSync(file, 'utf8'));
220
+ }
221
+ catch (error) {
222
+ declarationInvalid([`hfs.json cannot be read (${String(error?.code ?? error?.message ?? error).split('\n')[0]})`], file);
223
+ }
224
+ return resolveRepoDeclaration(manifest, declaration, { file, side });
201
225
  }
202
-
203
226
  // ------------------------------------------------------------------------------------------- resolver
204
-
205
227
  /**
206
228
  * The four questions for one scope: the app root (profile app, root paths) or one side (profile be or fe, paths relative to the
207
229
  * side folder). createSlotResolver composes them; nothing else calls this.
208
230
  */
209
231
  function createScopeResolver(manifest, repo) {
210
- const profile = repo.profile;
211
- const edition = repo.edition ?? 'full';
212
- const slots = manifest.slots.filter((s) => s.profiles.includes(profile) && slotInEdition(manifest, s, edition)).map((s) => effectiveSlot(s, profile, edition));
213
- const scope = { manifest, repo, profile, slots, byId: new Map(slots.map((s) => [s.id, s])), variants: slots.flatMap(variantsOf), appKind: new Map(repo.apps.map((a) => [a.name, a.kind])) };
214
- const classifyPath = (input) => classifyIn(scope, input);
215
- /** tracked | ignored | external for the slot owning `p`, or null when no slot owns it. */
216
- const trackingOf = (p) => classifyPath(p).tracking ?? null;
217
- return Object.freeze({
218
- repo,
219
- slot: (id) => scope.byId.get(id) ?? null,
220
- slots: () => slots,
221
- slotEnabled: (slot) => slotEnabled(scope, slot),
222
- classifyPath,
223
- slotOf: (p) => { const c = classifyPath(p); return c.slot ? scope.byId.get(c.slot) : null; },
224
- ownerOf: (input) => ownerIn(scope, input),
225
- tierOf: (input) => tierIn(scope, input),
226
- importAllowed: (fromPath, toPath) => importAllowedIn(scope, fromPath, toPath),
227
- requiredFiles: (input) => requiredFilesIn(scope, input),
228
- requiredPaths: () => requiredPathsIn(scope),
229
- trackingOf,
230
- /** True only for a path a slot owns and requires to be committed. */
231
- isTracked: (p) => trackingOf(p) === 'tracked',
232
- // The app's own database functions (hfs.json sides.be.dbFunctions) join the canon's list (R334).
233
- ruleParams: () => withDeclaredDbFunctions(ruleParams(manifest, profile, edition), repo.dbFunctions),
234
- allowedImports: (tier) => manifest.tiers[profile]?.[tier]?.mayImport ?? null,
235
- });
232
+ const profile = repo.profile;
233
+ const edition = repo.edition ?? 'full';
234
+ const slots = manifest.slots.filter((s) => s.profiles.includes(profile) && slotInEdition(manifest, s, edition)).map((s) => effectiveSlot(s, profile, edition));
235
+ const scope = { manifest, repo, profile, slots, byId: new Map(slots.map((s) => [s.id, s])), variants: slots.flatMap(variantsOf), appKind: new Map(repo.apps.map((a) => [a.name, a.kind])) };
236
+ const classifyPath = (input) => classifyIn(scope, input);
237
+ /** tracked | ignored | external for the slot owning `p`, or null when no slot owns it. */
238
+ const trackingOf = (p) => classifyPath(p).tracking ?? null;
239
+ return Object.freeze({
240
+ repo,
241
+ slot: (id) => scope.byId.get(id) ?? null,
242
+ slots: () => slots,
243
+ slotEnabled: (slot) => slotEnabled(scope, slot),
244
+ classifyPath,
245
+ slotOf: (p) => { const c = classifyPath(p); return c.slot ? scope.byId.get(c.slot) : null; },
246
+ ownerOf: (input) => ownerIn(scope, input),
247
+ tierOf: (input) => tierIn(scope, input),
248
+ importAllowed: (fromPath, toPath) => importAllowedIn(scope, fromPath, toPath),
249
+ requiredFiles: (input) => requiredFilesIn(scope, input),
250
+ requiredPaths: () => requiredPathsIn(scope),
251
+ trackingOf,
252
+ /** True only for a path a slot owns and requires to be committed. */
253
+ isTracked: (p) => trackingOf(p) === 'tracked',
254
+ // The app's own database functions (hfs.json sides.be.dbFunctions) join the canon's list (R334).
255
+ ruleParams: () => withDeclaredDbFunctions(ruleParams(manifest, profile, edition), repo.dbFunctions),
256
+ allowedImports: (tier) => manifest.tiers[profile]?.[tier]?.mayImport ?? null,
257
+ });
236
258
  }
237
-
238
259
  /**
239
260
  * The four questions for one app, or for one side of it. `repo` comes from resolveRepoDeclaration / readRepoDeclaration.
240
261
  * Paths are relative to the root the declaration was resolved for (backslashes and a leading ./ are folded; a trailing / or a
@@ -243,30 +264,32 @@ function createScopeResolver(manifest, repo) {
243
264
  * the side prefixed back onto every path and root it returns (and `side` added). Only the declared `reads` cross sides.
244
265
  */
245
266
  export function createSlotResolver(manifest, repo) {
246
- if (repo.profile !== APP_SCOPE) return createScopeResolver(manifest, repo);
247
- // The root declares no connection of its own, yet a provider slot of the app profile is enabled by a
248
- // side's connection with that provider: the root scope reads the union of the sides' connections.
249
- const root = createScopeResolver(manifest, { ...repo, connections: SIDES.flatMap((side) => repo.sides[side].connections) });
250
- const sides = Object.fromEntries(PROFILES.map((side) => [side, createScopeResolver(manifest, repo.sides[side])]));
251
- return appResolver(repo, root, sides);
267
+ if (repo.profile !== APP_SCOPE)
268
+ return createScopeResolver(manifest, repo);
269
+ // The root declares no connection of its own, yet a provider slot of the app profile is enabled by a
270
+ // side's connection with that provider: the root scope reads the union of the sides' connections.
271
+ // reason: the profile is app, so the declaration resolveRepoDeclaration built carries both sides
272
+ const app = repo;
273
+ const root = createScopeResolver(manifest, { ...repo, connections: SIDES.flatMap((side) => app.sides[side].connections) });
274
+ const sides = Object.fromEntries(PROFILES.map((side) => [side, createScopeResolver(manifest, app.sides[side])]));
275
+ return appResolver(app, root, sides);
252
276
  }
253
-
254
277
  /** The rule parameters with the app's declared database functions appended to `dbFunctions` (be only), frozen. */
255
278
  const withDeclaredDbFunctions = (params, declared = []) => (params.dbFunctions === undefined && !declared.length ? params
256
- : Object.freeze({ ...params, dbFunctions: Object.freeze([...(params.dbFunctions ?? []), ...declared]) }));
257
-
279
+ : Object.freeze({ ...params, dbFunctions: Object.freeze([...(params.dbFunctions ?? []), ...declared]) }));
258
280
  /** The rule parameters of one profile (be: infraOwners, suffixes, bannedSuffixes and the rest over the shared ruleParams.common fileLines and duplicateBlock; fe: common alone or with its overrides; runtime: ruleParams.runtime of a runtime manifest), as a frozen deep copy. Under `edition` lite the `lite` overrides of ruleParams.<profile> merge over the base (the `lite` key itself is never returned). */
259
281
  export function ruleParams(manifest, profile, edition = 'full') {
260
- const profiles = manifestKind(manifest) === RUNTIME_KIND ? [RUNTIME_KIND] : PROFILES;
261
- if (!profiles.includes(profile)) fail('HFS_MANIFEST_INVALID', `ruleParams has no profile ${profile}`, { profile });
262
- return editionRuleParams({ ...manifest.ruleParams.common, ...manifest.ruleParams[profile] }, edition);
282
+ const profiles = manifestKind(manifest) === RUNTIME_KIND ? [RUNTIME_KIND] : PROFILES;
283
+ if (!profiles.includes(profile))
284
+ fail('HFS_MANIFEST_INVALID', `ruleParams has no profile ${profile}`, { profile });
285
+ return editionRuleParams({ ...manifest.ruleParams.common, ...manifest.ruleParams[profile] }, edition);
263
286
  }
264
-
265
287
  /**
266
288
  * The manifest of this runtime plus the resolver for the app or side folder at `repoRoot` (a side folder gets that side's view), or
267
289
  * for an already-parsed declaration (the app, or with `side` that side's view).
268
290
  */
269
291
  export function openHfs({ root = skillRoot, repoRoot, declaration, side = null, manifest = loadSlotManifest({ root }) } = {}) {
270
- const repo = declaration !== undefined ? resolveRepoDeclaration(manifest, declaration, { side }) : readRepoDeclaration(manifest, repoRoot);
271
- return { manifest, repo, ...createSlotResolver(manifest, repo), rules: () => loadRuleCatalog({ root, manifest }) };
292
+ const repo = declaration !== undefined ? resolveRepoDeclaration(manifest, declaration, { side }) : readRepoDeclaration(manifest, repoRoot);
293
+ const resolver = createSlotResolver(manifest, repo);
294
+ return { manifest, ...resolver, rules: () => loadRuleCatalog({ root, manifest }) };
272
295
  }
@@ -1,98 +1,95 @@
1
1
  import { AsyncLocalStorage } from 'node:async_hooks';
2
2
  import path from 'node:path';
3
3
  import { byCodeUnit } from '../lib/list.mjs';
4
-
5
- // One TypeScript program per (compiler, root names, compiler options, project references) inside a program run, and
6
- // one value per (kind, compiler, input) for what callers derive from those programs (the architecture context).
7
- // A caller (canon-scan's architecture machine, a script checker) opens one run around its work and releases it after,
8
- // so nothing built here outlives the run that built it. Outside a run every call
9
- // builds afresh.
10
4
  const runs = new AsyncLocalStorage();
11
-
12
5
  /** Share compiler inputs only until this check finishes; nested callers join the same run. */
13
6
  export function withTypeScriptProgramRun(run) {
14
- return runs.getStore() ? run() : runs.run(new Map(), run);
7
+ return runs.getStore() ? run() : runs.run(new Map(), run);
15
8
  }
16
-
17
-
18
9
  function keyOf(value) {
19
- if (value === undefined) return 'u';
20
- if (value === null || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return JSON.stringify(value);
21
- if (Array.isArray(value)) {
22
- const items = value.map(keyOf);
23
- return items.includes(null) ? null : `[${items.join(',')}]`;
24
- }
25
- if (typeof value !== 'object' || (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)) return null;
26
- const entries = [];
27
- for (const name of Object.keys(value).sort(byCodeUnit)) {
28
- const item = keyOf(value[name]);
29
- if (item === null) return null;
30
- entries.push(`${JSON.stringify(name)}:${item}`);
31
- }
32
- return `{${entries.join(',')}}`;
10
+ if (value === undefined)
11
+ return 'u';
12
+ if (value === null || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean')
13
+ return JSON.stringify(value);
14
+ if (Array.isArray(value)) {
15
+ const items = value.map(keyOf);
16
+ return items.includes(null) ? null : `[${items.join(',')}]`;
17
+ }
18
+ if (typeof value !== 'object' || (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null))
19
+ return null;
20
+ const entries = [];
21
+ for (const name of Object.keys(value).sort(byCodeUnit)) {
22
+ const item = keyOf(value[name]);
23
+ if (item === null)
24
+ return null;
25
+ entries.push(`${JSON.stringify(name)}:${item}`);
26
+ }
27
+ return `{${entries.join(',')}}`;
33
28
  }
34
-
35
29
  /**
36
30
  * build(), shared inside a program run by every caller that asks for the same kind, compiler and input. An input that
37
31
  * is not plain data (a function, a class instance) is never shared.
38
32
  */
39
33
  export function sharedInProgramRun(kind, ts, input, build) {
40
- const values = runs.getStore();
41
- const key = values ? keyOf(input) : null;
42
- if (key === null) return build();
43
- let byKey = values.get(ts);
44
- if (!byKey) {
45
- byKey = new Map();
46
- values.set(ts, byKey);
47
- }
48
- const id = `${kind}\0${key}`;
49
- if (!byKey.has(id)) byKey.set(id, build());
50
- return byKey.get(id);
34
+ const values = runs.getStore();
35
+ const key = values ? keyOf(input) : null;
36
+ if (key === null || !values)
37
+ return build();
38
+ let byKey = values.get(ts);
39
+ if (!byKey) {
40
+ byKey = new Map();
41
+ values.set(ts, byKey);
42
+ }
43
+ const id = `${kind}\0${key}`;
44
+ if (!byKey.has(id))
45
+ byKey.set(id, build());
46
+ return byKey.get(id);
51
47
  }
52
-
53
48
  /** ts.createProgram, shared inside a program run. */
54
49
  export function createTypeScriptProgram(ts, { rootNames, options, projectReferences }) {
55
- return sharedInProgramRun('program', ts, { rootNames, options, projectReferences },
56
- () => ts.createProgram({ rootNames, options, projectReferences, host: sharedSourceHost(ts, options) }));
50
+ return sharedInProgramRun('program', ts, { rootNames, options, projectReferences }, () => ts.createProgram({ rootNames, options, projectReferences, host: sharedSourceHost(ts, options) })); // exactOptionalPropertyTypes: the optional host and references are passed through as undefined, as before
57
51
  }
58
-
59
52
  function sharedSourceHost(ts, options) {
60
- if (!runs.getStore()) return undefined;
61
- const registry = sharedInProgramRun('documents', ts, null,
62
- () => ts.createDocumentRegistry(ts.sys.useCaseSensitiveFileNames, ts.sys.getCurrentDirectory()));
63
- const host = ts.createCompilerHost(options);
64
- const readSource = host.getSourceFile;
65
- // TypeScript's registry separates parse-affecting settings and Node module formats. Sharing syntax trees leaves
66
- // module resolution and symbol/type caches with each program, including projects with different path aliases.
67
- host.getSourceFile = (file, sourceOptions, onError, fresh) => {
68
- if (fresh) return readSource(file, sourceOptions, onError, fresh);
69
- let text;
70
- try { text = host.readFile(file); } catch (error) {
71
- onError?.(String(error.message ?? error));
72
- text = '';
73
- }
74
- if (text === undefined) return undefined;
75
- return registry.acquireDocument(file, options, ts.ScriptSnapshot.fromString(text), '0', undefined, sourceOptions);
76
- };
77
- return host;
53
+ if (!runs.getStore())
54
+ return undefined;
55
+ const registry = sharedInProgramRun('documents', ts, null, () => ts.createDocumentRegistry(ts.sys.useCaseSensitiveFileNames, ts.sys.getCurrentDirectory()));
56
+ const host = ts.createCompilerHost(options);
57
+ const readSource = host.getSourceFile;
58
+ // TypeScript's registry separates parse-affecting settings and Node module formats. Sharing syntax trees leaves
59
+ // module resolution and symbol/type caches with each program, including projects with different path aliases.
60
+ host.getSourceFile = (file, sourceOptions, onError, fresh) => {
61
+ if (fresh)
62
+ return readSource(file, sourceOptions, onError, fresh);
63
+ let text;
64
+ try {
65
+ text = host.readFile(file);
66
+ }
67
+ catch (error) {
68
+ onError?.(String(error.message ?? error));
69
+ text = '';
70
+ }
71
+ if (text === undefined)
72
+ return undefined;
73
+ return registry.acquireDocument(file, options, ts.ScriptSnapshot.fromString(text), '0', undefined, sourceOptions);
74
+ };
75
+ return host;
78
76
  }
79
-
80
77
  /** Read and expand a tsconfig with the caller's TypeScript compiler and optional tracked file reader. */
81
78
  export function readTypeScriptProject(ts, configFile, readFile = ts.sys.readFile) {
82
- const read = ts.readConfigFile(configFile, readFile);
83
- if (read.error) return { read, parsed: null };
84
- const host = readFile === ts.sys.readFile ? ts.sys : { ...ts.sys, readFile };
85
- return { read, parsed: ts.parseJsonConfigFileContent(read.config, host, path.dirname(configFile), undefined, configFile) };
79
+ const read = ts.readConfigFile(configFile, readFile);
80
+ if (read.error)
81
+ return { read: read, parsed: null };
82
+ const host = readFile === ts.sys.readFile ? ts.sys : { ...ts.sys, readFile };
83
+ return { read: read, parsed: ts.parseJsonConfigFileContent(read.config, host, path.dirname(configFile), undefined, configFile) };
86
84
  }
87
-
88
85
  /** Resolve a project reference with the compiler's API, including older compiler fallback. */
89
86
  export function typeScriptProjectReferencePath(ts, reference) {
90
- if (typeof ts.resolveProjectReferencePath === 'function') return ts.resolveProjectReferencePath(reference);
91
- return path.extname(reference.path) ? reference.path : path.join(reference.path, 'tsconfig.json');
87
+ if (typeof ts.resolveProjectReferencePath === 'function')
88
+ return ts.resolveProjectReferencePath(reference);
89
+ return path.extname(reference.path) ? reference.path : path.join(reference.path, 'tsconfig.json');
92
90
  }
93
-
94
91
  /** Resolve an import using the selected project's compiler options. */
95
92
  export function resolveTypeScriptModule(ts, specifier, from, options, host, cache) {
96
- const resolverHost = host === undefined ? ts.sys : host;
97
- return ts.resolveModuleName(specifier, from, options, resolverHost, cache).resolvedModule?.resolvedFileName;
93
+ const resolverHost = host === undefined ? ts.sys : host;
94
+ return ts.resolveModuleName(specifier, from, options, resolverHost, cache).resolvedModule?.resolvedFileName;
98
95
  }