@starci/eslint-canon-be 3.0.10 → 3.2.0

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 (149) hide show
  1. package/CHANGELOG.md +7 -1
  2. package/README.md +2 -1
  3. package/builders.mjs +1 -1
  4. package/comments.mjs +1 -1
  5. package/config-owner.mjs +1 -1
  6. package/index.mjs +2 -0
  7. package/lib/config.mjs +4 -1
  8. package/lib/doors.mjs +3 -1
  9. package/module-shape.mjs +1 -2
  10. package/package.json +1 -1
  11. package/resilience.mjs +1 -1
  12. package/runtime/config.example.yaml +194 -0
  13. package/runtime/engine/by-code-unit.mjs +12 -0
  14. package/runtime/engine/config.mjs +222 -239
  15. package/runtime/engine/invalid-config.mjs +5 -5
  16. package/runtime/engine/model-config.mjs +90 -0
  17. package/runtime/engine/orca-config.mjs +3 -1
  18. package/runtime/engine/release-config.mjs +28 -0
  19. package/runtime/engine/removed-vocabulary.mjs +28 -0
  20. package/runtime/engine/resources-config.mjs +52 -0
  21. package/runtime/engine/runtime-root.mjs +17 -0
  22. package/runtime/engine/secrets.mjs +35 -31
  23. package/runtime/engine/sonar-config.mjs +21 -0
  24. package/runtime/engine/temp-root.mjs +32 -0
  25. package/runtime/knowledge/hfs/canon-pins.yaml +14 -14
  26. package/runtime/knowledge/hfs/rules.yaml +161 -65
  27. package/runtime/knowledge/hfs/slots.yaml +11 -14
  28. package/runtime/modules/kernel/removed-vocabulary.yaml +268 -0
  29. package/runtime/modules/models/registry.yaml +25 -359
  30. package/runtime/modules/models/runtimes.yaml +126 -265
  31. package/runtime/modules/models/tiers.yaml +66 -0
  32. package/runtime/scripts/api/git/lib.mjs +34 -1
  33. package/runtime/scripts/hfs/allows.mjs +3 -3
  34. package/runtime/scripts/hfs/architecture/ast-walks.mjs +63 -50
  35. package/runtime/scripts/hfs/architecture/automatic-gates.mjs +96 -0
  36. package/runtime/scripts/hfs/architecture/backend.mjs +261 -175
  37. package/runtime/scripts/hfs/architecture/background-unowned.mjs +69 -34
  38. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +55 -54
  39. package/runtime/scripts/hfs/architecture/clones.mjs +179 -106
  40. package/runtime/scripts/hfs/architecture/config-unread.mjs +5 -22
  41. package/runtime/scripts/hfs/architecture/config.mjs +95 -80
  42. package/runtime/scripts/hfs/architecture/connection-map.mjs +233 -141
  43. package/runtime/scripts/hfs/architecture/constructor-deps.mjs +15 -9
  44. package/runtime/scripts/hfs/architecture/context-coupling.mjs +76 -52
  45. package/runtime/scripts/hfs/architecture/context-map.mjs +215 -141
  46. package/runtime/scripts/hfs/architecture/context-owner.mjs +39 -30
  47. package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +31 -17
  48. package/runtime/scripts/hfs/architecture/context-transaction.mjs +38 -25
  49. package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +86 -40
  50. package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +342 -0
  51. package/runtime/scripts/hfs/architecture/contracts.mjs +263 -455
  52. package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +43 -34
  53. package/runtime/scripts/hfs/architecture/dead-exports.mjs +201 -154
  54. package/runtime/scripts/hfs/architecture/default-deny.mjs +118 -93
  55. package/runtime/scripts/hfs/architecture/doc-language.mjs +2 -1
  56. package/runtime/scripts/hfs/architecture/entrypoint.mjs +45 -30
  57. package/runtime/scripts/hfs/architecture/error-codes.mjs +19 -12
  58. package/runtime/scripts/hfs/architecture/error-masked.mjs +39 -26
  59. package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +2 -1
  60. package/runtime/scripts/hfs/architecture/feature-shape.mjs +26 -20
  61. package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
  62. package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +190 -0
  63. package/runtime/scripts/hfs/architecture/frontend-routing.mjs +186 -0
  64. package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +311 -0
  65. package/runtime/scripts/hfs/architecture/frontend-world.mjs +328 -0
  66. package/runtime/scripts/hfs/architecture/frontend.mjs +109 -829
  67. package/runtime/scripts/hfs/architecture/hfs-graph.mjs +14 -2
  68. package/runtime/scripts/hfs/architecture/hfs.mjs +120 -288
  69. package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +46 -26
  70. package/runtime/scripts/hfs/architecture/i18n-keys.mjs +114 -76
  71. package/runtime/scripts/hfs/architecture/index.mjs +164 -104
  72. package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +33 -28
  73. package/runtime/scripts/hfs/architecture/machine-ast.mjs +180 -144
  74. package/runtime/scripts/hfs/architecture/managed-scripts.mjs +8 -2
  75. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +150 -101
  76. package/runtime/scripts/hfs/architecture/next-data-contract.mjs +51 -43
  77. package/runtime/scripts/hfs/architecture/next-data-key.mjs +338 -0
  78. package/runtime/scripts/hfs/architecture/next-data.mjs +179 -427
  79. package/runtime/scripts/hfs/architecture/owners.mjs +27 -13
  80. package/runtime/scripts/hfs/architecture/package-shape.mjs +51 -23
  81. package/runtime/scripts/hfs/architecture/presentation.mjs +165 -0
  82. package/runtime/scripts/hfs/architecture/reachability.mjs +127 -61
  83. package/runtime/scripts/hfs/architecture/register-once.mjs +112 -63
  84. package/runtime/scripts/hfs/architecture/registration.mjs +187 -110
  85. package/runtime/scripts/hfs/architecture/required-files.mjs +121 -70
  86. package/runtime/scripts/hfs/architecture/route-files-thin.mjs +83 -58
  87. package/runtime/scripts/hfs/architecture/schema-owner.mjs +182 -108
  88. package/runtime/scripts/hfs/architecture/source-names-shape.mjs +318 -0
  89. package/runtime/scripts/hfs/architecture/source-names.mjs +83 -248
  90. package/runtime/scripts/hfs/architecture/sql-owner.mjs +175 -118
  91. package/runtime/scripts/hfs/architecture/sql-returning.mjs +39 -28
  92. package/runtime/scripts/hfs/architecture/sql-tokens.mjs +302 -172
  93. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +1 -1
  94. package/runtime/scripts/hfs/architecture/supabase-be.mjs +55 -45
  95. package/runtime/scripts/hfs/architecture/supabase-results.mjs +182 -0
  96. package/runtime/scripts/hfs/architecture/supabase-tables.mjs +20 -12
  97. package/runtime/scripts/hfs/architecture/supabase.mjs +191 -263
  98. package/runtime/scripts/hfs/architecture/symbols.mjs +175 -99
  99. package/runtime/scripts/hfs/architecture/test-world-files.mjs +82 -41
  100. package/runtime/scripts/hfs/architecture/tiers.mjs +120 -69
  101. package/runtime/scripts/hfs/architecture/transport-owner.mjs +84 -57
  102. package/runtime/scripts/hfs/architecture/type-context.mjs +282 -0
  103. package/runtime/scripts/hfs/architecture/typescript.mjs +116 -276
  104. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +84 -62
  105. package/runtime/scripts/hfs/declaration-shape.mjs +77 -36
  106. package/runtime/scripts/hfs/declaration-slots.mjs +1 -1
  107. package/runtime/scripts/hfs/edition-slots.mjs +5 -3
  108. package/runtime/scripts/hfs/linear-text.mjs +30 -0
  109. package/runtime/scripts/hfs/manifest-shape.mjs +164 -63
  110. package/runtime/scripts/hfs/path-findings.mjs +58 -38
  111. package/runtime/scripts/hfs/project-graph.mjs +4 -1
  112. package/runtime/scripts/hfs/repo-identity.mjs +21 -4
  113. package/runtime/scripts/hfs/rule-catalog.mjs +194 -0
  114. package/runtime/scripts/hfs/rule-params-shape.mjs +33 -27
  115. package/runtime/scripts/hfs/rules/fe-no-tests.mjs +36 -16
  116. package/runtime/scripts/hfs/rules/secrets.mjs +4 -3
  117. package/runtime/scripts/hfs/slot-app-view.mjs +102 -0
  118. package/runtime/scripts/hfs/slot-classify.mjs +65 -0
  119. package/runtime/scripts/hfs/slot-errors.mjs +11 -0
  120. package/runtime/scripts/hfs/slot-imports.mjs +76 -0
  121. package/runtime/scripts/hfs/slot-manifest-shape.mjs +73 -0
  122. package/runtime/scripts/hfs/slot-match.mjs +153 -0
  123. package/runtime/scripts/hfs/slot-path.mjs +8 -0
  124. package/runtime/scripts/hfs/slot-required.mjs +76 -0
  125. package/runtime/scripts/hfs/slot-semantic-problems.mjs +148 -0
  126. package/runtime/scripts/hfs/slot-side-problems.mjs +28 -0
  127. package/runtime/scripts/hfs/slots.mjs +74 -695
  128. package/runtime/scripts/hfs/trailing-slashes.mjs +8 -0
  129. package/runtime/scripts/hfs/typescript-programs.mjs +6 -4
  130. package/runtime/scripts/hfs/view.mjs +1 -1
  131. package/runtime/scripts/lib/dockerfile.mjs +56 -33
  132. package/runtime/scripts/lib/git.mjs +2 -2
  133. package/runtime/scripts/lib/i18n.mjs +3 -2
  134. package/runtime/scripts/lib/language.mjs +3 -3
  135. package/runtime/scripts/lib/list.mjs +8 -1
  136. package/runtime/scripts/lib/path-key.mjs +38 -5
  137. package/runtime/scripts/lib/regex.mjs +1 -1
  138. package/runtime/scripts/lib/secret-patterns.mjs +6 -2
  139. package/runtime/scripts/lib/sonar-syntax-rules.mjs +225 -0
  140. package/runtime/scripts/lib/sops-envelope.mjs +95 -48
  141. package/runtime/scripts/lib/stack-services.mjs +1 -2
  142. package/runtime/scripts/lib/ts-ast.mjs +9 -0
  143. package/runtime/scripts/lib/walk.mjs +6 -1
  144. package/runtime/scripts/lib/yaml-cached.mjs +14 -0
  145. package/schema-authority.mjs +1 -1
  146. package/service-deps.mjs +1 -1
  147. package/sonar-parity.mjs +19 -0
  148. package/suppression.mjs +2 -6
  149. package/test-world.mjs +2 -10
