@starci/eslint-canon-fe 9.5.23 → 9.6.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 (164) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/docs/button-intent.md +1 -1
  3. package/package.json +1 -1
  4. package/runtime/bundle.json +192 -0
  5. package/runtime/engine/by-code-unit.mjs +6 -6
  6. package/runtime/engine/model-config.mjs +98 -76
  7. package/runtime/engine/orca-config.mjs +22 -17
  8. package/runtime/engine/release-config.mjs +22 -18
  9. package/runtime/engine/removed-vocabulary.mjs +21 -20
  10. package/runtime/engine/resources-config.mjs +42 -35
  11. package/runtime/engine/runtime-root.mjs +11 -13
  12. package/runtime/engine/secrets.mjs +170 -102
  13. package/runtime/engine/sonar-config.mjs +16 -13
  14. package/runtime/engine/temp-root.mjs +3 -9
  15. package/runtime/knowledge/hfs/canon-pins.yaml +10 -10
  16. package/runtime/knowledge/hfs/slots.yaml +2 -2
  17. package/runtime/knowledge/sonar-gate.yaml +1 -1
  18. package/runtime/packages/hfs/templates/app/package-scripts/package.json +1 -1
  19. package/runtime/scripts/api/git/cat-file.mjs +0 -1
  20. package/runtime/scripts/api/git/config-get.mjs +1 -2
  21. package/runtime/scripts/api/git/diff.mjs +0 -1
  22. package/runtime/scripts/api/git/lib.mjs +32 -32
  23. package/runtime/scripts/api/git/ls-files.mjs +0 -1
  24. package/runtime/scripts/api/git/ls-tree.mjs +0 -1
  25. package/runtime/scripts/api/git/merge-base.mjs +2 -3
  26. package/runtime/scripts/api/git/remote.mjs +0 -1
  27. package/runtime/scripts/api/git/rev-parse-query.mjs +0 -1
  28. package/runtime/scripts/api/git/show.mjs +0 -1
  29. package/runtime/scripts/hfs/allows.mjs +37 -38
  30. package/runtime/scripts/hfs/architecture/ast-walks.mjs +180 -163
  31. package/runtime/scripts/hfs/architecture/automatic-gates.mjs +80 -73
  32. package/runtime/scripts/hfs/architecture/backend-app-files.mjs +88 -77
  33. package/runtime/scripts/hfs/architecture/backend.mjs +378 -357
  34. package/runtime/scripts/hfs/architecture/background-unowned.mjs +201 -187
  35. package/runtime/scripts/hfs/architecture/canon-clones.mjs +260 -209
  36. package/runtime/scripts/hfs/architecture/canon-jsx-clones.mjs +149 -116
  37. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +72 -68
  38. package/runtime/scripts/hfs/architecture/clones.mjs +212 -217
  39. package/runtime/scripts/hfs/architecture/config-unread.mjs +7 -9
  40. package/runtime/scripts/hfs/architecture/config.mjs +302 -268
  41. package/runtime/scripts/hfs/architecture/connection-map.mjs +258 -225
  42. package/runtime/scripts/hfs/architecture/constructor-deps.mjs +80 -71
  43. package/runtime/scripts/hfs/architecture/context-coupling.mjs +82 -77
  44. package/runtime/scripts/hfs/architecture/context-map.mjs +267 -234
  45. package/runtime/scripts/hfs/architecture/context-owner.mjs +73 -61
  46. package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +47 -43
  47. package/runtime/scripts/hfs/architecture/context-transaction.mjs +54 -52
  48. package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +113 -104
  49. package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +288 -270
  50. package/runtime/scripts/hfs/architecture/contracts.mjs +408 -393
  51. package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +50 -51
  52. package/runtime/scripts/hfs/architecture/dead-exports-bindings.mjs +143 -119
  53. package/runtime/scripts/hfs/architecture/dead-exports.mjs +286 -228
  54. package/runtime/scripts/hfs/architecture/default-deny.mjs +129 -110
  55. package/runtime/scripts/hfs/architecture/doc-language.mjs +24 -19
  56. package/runtime/scripts/hfs/architecture/entity-registry.mjs +20 -22
  57. package/runtime/scripts/hfs/architecture/entrypoint.mjs +47 -45
  58. package/runtime/scripts/hfs/architecture/error-codes.mjs +37 -35
  59. package/runtime/scripts/hfs/architecture/error-masked.mjs +55 -59
  60. package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +24 -23
  61. package/runtime/scripts/hfs/architecture/feature-shape.mjs +40 -38
  62. package/runtime/scripts/hfs/architecture/framework-pinned.mjs +21 -21
  63. package/runtime/scripts/hfs/architecture/frontend-block-render.mjs +106 -98
  64. package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +153 -150
  65. package/runtime/scripts/hfs/architecture/frontend-routing.mjs +161 -149
  66. package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +343 -305
  67. package/runtime/scripts/hfs/architecture/frontend-world.mjs +340 -294
  68. package/runtime/scripts/hfs/architecture/frontend.mjs +174 -184
  69. package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +124 -106
  70. package/runtime/scripts/hfs/architecture/hfs-graph.mjs +52 -48
  71. package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +81 -71
  72. package/runtime/scripts/hfs/architecture/i18n-keys.mjs +155 -141
  73. package/runtime/scripts/hfs/architecture/identity.mjs +141 -125
  74. package/runtime/scripts/hfs/architecture/index.mjs +272 -272
  75. package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +45 -43
  76. package/runtime/scripts/hfs/architecture/jwt-ast.mjs +195 -176
  77. package/runtime/scripts/hfs/architecture/machine-ast.mjs +163 -155
  78. package/runtime/scripts/hfs/architecture/managed-scripts.mjs +19 -20
  79. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +177 -172
  80. package/runtime/scripts/hfs/architecture/next-data-contract.mjs +100 -78
  81. package/runtime/scripts/hfs/architecture/next-data-inference.mjs +170 -154
  82. package/runtime/scripts/hfs/architecture/next-data-key.mjs +311 -275
  83. package/runtime/scripts/hfs/architecture/next-data-symbols.mjs +17 -17
  84. package/runtime/scripts/hfs/architecture/next-data.mjs +351 -334
  85. package/runtime/scripts/hfs/architecture/operation-contract-roots.mjs +39 -37
  86. package/runtime/scripts/hfs/architecture/owners.mjs +97 -98
  87. package/runtime/scripts/hfs/architecture/package-shape.mjs +56 -55
  88. package/runtime/scripts/hfs/architecture/presentation.mjs +138 -130
  89. package/runtime/scripts/hfs/architecture/reachability.mjs +258 -241
  90. package/runtime/scripts/hfs/architecture/register-once.mjs +152 -139
  91. package/runtime/scripts/hfs/architecture/registration.mjs +301 -267
  92. package/runtime/scripts/hfs/architecture/required-files.mjs +172 -167
  93. package/runtime/scripts/hfs/architecture/route-files-thin.mjs +85 -77
  94. package/runtime/scripts/hfs/architecture/schema-owner.mjs +206 -181
  95. package/runtime/scripts/hfs/architecture/server-action.mjs +48 -48
  96. package/runtime/scripts/hfs/architecture/side-boundary.mjs +2 -5
  97. package/runtime/scripts/hfs/architecture/source-names-shape.mjs +261 -259
  98. package/runtime/scripts/hfs/architecture/source-names.mjs +304 -278
  99. package/runtime/scripts/hfs/architecture/sql-keys.mjs +277 -194
  100. package/runtime/scripts/hfs/architecture/sql-owner.mjs +236 -194
  101. package/runtime/scripts/hfs/architecture/sql-returning.mjs +46 -45
  102. package/runtime/scripts/hfs/architecture/sql-token-forms.mjs +86 -74
  103. package/runtime/scripts/hfs/architecture/sql-tokens.mjs +456 -341
  104. package/runtime/scripts/hfs/architecture/staged-consumers.mjs +35 -30
  105. package/runtime/scripts/hfs/architecture/surface.mjs +64 -74
  106. package/runtime/scripts/hfs/architecture/symbols.mjs +209 -216
  107. package/runtime/scripts/hfs/architecture/test-world-files.mjs +165 -155
  108. package/runtime/scripts/hfs/architecture/tiers.mjs +161 -154
  109. package/runtime/scripts/hfs/architecture/transport-owner.mjs +155 -149
  110. package/runtime/scripts/hfs/architecture/type-context.mjs +255 -250
  111. package/runtime/scripts/hfs/architecture/typescript.mjs +358 -318
  112. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +110 -92
  113. package/runtime/scripts/hfs/declaration-shape.mjs +140 -103
  114. package/runtime/scripts/hfs/declaration-slots.mjs +64 -61
  115. package/runtime/scripts/hfs/edition-slots.mjs +43 -40
  116. package/runtime/scripts/hfs/linear-text.mjs +20 -20
  117. package/runtime/scripts/hfs/param-names.mjs +6 -11
  118. package/runtime/scripts/hfs/path-findings.mjs +120 -100
  119. package/runtime/scripts/hfs/repo-identity.mjs +55 -42
  120. package/runtime/scripts/hfs/rule-catalog.mjs +353 -257
  121. package/runtime/scripts/hfs/rule-params-shape.mjs +60 -47
  122. package/runtime/scripts/hfs/rules/database-migrations.mjs +428 -388
  123. package/runtime/scripts/hfs/rules/fe-no-tests.mjs +37 -36
  124. package/runtime/scripts/hfs/rules/read.mjs +19 -15
  125. package/runtime/scripts/hfs/rules/secrets.mjs +1 -1
  126. package/runtime/scripts/hfs/rules/spec-placement.mjs +12 -13
  127. package/runtime/scripts/hfs/slot-app-view.mjs +69 -74
  128. package/runtime/scripts/hfs/slot-classify.mjs +42 -38
  129. package/runtime/scripts/hfs/slot-errors.mjs +8 -7
  130. package/runtime/scripts/hfs/slot-imports.mjs +70 -63
  131. package/runtime/scripts/hfs/slot-manifest-shape.mjs +74 -52
  132. package/runtime/scripts/hfs/slot-match.mjs +122 -111
  133. package/runtime/scripts/hfs/slot-path.mjs +0 -2
  134. package/runtime/scripts/hfs/slot-required.mjs +65 -59
  135. package/runtime/scripts/hfs/slot-semantic-problems.mjs +138 -112
  136. package/runtime/scripts/hfs/slot-side-problems.mjs +23 -19
  137. package/runtime/scripts/hfs/slots.mjs +175 -152
  138. package/runtime/scripts/hfs/test-world-slot.mjs +0 -1
  139. package/runtime/scripts/hfs/trailing-slashes.mjs +4 -4
  140. package/runtime/scripts/hfs/typescript-programs.mjs +66 -69
  141. package/runtime/scripts/hfs/view.mjs +38 -40
  142. package/runtime/scripts/lib/dockerfile.mjs +131 -114
  143. package/runtime/scripts/lib/env.mjs +3 -6
  144. package/runtime/scripts/lib/event-contract.mjs +73 -78
  145. package/runtime/scripts/lib/git.mjs +15 -16
  146. package/runtime/scripts/lib/glob.mjs +23 -14
  147. package/runtime/scripts/lib/grammar-lint.mjs +33 -37
  148. package/runtime/scripts/lib/i18n.mjs +32 -35
  149. package/runtime/scripts/lib/in-order.mjs +57 -38
  150. package/runtime/scripts/lib/json-clone.mjs +45 -36
  151. package/runtime/scripts/lib/json.mjs +26 -14
  152. package/runtime/scripts/lib/language.mjs +133 -129
  153. package/runtime/scripts/lib/list.mjs +9 -14
  154. package/runtime/scripts/lib/mutation-fence.mjs +10 -5
  155. package/runtime/scripts/lib/path-key.mjs +53 -38
  156. package/runtime/scripts/lib/read-text.mjs +30 -17
  157. package/runtime/scripts/lib/regex.mjs +1 -2
  158. package/runtime/scripts/lib/same-text.mjs +0 -1
  159. package/runtime/scripts/lib/sops-envelope.mjs +148 -120
  160. package/runtime/scripts/lib/stack-services.mjs +128 -129
  161. package/runtime/scripts/lib/ts-analysis.mjs +28 -26
  162. package/runtime/scripts/lib/ts-ast.mjs +36 -42
  163. package/runtime/scripts/lib/walk.mjs +33 -27
  164. package/runtime/scripts/lib/yaml-cached.mjs +4 -4