@@ -0,0 +1,66 @@
1
+ schema: starci/model-tiers@1
2
+ purpose: >-
3
+ The ONE owner of which model takes which work. A tier is an ordered chain of members (agent + model, optional
4
+ effort); the first member with tokens is taken. Tiers are named by capability. A model may sit in several tiers.
5
+ Adding a tier or a member is a line here (or in config.yaml models.tiers) and needs no code.
6
+ tiers:
7
+ frontier:
8
+ - {agent: claude, model: claude-opus-5-5}
9
+ - {agent: codex, model: gpt-6.1-sol, effort: max}
10
+ high:
11
+ - {agent: claude, model: claude-sonnet-5-5}
12
+ - {agent: codex, model: gpt-6.1-sol, effort: high}
13
+ medium:
14
+ - {agent: devin, model: swe-2-max}
15
+ - {agent: codex, model: gpt-6.1-sol, effort: medium}
16
+ low:
17
+ - {agent: devin, model: swe-2-max}
18
+ - {agent: codex, model: gpt-6-luna, effort: low}
19
+ imagegen:
20
+ - {agent: codex, model: gpt-6.1-sol, effort: high}
21
+ # Highest first: a provider with no frontier member takes its member of the first tier that has one.
22
+ tierOrder: [frontier, high, medium, low]
23
+ # Seat -> tier. A seat is a long-lived agent role; an op takes the tier of its difficulty.
24
+ seats:
25
+ supervisor: frontier
26
+ planner: frontier
27
+ validator: frontier
28
+ kernelManager: frontier
29
+ kernel: high
30
+ worker: high
31
+ critic: frontier
32
+ # The kernel's own model calls (selection.yaml kernelFunctionKinds) take the tier of their seat.
33
+ kindSeats:
34
+ model.assessGoal: planner
35
+ model.planOp: planner
36
+ model.decide: kernelManager
37
+ model.manageWorkflow: kernelManager
38
+ model.validateOp: validator
39
+ model.critiqueGoal: validator
40
+ model.classifyScreen: kernelManager
41
+ model.presentOwnerQuestion: kernelManager
42
+ judge: validator
43
+ difficulty:
44
+ insane: frontier
45
+ hard: high
46
+ medium: medium
47
+ easy: low
48
+ # A kind named here takes that tier whatever its difficulty; the drawing ops take the tier of their difficulty like every core op.
49
+ kindTiers: {}
50
+ # A tier no seat and no op takes: only a headless call made by one op is made on it (`calls`). A tier not named here is a seat tier.
51
+ tierUse:
52
+ imagegen: call
53
+ # Headless calls: the call tier whose chain the call is picked from (the same picker: hard filter, bias, balance, token use),
54
+ # the longest one call may run, and the most images and reference images it takes.
55
+ calls:
56
+ imagegen: {tier: imagegen, timeoutMs: 600000, maxImages: 8, maxReferences: 6}
57
+ # Balance over the recent picks of one tier, running seats included.
58
+ balance:
59
+ maxStreak: 3
60
+ maxSharePercent: 70
61
+ # Token use of the chosen member, in percent of its most used quota window. Automatic picks skip a member at
62
+ # `reservePercent`; an owner bias naming the member keeps it usable up to `biasPercent`; `exhaustedPercent` is never used.
63
+ usage:
64
+ reservePercent: 90
65
+ biasPercent: 95
66
+ exhaustedPercent: 100
@@ -7,8 +7,12 @@
7
7
  // (diff.mjs, ls-files.mjs, worktree-*.mjs, ...), and a caller folds the spawn result it gets back with the pure