@@ -1,53 +1,55 @@
1
1
  /** The app emitter consumes the API app's declared operation table without importing it from app.module.ts. */
2
2
  import path from 'node:path';
3
3
  import { hasAnyFlag } from '../../lib/ts-ast.mjs';
4
-
5
4
  const actualSymbol = (ts, checker, symbol) => symbol && hasAnyFlag(symbol.flags, ts.SymbolFlags.Alias) ? checker.getAliasedSymbol(symbol) : symbol;
6
-
7
5
  /** Follow declaration aliases while retaining the actual expression rather than an asserted type. */
8
6
  const initializerOf = (ts, checker, node, seen = new Set()) => {
9
- if (!node || seen.has(node)) return null;
10
- seen.add(node);
11
- if (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)
12
- || ts.isSatisfiesExpression(node) || ts.isNonNullExpression(node)) return initializerOf(ts, checker, node.expression, seen);
13
- if (!ts.isIdentifier(node) && !ts.isPropertyAccessExpression(node)) return node;
14
- const declaration = actualSymbol(ts, checker, checker.getSymbolAtLocation(node))?.valueDeclaration;
15
- return declaration?.initializer ? initializerOf(ts, checker, declaration.initializer, seen) : node;
7
+ if (!node || seen.has(node))
8
+ return null;
9
+ seen.add(node);
10
+ if (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)
11
+ || ts.isSatisfiesExpression(node) || ts.isNonNullExpression(node))
12
+ return initializerOf(ts, checker, node.expression, seen);
13
+ if (!ts.isIdentifier(node) && !ts.isPropertyAccessExpression(node))
14
+ return node;
15
+ const declaration = actualSymbol(ts, checker, checker.getSymbolAtLocation(node))?.valueDeclaration; // reason: a value declaration may or may not be a variable declaration; `initializer` is read only when present
16
+ return declaration?.initializer ? initializerOf(ts, checker, declaration.initializer, seen) : node;
16
17
  };
17
-
18
18
  /** A same-named local function cannot declare the platform operation contract. */
19
19
  const isOperationDefiner = (ts, checker, graph, config, expression, seen = new Set()) => {
20
- const symbol = actualSymbol(ts, checker, checker.getSymbolAtLocation(expression));
21
- if (!symbol || seen.has(symbol)) return false;
22
- seen.add(symbol);
23
- return (symbol?.declarations ?? []).some(declaration => {
24
- const relative = path.relative(config.root, declaration.getSourceFile().fileName).split(path.sep).join('/');
25
- const owner = graph.resolver.ownerOf(relative);
26
- if (declaration.name?.text === 'defineOperations' && owner?.slot === 'be.platform' && path.posix.basename(owner.root) === 'operations') return true;
27
- const alias = declaration.initializer;
28
- return Boolean(alias && (ts.isIdentifier(alias) || ts.isPropertyAccessExpression(alias)) && isOperationDefiner(ts, checker, graph, config, alias, seen));
29
- });
20
+ const symbol = actualSymbol(ts, checker, checker.getSymbolAtLocation(expression));
21
+ if (!symbol || seen.has(symbol))
22
+ return false;
23
+ seen.add(symbol);
24
+ return (symbol.declarations ?? []).some((declaration) => {
25
+ const relative = path.relative(config.root, declaration.getSourceFile().fileName).split(path.sep).join('/');
26
+ const owner = graph.resolver.ownerOf(relative);
27
+ if (declaration.name?.text === 'defineOperations' && owner?.slot === 'be.platform' && path.posix.basename(owner.root) === 'operations')
28
+ return true;
29
+ const alias = declaration.initializer;
30
+ return Boolean(alias && (ts.isIdentifier(alias) || ts.isPropertyAccessExpression(alias)) && isOperationDefiner(ts, checker, graph, config, alias, seen));
31
+ });
30
32
  };