8
8
  // helpers of scripts/lib/git.mjs (gitResultOf, gitOutputOf).
9
9
  import { assertMutationFence } from '../../lib/mutation-fence.mjs';
10
+ import fs from 'node:fs';
11
+ import os from 'node:os';
12
+ import path from 'node:path';
10
13
  import { spawnSync } from 'node:child_process';
11
14
  import { withoutGitLocalEnv } from '../../lib/git.mjs';
15
+ import { withTempEnv } from '../../../engine/temp-root.mjs';
12
16
 
13
17
  /**
14
18
  * Spawn `file` (the git binary) once with `args`: utf8 text, a hidden window, never a shell.
@@ -20,7 +24,8 @@ export const gitSpawn = (file, args, options = {}) => {
20
24
  assertMutationFence({ kind: 'git-effect', args });
21
25
  const spawn = { encoding: 'utf8', windowsHide: true, ...options };
22
26
  if (spawn.encoding === 'buffer') spawn.encoding = null;
23
- return spawnSync(file, args, spawn);
27
+ // tempChildEnv copies the env (node adds NODE_V8_COVERAGE to the env object it is given: a frozen caller env must not throw under coverage) and points TEMP/TMP/TMPDIR at the temp root
28
+ return spawnSync(file, args, withTempEnv(spawn));
24
29
  };
25
30
 
26
31
  /**
@@ -42,3 +47,31 @@ export const gitRunner = (git = null) => (args, opts = {}) => {
42
47
  const r = git ? git(args, opts) : runGit(args, { timeout: 300_000, maxBuffer: 64 * 1024 * 1024, ...opts });
43
48
  return { ok: r?.ok ?? (!r?.error && r?.status === 0), stdout: String(r?.stdout ?? r?.out ?? '').trim(), stderr: String(r?.stderr ?? r?.err ?? r?.error?.message ?? r?.error ?? '').trim() };
44
49
  };
50
+
51
+ let cachedGit;
52
+ const gitPathOf = ({ env, platform, home, exists }) => {
53
+ const p = platform === 'win32' ? path.win32 : path.posix;
54
+ const pathKey = Object.keys(env).find((name) => name.toLowerCase() === 'path');
55
+ const shim = p.join(home, '.starci', 'bin');
56
+ const same = (a, b) => (platform === 'win32' ? p.resolve(a).toLowerCase() === p.resolve(b).toLowerCase() : p.resolve(a) === p.resolve(b));
57
+ const binary = platform === 'win32' ? 'git.exe' : 'git';
58
+ for (const raw of String(pathKey ? env[pathKey] : '').split(platform === 'win32' ? ';' : ':')) {
59
+ const dir = raw.replace(/^"(.*)"$/, '$1');
60
+ const file = dir && p.isAbsolute(dir) && !same(dir, shim) ? p.join(dir, binary) : null;
61
+ if (file && exists(file)) return file;
62
+ }
63
+ return null;
64
+ };
65
+ /**
66
+ * The absolute path of the real git binary, or null: the first PATH directory holding git (git.exe on Windows) that is not
67
+ * the StarCi shim directory <home>/.starci/bin (a seat's guard shim is never the git the runtime itself runs). A bare name is never
68
+ * spawned, so a writable directory cannot be reached by name lookup at spawn time. Resolved once per process; options (env,
69
+ * platform, home, exists) are for a spec and bypass the cache.
70
+ */
71
+ export const gitExecutable = (options = null) => {
72
+ if (!options && cachedGit !== undefined) return cachedGit;
73
+ const { env = process.env, platform = process.platform, home = os.homedir(), exists = (file) => fs.statSync(file, { throwIfNoEntry: false })?.isFile() === true } = options ?? {};
74
+ const found = gitPathOf({ env, platform, home, exists });
75
+ if (!options) cachedGit = found;
76
+ return found;
77
+ };
@@ -36,9 +36,9 @@ export function slotAdmitsFile(resolver, file) {
36
36
  if (classified.status === 'no-slot' || classified.status === 'ambiguous' || !classified.slot) return null;
37
37
  const slot = resolver.slot(classified.slot);
38
38
  const relative = relativeToRoot(classified.path, classified.root);
39
- const hit = matchersOf([...(slot.requires ?? []), ...(slot.allows ?? [])], classified.bindings).find(matcher => matcher.expression.test(relative));
40
- const forbidden = matchersOf(slot.forbids, classified.bindings).find(matcher => matcher.expression.test(relative) || matcher.expression.test(`${relative}/`));
41
- return Boolean(hit) && !forbidden;
39
+ const hit = matchersOf([...(slot.requires ?? []), ...(slot.allows ?? [])], classified.bindings).some(matcher => matcher.expression.test(relative));
40
+ const forbidden = matchersOf(slot.forbids, classified.bindings).some(matcher => matcher.expression.test(relative) || matcher.expression.test(`${relative}/`));
41
+ return hit && !forbidden;
42
42
  }