31
-
32
33
  /** True only for an exported, nonempty registration by the canon platform capability. */
33
34
  const declaresOperations = (context, graph, config, sourceFile) => {
34
- const { ts } = context;
35
- const checker = context.checkerFor?.(sourceFile.fileName);
36
- const moduleSymbol = checker?.getSymbolAtLocation(sourceFile);
37
- if (!moduleSymbol) return false;
38
- const exported = checker.getExportsOfModule(moduleSymbol).find(symbol => symbol.getName() === 'OPERATIONS');
39
- const declaration = actualSymbol(ts, checker, exported)?.valueDeclaration;
40
- const registration = initializerOf(ts, checker, declaration?.initializer);
41
- if (!registration || !ts.isCallExpression(registration) || !isOperationDefiner(ts, checker, graph, config, registration.expression)) return false;
42
- const entries = initializerOf(ts, checker, registration.arguments[0]);
43
- return entries !== null && ts.isObjectLiteralExpression(entries) && checker.getPropertiesOfType(checker.getTypeAtLocation(entries)).length > 0;
35
+ const { ts } = context;
36
+ const checker = context.checkerFor?.(sourceFile.fileName);
37
+ const moduleSymbol = checker?.getSymbolAtLocation(sourceFile);
38
+ if (!checker || !moduleSymbol)
39
+ return false;
40
+ const exported = checker.getExportsOfModule(moduleSymbol).find(symbol => symbol.getName() === 'OPERATIONS');
41
+ const declaration = actualSymbol(ts, checker, exported)?.valueDeclaration; // reason: see initializerOf
42
+ const registration = initializerOf(ts, checker, declaration?.initializer);
43
+ if (!registration || !ts.isCallExpression(registration) || !isOperationDefiner(ts, checker, graph, config, registration.expression))
44
+ return false;
45
+ const entries = initializerOf(ts, checker, registration.arguments[0]);
46
+ return entries !== null && ts.isObjectLiteralExpression(entries) && checker.getPropertiesOfType(checker.getTypeAtLocation(entries)).length > 0;
44
47
  };
45
-
46
48
  /** Only the emitter's exact entry of a declared API app is a contract root; workers and arbitrary app files remain judged. */
47
49
  export function operationContractRoots(context, graph, config) {
48
- return (config.apps ?? []).filter(app => app.kind === 'api').flatMap(app => {
49
- const relative = `apps/${app.name}/src/operations.ts`;
50
- const node = graph.files.get(relative);
51
- return node?.tier === 'app' && node.slot === 'be.app.api' && declaresOperations(context, graph, config, node.sourceFile) ? [relative] : [];
52
- });
50
+ return (config.apps ?? []).filter(app => app.kind === 'api').flatMap(app => {
51
+ const relative = `apps/${app.name}/src/operations.ts`;
52
+ const node = graph.files.get(relative);
53
+ return node?.tier === 'app' && node.slot === 'be.app.api' && declaresOperations(context, graph, config, node.sourceFile) ? [relative] : [];
54
+ });
53
55
  }
@@ -5,139 +5,138 @@ import { sourceLocation } from '../../lib/ts-ast.mjs';
5
5
  import { globExpression } from '../../lib/glob.mjs';
6
6
  import { posixPath } from '../../lib/path-key.mjs';
7
7
  import { registryImporter, registrySlot } from './entity-registry.mjs';
8
-
9
8
  function absolute(root, relative) {
10
- return canonical(path.resolve(root, ...relative.split('/')));
9
+ return canonical(path.resolve(root, ...relative.split('/')));
11
10
  }
12
-
13
11
  /**
14
12
  * The extra public entries the slot manifest declares for the owner (slot field `entries`, on the slot that holds its entry file):
15
13
  * exact files as absolute paths, and the entries holding a `*` (one file per operation, `graphql/queries/query-*.ts`) as patterns.
16
14
  */
17
15
  function slotEntries(config, owner) {
18
- const found = config.hfs?.classifyPath?.(owner.entry);
19
- const names = found?.slot ? config.hfs.slot(found.slot)?.entries ?? [] : [];
20
- const exact = names.filter(name => !name.includes('*')).map(name => absolute(config.root, `${found.root}/${name}`));
21
- const patterns = names.filter(name => name.includes('*')).map(name => globExpression(`${posixPath(absolute(config.root, found.root))}/${name}`));
22
- return { exact, patterns };
16
+ const found = config.hfs?.classifyPath?.(owner.entry);
17
+ if (!found?.slot)
18
+ return { exact: [], patterns: [] };
19
+ const names = config.hfs.slot(found.slot)?.entries ?? [];
20
+ const exact = names.filter(name => !name.includes('*')).map(name => absolute(config.root, `${found.root}/${name}`));
21
+ const patterns = names.filter(name => name.includes('*')).map(name => globExpression(`${posixPath(absolute(config.root, found.root))}/${name}`));
22
+ return { exact, patterns };
23
23
  }
24
-
25
24
  /** The owner's public entries: its declared entry, the `entries` its slot declares, plus every source file its package.json `exports` maps when the owner is a workspace package. */
26
25
  function ownerDeclarations(config, context) {
27
- return config.owners.map(owner => {
28
- const root = absolute(config.root, owner.root);
29
- const entry = absolute(config.root, owner.entry);
30
- const workspace = context.workspaces?.find(item => item.root === root);
31
- const { exact: declared, patterns } = slotEntries(config, owner);
32
- const entries = new Set([entry, ...declared, ...(workspace ? workspaceExportSources(context.ts, workspace) : [])]);
33
- const isEntry = file => entries.has(file) || patterns.some(pattern => pattern.test(posixPath(file)));
34
- return { ...owner, root, entry, entries, isEntry, declared };
35
- }).sort((a, b) => b.root.length - a.root.length);
26
+ return config.owners.map((owner) => {
27
+ const root = absolute(config.root, owner.root);
28
+ const entry = absolute(config.root, owner.entry);
29
+ const workspace = context.workspaces?.find(item => item.root === root);
30
+ const { exact: declared, patterns } = slotEntries(config, owner);
31
+ const entries = new Set([entry, ...declared, ...(workspace ? workspaceExportSources(context.ts, workspace) : [])]);
32
+ const isEntry = (file) => entries.has(file) || patterns.some(pattern => pattern.test(posixPath(file)));
33
+ return { ...owner, root, entry, entries, isEntry, declared };
34
+ }).sort((a, b) => b.root.length - a.root.length);
36
35
  }
37
-
38
36
  function ownerOf(owners, file) {
39
- return owners.find(owner => isInside(owner.root, file)) ?? null;
37
+ return owners.find(owner => isInside(owner.root, file)) ?? null;
40
38
  }
41
-
42
39
  function reexportSteps(context, current) {
43
- return (context.edges.get(current.file) ?? []).filter(candidate => candidate.reexport).map(candidate => ({ file: candidate.to, chain: [...current.chain, candidate.to] }));
40
+ return (context.edges.get(current.file) ?? []).filter(candidate => candidate.reexport).map(candidate => ({ file: candidate.to, chain: [...current.chain, candidate.to] }));
44
41
  }
45
-
46
42
  function privateOwnerChain(context, owners, edge) {
47
- const sourceOwner = ownerOf(owners, edge.from);
48
- const queue = [{ file: edge.to, chain: [edge.from, edge.to] }];
49
- const visited = new Set();
50
- while (queue.length) {
51
- const current = queue.shift();
52
- if (visited.has(current.file)) continue;
53
- visited.add(current.file);
54
- const owner = ownerOf(owners, current.file);
55
- if (owner && sourceOwner?.id !== owner.id) return owner.isEntry(current.file) ? null : { owner, chain: current.chain };
56
- queue.push(...reexportSteps(context, current));
57
- }
58
- return null;
43
+ const sourceOwner = ownerOf(owners, edge.from);
44
+ const queue = [{ file: edge.to, chain: [edge.from, edge.to] }];
45
+ const visited = new Set();
46
+ for (let current = queue.shift(); current; current = queue.shift()) {
47
+ if (visited.has(current.file))
48
+ continue;
49
+ visited.add(current.file);
50
+ const owner = ownerOf(owners, current.file);
51
+ if (owner && sourceOwner?.id !== owner.id)
52
+ return owner.isEntry(current.file) ? null : { owner, chain: current.chain };
53
+ queue.push(...reexportSteps(context, current));
54
+ }
55
+ return null;
59
56
  }
60
-
61
57
  /**
62
58
  * A `*.builder.ts` of slot be.tests.fixtures.builders arranges data at the schema level: it may deep-import the persistence
63
59
  * entity classes and connection entity lists (slot be.persistence) of any capability. No other file may.
64
60
  */