43
43
 
44
44
  /**
@@ -1,19 +1,20 @@
1
1
  // ast-walks.mjs - AST walks the architecture checkers share (moved out of typescript.mjs, which keeps the program loading and module resolution).
2
2
  import { canonical, isInside } from './config.mjs';
3
- import { sourceLocation } from '../../lib/ts-ast.mjs';
3
+ import { hasAnyFlag, isConstVariable, sourceLocation } from '../../lib/ts-ast.mjs';
4
4
  import { UNPROVEN_FRAMEWORK, isUnshadowedCommonJsRequire, relativePath, unwrapExpression } from './typescript.mjs';
5
5
 
6
6
  /** `expression` unwrapped by the caller's own predicate set (a predicate absent on an older ts reads as never). */
7
7
  export function unwrapEach(ts, expression, predicates) {
8
- while (expression && predicates.some((predicate) => predicate?.(expression))) expression = expression.expression;
9
- return expression;
8
+ let current = expression;
9
+ while (current && predicates.some((predicate) => predicate?.(current))) current = current.expression;
10
+ return current;
10
11
  }
11
12
 
12
13
  /** `value` with every TS alias hop resolved (a re-exported import reads as its target symbol). */
13
- export function normalizedSymbolValue(ts, checker, value) {
14
- let symbol = value ?? null;
14
+ export function normalizedSymbolValue(ts, checker, value = null) {
15
+ let symbol = value;
15
16
  const seen = new Set();
16
- while (symbol && (symbol.flags & ts.SymbolFlags.Alias) && !seen.has(symbol)) {
17
+ while (symbol && hasAnyFlag(symbol.flags, ts.SymbolFlags.Alias) && !seen.has(symbol)) {
17
18
  seen.add(symbol);
18
19
  const target = checker.getAliasedSymbol(symbol);
19
20
  if (!target || target === symbol) break;
@@ -48,7 +49,7 @@ export function valueSymbol(ts, checker, node, seen = new Set(), { unselected =
48
49
  seen.add(symbol);
49
50
  const declarations = symbol.getDeclarations?.() ?? [];
50
51
  if (declarations.length === 1 && ts.isVariableDeclaration(declarations[0]) && declarations[0].initializer
51
- && (ts.getCombinedNodeFlags(declarations[0].parent) & ts.NodeFlags.Const)) {
52
+ && isConstVariable(ts, declarations[0])) {
52
53
  return valueSymbol(ts, checker, declarations[0].initializer, seen, { unselected });
53
54
  }
54
55
  return symbol;
@@ -102,50 +103,59 @@ export function anyDescendant(ts, node, match) {
102
103
  * selected node's symbols to a kind (default `targets.get(symbol)`); `shorthandOf` (when set) also resolves
103
104
  * shorthand-property declarations and descends property assignments; `newExpression` descends `new X()` callees.
104
105
  */
106
+ const addKinds = (target, source) => { for (const kind of source) target.add(kind); };
107
+
108
+ const kindsFromReturns = (ts, checker, declaration, targets, options) => {
109
+ const kinds = new Set();
110
+ for (const returned of returnedExpressions(ts, declaration)) addKinds(kinds, tracedFrameworkKinds(ts, checker, returned, targets, options));
111
+ return kinds;
112
+ };
113
+
114
+ const kindsFromDeclaration = (ts, checker, declaration, targets, options, shorthandOf) => {
115
+ const kinds = new Set();
116
+ if (ts.isVariableDeclaration(declaration) && declaration.initializer)
117
+ addKinds(kinds, tracedFrameworkKinds(ts, checker, declaration.initializer, targets, options));
118
+ if (shorthandOf && ts.isShorthandPropertyAssignment(declaration)) {
119
+ const target = normalizedSymbolValue(ts, checker, checker.getShorthandAssignmentValueSymbol?.(declaration));
120
+ const kind = shorthandOf(ts, checker, target, targets);
121
+ if (kind) kinds.add(kind);
122
+ }
123
+ if (shorthandOf && ts.isPropertyAssignment(declaration))
124
+ addKinds(kinds, tracedFrameworkKinds(ts, checker, declaration.initializer, targets, options));
125
+ addKinds(kinds, kindsFromReturns(ts, checker, declaration, targets, options));
126
+ return kinds;
127
+ };
128
+
129
+ const kindsFromSymbol = (ts, checker, symbol, targets, seen, options) => {
130
+ const nextSeen = new Set(seen).add(symbol);
131
+ const declarationOptions = { ...options, seen: nextSeen };
132
+ const kinds = new Set();
133
+ for (const declaration of symbol.getDeclarations?.() ?? [])
134
+ addKinds(kinds, kindsFromDeclaration(ts, checker, declaration, targets, declarationOptions, options.shorthandOf));
135
+ return kinds;
136
+ };
137
+
138
+ const defaultDirectOf = (ts, checker, { symbol, targets }) => targets.get(symbol) ?? null;
139
+
105
140
  export function tracedFrameworkKinds(ts, checker, expression, targets,
106
141
  { seen = new Set(), depth = 0, directOf = null, shorthandOf = null, newExpression = false } = {}) {
107
142
  if (!expression) return new Set();
108
143
  if (depth > 10) return new Set([UNPROVEN_FRAMEWORK]);
109
- expression = unwrapExpression(ts, expression);
110
- const selected = selectedNode(ts, expression);
144
+ const unwrapped = unwrapExpression(ts, expression);
145
+ const selected = selectedNode(ts, unwrapped);
111
146
  const symbol = selected ? normalizedSymbol(ts, checker, selected) : null;
112
- const direct = symbol ? (directOf ?? ((ts2, c, { symbol: s, targets: t }) => t.get(s) ?? null))(ts, checker, { symbol, selected, targets }) : null;
147
+ const direct = symbol ? (directOf ?? defaultDirectOf)(ts, checker, { symbol, selected, targets }) : null;
113
148
  if (direct) return new Set([direct]);
114
- const opts = { depth: depth + 1, directOf, shorthandOf, newExpression };
115
- if (ts.isArrowFunction(expression) || ts.isFunctionExpression(expression)) {
116
- const kinds = new Set();
117
- for (const returned of returnedExpressions(ts, expression)) {
118
- for (const kind of tracedFrameworkKinds(ts, checker, returned, targets, { ...opts, seen })) kinds.add(kind);
119
- }
120
- return kinds;
121
- }
122
- if (ts.isCallExpression(expression)) {
123
- const kinds = tracedFrameworkKinds(ts, checker, expression.expression, targets, { ...opts, seen });
149
+ const options = { depth: depth + 1, directOf, shorthandOf, newExpression };
150
+ if (ts.isArrowFunction(unwrapped) || ts.isFunctionExpression(unwrapped)) return kindsFromReturns(ts, checker, unwrapped, targets, { ...options, seen });
151
+ if (ts.isCallExpression(unwrapped)) {
152
+ const kinds = tracedFrameworkKinds(ts, checker, unwrapped.expression, targets, { ...options, seen });
124
153
  if (kinds.size) return kinds;
125
154
  }
126
- if (newExpression && ts.isNewExpression(expression)) {
127
- return tracedFrameworkKinds(ts, checker, expression.expression, targets, { ...opts, seen });
128
- }
155
+ if (newExpression && ts.isNewExpression(unwrapped))
156
+ return tracedFrameworkKinds(ts, checker, unwrapped.expression, targets, { ...options, seen });
129
157
  if (!symbol || seen.has(symbol)) return new Set();
130
- const nextSeen = new Set(seen).add(symbol);
131
- const kinds = new Set();
132
- for (const declaration of symbol.getDeclarations?.() ?? []) {
133
- if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
134
- for (const kind of tracedFrameworkKinds(ts, checker, declaration.initializer, targets, { ...opts, seen: nextSeen })) kinds.add(kind);
135
- }
136
- if (shorthandOf && ts.isShorthandPropertyAssignment(declaration)) {
137
- const target = normalizedSymbolValue(ts, checker, checker.getShorthandAssignmentValueSymbol?.(declaration));
138
- const kind = shorthandOf(ts, checker, target, targets);
139
- if (kind) kinds.add(kind);
140
- }
141
- if (shorthandOf && ts.isPropertyAssignment(declaration)) {
142
- for (const kind of tracedFrameworkKinds(ts, checker, declaration.initializer, targets, { ...opts, seen: nextSeen })) kinds.add(kind);
143
- }
144
- for (const returned of returnedExpressions(ts, declaration)) {
145
- for (const kind of tracedFrameworkKinds(ts, checker, returned, targets, { ...opts, seen: nextSeen })) kinds.add(kind);
146
- }
147
- }
148
- return kinds;
158
+ return kindsFromSymbol(ts, checker, symbol, targets, seen, options);
149
159
  }
150
160
 
151
161
  /** 'unproven framework' | 'multiple framework' | the one kind | null, for a decorator whose expression constructs. */
@@ -153,7 +163,8 @@ export function constructedDecoratorKind(ts, checker, decorator, targets, traceO
153
163
  const callee = ts.isCallExpression(decorator.expression) ? decorator.expression.expression : decorator.expression;
154
164
  const kinds = tracedFrameworkKinds(ts, checker, callee, targets, traceOpts);
155
165
  if (kinds.has(UNPROVEN_FRAMEWORK)) return 'unproven framework';
156
- return kinds.size === 1 ? [...kinds][0] : kinds.size ? 'multiple framework' : null;
166
+ if (kinds.size === 1) return [...kinds][0];
167
+ return kinds.size ? 'multiple framework' : null;
157
168
  }
158
169
 
159
170
  /** The target a decorator resolves to through a `let`/`var` alias's single initializer, or null. */
@@ -163,7 +174,7 @@ export function mutableDecoratorKind(ts, checker, decorator, targets) {
163
174
  const symbol = selected ? normalizedSymbol(ts, checker, selected) : null;
164
175
  const declarations = symbol?.getDeclarations?.() ?? [];
165
176
  if (declarations.length !== 1 || !ts.isVariableDeclaration(declarations[0]) || !declarations[0].initializer
166
- || (ts.getCombinedNodeFlags(declarations[0].parent) & ts.NodeFlags.Const)) return null;
177
+ || isConstVariable(ts, declarations[0])) return null;
167
178
  return targets.get(valueSymbol(ts, checker, declarations[0].initializer)) ?? null;
168
179
  }
169
180
 
@@ -178,11 +189,12 @@ export function decoratorCallee(ts, decorator) {
178
189
  * not resolve. Null when the statement names no static specifier.
179
190
  */
180
191
  export function moduleExportsOf(ts, checker, statement) {
181
- const moduleSpecifier = (ts.isImportDeclaration(statement) || ts.isExportDeclaration(statement))
182
- && statement.moduleSpecifier && ts.isStringLiteralLike(statement.moduleSpecifier) ? statement.moduleSpecifier : null;
183
- const importEquals = ts.isImportEqualsDeclaration(statement) && ts.isExternalModuleReference(statement.moduleReference)
184
- && statement.moduleReference.expression && ts.isStringLiteralLike(statement.moduleReference.expression)
185
- ? statement.moduleReference.expression : null;
192
+ let moduleSpecifier = null;
193
+ if ((ts.isImportDeclaration(statement) || ts.isExportDeclaration(statement))
194
+ && statement.moduleSpecifier && ts.isStringLiteralLike(statement.moduleSpecifier)) moduleSpecifier = statement.moduleSpecifier;
195
+ let importEquals = null;
196
+ if (ts.isImportEqualsDeclaration(statement) && ts.isExternalModuleReference(statement.moduleReference)
197
+ && statement.moduleReference.expression && ts.isStringLiteralLike(statement.moduleReference.expression)) importEquals = statement.moduleReference.expression;
186
198
  const specifierNode = moduleSpecifier ?? importEquals;
187
199
  const specifier = specifierNode?.text;
188
200
  if (specifier == null) return null;
@@ -208,7 +220,8 @@ export function programSourcesOf(context, checker, localFiles, { root = null } =
208
220
  * The CommonJS require() caveat every framework scan reports: each `require('<specifier>')` `accepted` pushes
209
221
  * `${filePath} uses a CommonJS <label ?? specifier> <detail>` (the import-shape reasoning cannot prove that binding).
210
222
  */
211
- export function commonJsRequireReasons(ts, checker, sourceFile, accepted, reasons, filePath, detail, label = null) {
223
+ export function commonJsRequireReasons(ts, checker, sourceFile, accepted, reasons, filePath, detail) {
224
+ const label = arguments[7] ?? null;
212
225
  const visit = node => {
213
226
  if (ts.isCallExpression(node) && isUnshadowedCommonJsRequire(ts, checker, node.expression)
214
227
  && node.arguments.length === 1 && ts.isStringLiteralLike(node.arguments[0]) && accepted(node.arguments[0].text)) {
@@ -0,0 +1,96 @@
1
+ import { readTextFile } from '../../lib/read-text.mjs';
2
+
3
+ // Owner ruling 2026-09-29 (layout 2026-09-30): integration, e2e and contract run MANUALLY only. No hook, default typecheck,
4
+ // coverage run or automatic CI trigger may include those trees or run those projects. Linting the e2e files is not running them: ESLint reads them
5
+ // as syntax in the one repository-wide lint run (the factory's e2e block), so no `lint:e2e` command exists to judge.
6
+ const E2E_COMMAND = new RegExp([
7
+ String.raw`\btest:(?:e2e|integration|contract)\b|`,
8
+ String.raw`\btypecheck:tests\b|`,
9
+ String.raw`--selectProjects\s+(?:e2e|integration|contract)\b|`,
10
+ String.raw`src\/tests\/(?:world|integration|e2e|contract)\b|`,
11
+ String.raw`jest[^\n|&;]*(?:e2e|integration|contract)`,
12
+ ].join(''), 'u');
13
+ const UNIT_RUN_SCRIPTS = ['test', 'test:unit', 'test:ci', 'test:affected', 'test:coverage', 'test:cov'];
14
+
15
+ const runsE2e = text => E2E_COMMAND.test(String(text).replace(/--ignore-pattern[= ]+(?:"[^"]*"|'[^']*'|\S+)/gu, ''));
16
+ const withoutComments = text => text.split('\n').filter(line => !/^\s*#/u.test(line)).join('\n');
17
+
18
+ function inspectHooks(state) {
19
+ for (const hook of ['.husky/pre-commit', '.husky/pre-push']) {
20
+ const text = readTextFile(state.root, hook);
21
+ if (text === null) continue;
22
+ const body = withoutComments(text);
23
+ if (runsE2e(body)) state.finding(state.rule, hook, `${hook} runs integration, e2e or contract. They are manual only; hooks run unit, lint and typecheck.`);
24
+ for (const match of body.matchAll(/npm\s+run\s+([\w:.-]+)/gu)) state.pending.push(match[1]);
25
+ }
26
+ }
27
+
28
+ function inspectLintStaged(state) {
29
+ const lintStaged = state.pkg?.['lint-staged'];
30
+ if (lintStaged && runsE2e(Object.values(lintStaged).flat().join('\n')))
31
+ state.finding(state.rule, 'package.json', 'lint-staged runs an integration, e2e or contract command. They are manual only.');
32
+ }
33
+
34
+ function inspectHookScripts(state) {
35
+ for (const name of state.pending) {
36
+ if (state.called.has(name)) continue;
37
+ state.called.add(name);
38
+ const command = state.scripts[name];
39
+ if (typeof command !== 'string') continue;
40
+ if (runsE2e(command)) state.finding(state.rule, 'package.json', `Script ${name} is run by a husky hook and touches integration, e2e or contract. They are manual only.`);
41
+ for (const match of command.matchAll(/npm\s+run\s+([\w:.-]+)/gu)) state.pending.push(match[1]);
42
+ }
43
+ }
44
+
45
+ function inspectUnitGates(state) {
46
+ if (!state.tree.hasFile(state.jestConfig)) return;
47
+ for (const name of UNIT_RUN_SCRIPTS) {
48
+ const command = state.scripts[name];
49
+ if (typeof command === 'string' && /\bjest\b/u.test(command) && !/--selectProjects\s+unit\b/u.test(command))
50
+ state.finding(state.rule, 'package.json', `Script ${name} runs jest without --selectProjects unit and would run the integration, e2e or contract project.`);
51
+ }
52
+ const jestText = readTextFile(state.root, state.jestConfig) ?? '';
53
+ if (/collectCoverageFrom/u.test(jestText) && !/!src\/tests\/(?:\*\*|e2e)/u.test(jestText))
54
+ state.finding(state.rule, state.jestConfig, 'collectCoverageFrom must exclude src/tests/** so no integration, e2e or contract file counts toward coverage.');
55
+ }
56
+
57
+ function inspectWorkflows(state) {
58
+ for (const file of state.tree.files().filter(entry => /^\.github\/workflows\/[^/]+\.ya?ml$/u.test(entry))) {
59
+ const text = readTextFile(state.root, file);
60
+ if (text === null) continue;
61
+ const trigger = /^on:.*(?:\n(?:[ \t].*)?$)*/mu.exec(text)?.[0] ?? '';
62
+ if (!/\b(?:push|pull_request)\b/u.test(trigger)) continue;
63
+ if (runsE2e(withoutComments(text)))
64
+ state.finding(state.rule, file, `${file} runs e2e on push or pull_request. Move the e2e job to its own workflow with on: workflow_dispatch only.`);
65
+ }
66
+ }
67
+
68
+ /** Check hooks, transitive hook scripts, unit scripts, coverage and automatic workflows. */
69
+ export function e2eInAutomaticGate({ root, tree, jestConfig, finding }) {
70
+ const rule = 'HFS_E2E_IN_AUTOMATIC_GATE';
71
+ let pkg = null;
72
+ try { pkg = JSON.parse(readTextFile(root, 'package.json') ?? ''); } catch { /* the package checks own invalid JSON */ }
73
+ const state = { root, tree, jestConfig, finding, rule, pkg, scripts: pkg?.scripts ?? {}, pending: [], called: new Set() };
74
+ inspectHooks(state);
75
+ inspectLintStaged(state);
76
+ inspectHookScripts(state);
77
+ inspectUnitGates(state);
78
+ inspectWorkflows(state);
79
+ }
80
+
81
+ /** The be side's default tsconfig excludes the world, integration, e2e and contract trees (they are checked by src/tests/tsconfig.json). */
82
+ export function testTreesOutOfDefaultProgram({ root, tree, finding }) {
83
+ const rule = 'HFS_E2E_IN_AUTOMATIC_GATE';
84
+ const tsconfigText = readTextFile(root, 'tsconfig.json');
85
+ let tsconfig = null;
86
+ try { tsconfig = tsconfigText === null ? null : JSON.parse(tsconfigText); } catch { /* the typecheck itself owns parsing */ }
87
+ if (tsconfig) {
88
+ const excludedText = JSON.stringify(tsconfig.exclude ?? []);
89
+ const excludesTestTrees = ['world', 'integration', 'e2e', 'contract'].every(tree => excludedText.includes(`src/tests/${tree}`));
90
+ const defaultAll = tsconfig.include === undefined && tsconfig.files === undefined;
91
+ const files = tree.files();
92
+ if (files.some(file => /^src\/tests\/(?:world|integration|e2e|contract)\/.+\.[cm]?tsx?$/u.test(file)) && !excludesTestTrees &&
93
+ (defaultAll || JSON.stringify(tsconfig.include ?? []).includes('src')))
94
+ finding(rule, 'tsconfig.json', 'The default tsconfig includes src/tests/{world,integration,e2e,contract}/**. Exclude those trees and check them with src/tests/tsconfig.json (typecheck:tests).');
95
+ }
96
+ }