65
61
  function arrangesSchema(config, from, to) {
66
- const resolver = config.hfs;
67
- if (!resolver?.classifyPath) return false;
68
- return resolver.classifyPath(relativePath(config.root, from)).slot === 'be.tests.fixtures.builders'
69
- && resolver.classifyPath(relativePath(config.root, to)).slot === 'be.persistence';
62
+ const resolver = config.hfs;
63
+ if (!resolver?.classifyPath)
64
+ return false;
65
+ return resolver.classifyPath(relativePath(config.root, from)).slot === 'be.tests.fixtures.builders'
66
+ && resolver.classifyPath(relativePath(config.root, to)).slot === 'be.persistence';
70
67
  }
71
-
72
68
  /**
73
69
  * The entity registry (slot role entity-registry, entity-registry.mjs) is imported by deep path, and only from the files its slot's
74
70
  * `importedBy` names: app composition and the test world. Any other importer goes through the owner's entry, which never exports it.
75
71
  */
76
72
  function importsRegistry(config, from, to) {
77
- const slot = registrySlot(config.hfs, relativePath(config.root, to));
78
- return slot !== null && registryImporter(config.hfs, slot, relativePath(config.root, from));
73
+ const slot = registrySlot(config.hfs, relativePath(config.root, to));
74
+ return slot !== null && registryImporter(config.hfs, slot, relativePath(config.root, from));
79
75
  }
80
-
81
76
  function publicEntryViolations(config, context, owners, sourceFiles) {
82
- const violations = [];
83
- for (const owner of owners) {
84
- const sourceFile = sourceFiles.get(owner.entry);
85
- if (!sourceFile) {
86
- violations.push({ ruleId: 'ARCH_OWNER_EXPORT_BYPASS', path: relativePath(config.root, owner.entry), line: 1, column: 1, owner: owner.id,
87
- message: `Owner ${owner.id} public entry is outside the configured TypeScript programs, so its boundary cannot be checked.` });
88
- continue;
89
- }
90
- const publicFiles = new Set([
91
- sourceFile,
92
- ...owner.declared.map(file => sourceFiles.get(file)).filter(Boolean),
93
- ]);
94
- for (const entryFile of publicFiles) {
95
- for (const statement of entryFile.statements) if (context.ts.isExportDeclaration(statement) && !statement.exportClause) {
96
- violations.push({
97
- ruleId: 'ARCH_OWNER_EXPORT_STAR',
98
- path: relativePath(config.root, canonical(entryFile.fileName)),
99
- ...sourceLocation(entryFile, statement),
100
- owner: owner.id,
101
- message: `Owner ${owner.id} public entry must use explicit named exports rather than export *.`,
102
- });
103
- }
77
+ const violations = [];
78
+ for (const owner of owners) {
79
+ const sourceFile = sourceFiles.get(owner.entry);
80
+ if (!sourceFile) {
81
+ violations.push({ ruleId: 'ARCH_OWNER_EXPORT_BYPASS', path: relativePath(config.root, owner.entry), line: 1, column: 1, owner: owner.id,
82
+ message: `Owner ${owner.id} public entry is outside the configured TypeScript programs, so its boundary cannot be checked.` });
83
+ continue;
84
+ }
85
+ const publicFiles = new Set([
86
+ sourceFile,
87
+ ...owner.declared.map(file => sourceFiles.get(file)).filter((file) => Boolean(file)),
88
+ ]);
89
+ for (const entryFile of publicFiles) {
90
+ for (const statement of entryFile.statements)
91
+ if (context.ts.isExportDeclaration(statement) && !statement.exportClause) {
92
+ violations.push({
93
+ ruleId: 'ARCH_OWNER_EXPORT_STAR',
94
+ path: relativePath(config.root, canonical(entryFile.fileName)),
95
+ ...sourceLocation(entryFile, statement),
96
+ owner: owner.id,
97
+ message: `Owner ${owner.id} public entry must use explicit named exports rather than export *.`,
98
+ });
99
+ }
100
+ }
104
101
  }
105
- }
106
- return violations;
102
+ return violations;
107
103
  }
108
-
109
104
  function ownerImportViolations(config, context, owners) {
110
- const violations = [];
111
- for (const sourceFile of context.files) {
112
- const from = canonical(sourceFile.fileName);
113
- for (const edge of context.edges.get(from) ?? []) {
114
- const bypass = privateOwnerChain(context, owners, edge);
115
- if (!bypass) continue;
116
- if (arrangesSchema(config, from, bypass.chain.at(-1))) continue;
117
- if (bypass.chain.length === 2 && importsRegistry(config, from, bypass.chain.at(-1))) continue;
118
- violations.push({
119
- ruleId: 'ARCH_OWNER_EXPORT_BYPASS',
120
- path: relativePath(config.root, from),
121
- line: edge.line,
122
- column: edge.column,
123
- owner: bypass.owner.id,
124
- specifier: edge.specifier,
125
- resolvedPath: relativePath(config.root, bypass.chain.at(-1)),
126
- dependencyChain: bypass.chain.map(file => relativePath(config.root, file)),
127
- message: `Cross-owner dependency must enter ${bypass.owner.id} through ${relativePath(config.root, bypass.owner.entry)}.`,
128
- });
105
+ const violations = [];
106
+ for (const sourceFile of context.files) {
107
+ const from = canonical(sourceFile.fileName);
108
+ for (const edge of context.edges.get(from) ?? []) {
109
+ const bypass = privateOwnerChain(context, owners, edge);
110
+ if (!bypass)
111
+ continue;
112
+ const target = bypass.chain.at(-1) ?? edge.to;
113
+ if (arrangesSchema(config, from, target))
114
+ continue;
115
+ if (bypass.chain.length === 2 && importsRegistry(config, from, target))
116
+ continue;
117
+ violations.push({
118
+ ruleId: 'ARCH_OWNER_EXPORT_BYPASS',
119
+ path: relativePath(config.root, from),
120
+ line: edge.line,
121
+ column: edge.column,
122
+ owner: bypass.owner.id,
123
+ specifier: edge.specifier,
124
+ resolvedPath: relativePath(config.root, target),
125
+ dependencyChain: bypass.chain.map(file => relativePath(config.root, file)),
126
+ message: `Cross-owner dependency must enter ${bypass.owner.id} through ${relativePath(config.root, bypass.owner.entry)}.`,
127
+ });
128
+ }
129
129
  }
130
- }
131
- return violations;
130
+ return violations;
132
131
  }
133
-
134
132
  /** Enforce explicit same-source owner entries without constraining imports inside one owner. */
135
133
  export function checkOwners(config, context) {
136
- if (!config.owners?.length) return [];
137
- const owners = ownerDeclarations(config, context);
138
- const sourceFiles = new Map(context.files.map(file => [canonical(file.fileName), file]));
139
- return [
140
- ...publicEntryViolations(config, context, owners, sourceFiles),
141
- ...ownerImportViolations(config, context, owners),
142
- ];
134
+ if (!config.owners?.length)
135
+ return [];
136
+ const owners = ownerDeclarations(config, context);
137
+ const sourceFiles = new Map(context.files.map((file) => [canonical(file.fileName), file]));
138
+ return [
139
+ ...publicEntryViolations(config, context, owners, sourceFiles),
140
+ ...ownerImportViolations(config, context, owners),
141
+ ];
143
142
  }
@@ -2,7 +2,6 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { treeOf } from './required-files.mjs';
4
4
  import { byCodeUnit } from '../../lib/list.mjs';
5
-
6
5
  /**
7
6
  * R63 `package-shape` (FE_PACKAGE_SHAPE, knowledge/hfs/README.md 6.2). A shared package is built to `dist` and says what it
8
7
  * exports. The package manifest of every package owner (slots repo.packages and fe.package.ui, packages/<pkg>/package.json)
@@ -17,75 +16,77 @@ import { byCodeUnit } from '../../lib/list.mjs';
17
16
  * eslint rule `monorepo-tier-belongs-to-its-side`; none is judged a second time here.
18
17
  */
19
18
  export const PACKAGE_SHAPE_RULE_IDS = ['FE_PACKAGE_SHAPE'];
20
-
21
19
  const RULE = 'FE_PACKAGE_SHAPE';
22
20
  const UNREADABLE = Symbol('unreadable');
23
-
24
21
  /** Every string target of an exports value (a string, a condition map, an array of either), with the condition path. */
25
22
  function targetsOf(value, trail = []) {
26
- if (typeof value === 'string') return [{ target: value, trail }];
27
- if (Array.isArray(value)) return value.flatMap((item, index) => targetsOf(item, [...trail, String(index)]));
28
- if (value && typeof value === 'object') return Object.entries(value).flatMap(([key, item]) => targetsOf(item, [...trail, key]));
29
- return [];
23
+ if (typeof value === 'string')
24
+ return [{ target: value, trail }];
25
+ if (Array.isArray(value))
26
+ return value.flatMap((item, index) => targetsOf(item, [...trail, String(index)]));
27
+ if (value && typeof value === 'object')
28
+ return Object.entries(value).flatMap(([key, item]) => targetsOf(item, [...trail, key]));
29
+ return [];
30
30
  }
31
-
32
- const insideDist = target => path.posix.normalize(target).replace(/^\.\//u, '').startsWith('dist/');
33
-
31
+ const insideDist = (target) => path.posix.normalize(target).replace(/^\.\//u, '').startsWith('dist/');
34
32
  function packageRoots(config, resolver) {
35
- const roots = new Set();
36
- for (const rel of treeOf(config.root).files) {
37
- const owner = resolver.ownerOf(rel);
38
- if (owner && resolver.slot(owner.slot)?.tier === 'package') roots.add(owner.root);
39
- }
40
- return [...roots].sort(byCodeUnit);
33
+ const roots = new Set();
34
+ for (const rel of treeOf(config.root).files) {
35
+ const owner = resolver.ownerOf(rel);
36
+ if (owner && resolver.slot(owner.slot)?.tier === 'package')
37
+ roots.add(owner.root);
38
+ }
39
+ return [...roots].sort(byCodeUnit);
41
40
  }
42
-
43
41
  function readManifest(config, manifestRel) {
44
- try {
45
- return JSON.parse(fs.readFileSync(path.join(config.root, ...manifestRel.split('/')), 'utf8'));
46
- } catch {
47
- return UNREADABLE;
48
- }
42
+ try {
43
+ return JSON.parse(fs.readFileSync(path.join(config.root, ...manifestRel.split('/')), 'utf8')); // reason: parsed from disk; the fields read are checked at use
44
+ }
45
+ catch {
46
+ return UNREADABLE;
47
+ }
49
48
  }
50
-
51
49
  function reportExportIssues(manifest, manifestRel, report) {
52
- if (manifest.exports === undefined) {
53
- report(`${manifestRel} declares no exports map; a package lists what it exports, subpath by subpath, each pointing into ./dist/.`);
54
- return;
55
- }
56
- if (typeof manifest.exports === 'object' && !Array.isArray(manifest.exports)) {
57
- for (const key of Object.keys(manifest.exports)) {
58
- if (key.includes('*') || key.endsWith('/')) report(`exports key "${key}" of ${manifestRel} is a pattern or folder mapping; list each exported subpath explicitly.`, { export: key });
50
+ if (manifest.exports === undefined) {
51
+ report(`${manifestRel} declares no exports map; a package lists what it exports, subpath by subpath, each pointing into ./dist/.`);
52
+ return;
53
+ }
54
+ if (typeof manifest.exports === 'object' && !Array.isArray(manifest.exports)) {
55
+ for (const key of Object.keys(manifest.exports)) { // reason: typeof narrows to object | null; a null exports map throws here exactly as before
56
+ if (key.includes('*') || key.endsWith('/'))
57
+ report(`exports key "${key}" of ${manifestRel} is a pattern or folder mapping; list each exported subpath explicitly.`, { export: key });
58
+ }
59
+ }
60
+ for (const { target, trail } of targetsOf(manifest.exports)) {
61
+ if (!insideDist(target))
62
+ report(`exports target "${target}" (${trail.join(' > ') || '.'}) of ${manifestRel} is not inside ./dist/; a package exports what it built, never its source.`, { export: target });
59
63
  }
60
- }
61
- for (const { target, trail } of targetsOf(manifest.exports)) {
62
- if (!insideDist(target)) report(`exports target "${target}" (${trail.join(' > ') || '.'}) of ${manifestRel} is not inside ./dist/; a package exports what it built, never its source.`, { export: target });
63
- }
64
64
  }
65
-
66
65
  function reportManifestIssues(manifest, manifestRel, report) {
67
- if (typeof manifest.scripts?.build !== 'string' || !manifest.scripts.build.trim()) report(`${manifestRel} has no scripts.build; a shared package is built to dist before an app consumes it.`);
68
- reportExportIssues(manifest, manifestRel, report);
69
- for (const field of ['main', 'module', 'types', 'typings']) {
70
- if (typeof manifest[field] === 'string' && !insideDist(manifest[field])) report(`${field} "${manifest[field]}" of ${manifestRel} is not inside ./dist/; a package points at what it built.`, { field });
71
- }
66
+ if (typeof manifest.scripts?.build !== 'string' || !manifest.scripts.build.trim())
67
+ report(`${manifestRel} has no scripts.build; a shared package is built to dist before an app consumes it.`);
68
+ reportExportIssues(manifest, manifestRel, report);
69
+ for (const field of ['main', 'module', 'types', 'typings']) {
70
+ if (typeof manifest[field] === 'string' && !insideDist(manifest[field]))
71
+ report(`${field} "${manifest[field]}" of ${manifestRel} is not inside ./dist/; a package points at what it built.`, { field });
72
+ }
72
73
  }
73
-
74
74
  function inspectPackage(config, root, violations) {
75
- const manifestRel = `${root}/package.json`;
76
- const report = (message, extra = {}) => violations.push({ ruleId: RULE, path: manifestRel, line: 1, column: 1, package: root, message, ...extra });
77
- const manifest = readManifest(config, manifestRel);
78
- if (manifest === UNREADABLE) return false;
79
- reportManifestIssues(manifest, manifestRel, report);
80
- return true;
75
+ const manifestRel = `${root}/package.json`;
76
+ const report = (message, extra = {}) => violations.push({ ruleId: RULE, path: manifestRel, line: 1, column: 1, package: root, message, ...extra });
77
+ const manifest = readManifest(config, manifestRel);
78
+ if (manifest === UNREADABLE)
79
+ return false;
80
+ reportManifestIssues(manifest, manifestRel, report);
81
+ return true;
81
82
  }
82
-
83
83
  export function checkPackageShape({ config, graph }) {
84
- const roots = packageRoots(config, graph.resolver);
85
- const violations = [];
86
- let packages = 0;
87
- for (const root of roots) {
88
- if (inspectPackage(config, root, violations)) packages += 1;
89
- }
90
- return { violations, coverage: { status: 'checked', packages } };
84
+ const roots = packageRoots(config, graph.resolver);
85
+ const violations = [];
86
+ let packages = 0;
87
+ for (const root of roots) {
88
+ if (inspectPackage(config, root, violations))
89
+ packages += 1;
90
+ }
91
+ return { violations, coverage: { status: 'checked', packages } };
91
92
  }