@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.
- package/CHANGELOG.md +7 -1
- package/README.md +2 -1
- package/builders.mjs +1 -1
- package/comments.mjs +1 -1
- package/config-owner.mjs +1 -1
- package/index.mjs +2 -0
- package/lib/config.mjs +4 -1
- package/lib/doors.mjs +3 -1
- package/module-shape.mjs +1 -2
- package/package.json +1 -1
- package/resilience.mjs +1 -1
- package/runtime/config.example.yaml +194 -0
- package/runtime/engine/by-code-unit.mjs +12 -0
- package/runtime/engine/config.mjs +222 -239
- package/runtime/engine/invalid-config.mjs +5 -5
- package/runtime/engine/model-config.mjs +90 -0
- package/runtime/engine/orca-config.mjs +3 -1
- package/runtime/engine/release-config.mjs +28 -0
- package/runtime/engine/removed-vocabulary.mjs +28 -0
- package/runtime/engine/resources-config.mjs +52 -0
- package/runtime/engine/runtime-root.mjs +17 -0
- package/runtime/engine/secrets.mjs +35 -31
- package/runtime/engine/sonar-config.mjs +21 -0
- package/runtime/engine/temp-root.mjs +32 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +14 -14
- package/runtime/knowledge/hfs/rules.yaml +161 -65
- package/runtime/knowledge/hfs/slots.yaml +11 -14
- package/runtime/modules/kernel/removed-vocabulary.yaml +268 -0
- package/runtime/modules/models/registry.yaml +25 -359
- package/runtime/modules/models/runtimes.yaml +126 -265
- package/runtime/modules/models/tiers.yaml +66 -0
- package/runtime/scripts/api/git/lib.mjs +34 -1
- package/runtime/scripts/hfs/allows.mjs +3 -3
- package/runtime/scripts/hfs/architecture/ast-walks.mjs +63 -50
- package/runtime/scripts/hfs/architecture/automatic-gates.mjs +96 -0
- package/runtime/scripts/hfs/architecture/backend.mjs +261 -175
- package/runtime/scripts/hfs/architecture/background-unowned.mjs +69 -34
- package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +55 -54
- package/runtime/scripts/hfs/architecture/clones.mjs +179 -106
- package/runtime/scripts/hfs/architecture/config-unread.mjs +5 -22
- package/runtime/scripts/hfs/architecture/config.mjs +95 -80
- package/runtime/scripts/hfs/architecture/connection-map.mjs +233 -141
- package/runtime/scripts/hfs/architecture/constructor-deps.mjs +15 -9
- package/runtime/scripts/hfs/architecture/context-coupling.mjs +76 -52
- package/runtime/scripts/hfs/architecture/context-map.mjs +215 -141
- package/runtime/scripts/hfs/architecture/context-owner.mjs +39 -30
- package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +31 -17
- package/runtime/scripts/hfs/architecture/context-transaction.mjs +38 -25
- package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +86 -40
- package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +342 -0
- package/runtime/scripts/hfs/architecture/contracts.mjs +263 -455
- package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +43 -34
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +201 -154
- package/runtime/scripts/hfs/architecture/default-deny.mjs +118 -93
- package/runtime/scripts/hfs/architecture/doc-language.mjs +2 -1
- package/runtime/scripts/hfs/architecture/entrypoint.mjs +45 -30
- package/runtime/scripts/hfs/architecture/error-codes.mjs +19 -12
- package/runtime/scripts/hfs/architecture/error-masked.mjs +39 -26
- package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +2 -1
- package/runtime/scripts/hfs/architecture/feature-shape.mjs +26 -20
- package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
- package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +190 -0
- package/runtime/scripts/hfs/architecture/frontend-routing.mjs +186 -0
- package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +311 -0
- package/runtime/scripts/hfs/architecture/frontend-world.mjs +328 -0
- package/runtime/scripts/hfs/architecture/frontend.mjs +109 -829
- package/runtime/scripts/hfs/architecture/hfs-graph.mjs +14 -2
- package/runtime/scripts/hfs/architecture/hfs.mjs +120 -288
- package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +46 -26
- package/runtime/scripts/hfs/architecture/i18n-keys.mjs +114 -76
- package/runtime/scripts/hfs/architecture/index.mjs +164 -104
- package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +33 -28
- package/runtime/scripts/hfs/architecture/machine-ast.mjs +180 -144
- package/runtime/scripts/hfs/architecture/managed-scripts.mjs +8 -2
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +150 -101
- package/runtime/scripts/hfs/architecture/next-data-contract.mjs +51 -43
- package/runtime/scripts/hfs/architecture/next-data-key.mjs +338 -0
- package/runtime/scripts/hfs/architecture/next-data.mjs +179 -427
- package/runtime/scripts/hfs/architecture/owners.mjs +27 -13
- package/runtime/scripts/hfs/architecture/package-shape.mjs +51 -23
- package/runtime/scripts/hfs/architecture/presentation.mjs +165 -0
- package/runtime/scripts/hfs/architecture/reachability.mjs +127 -61
- package/runtime/scripts/hfs/architecture/register-once.mjs +112 -63
- package/runtime/scripts/hfs/architecture/registration.mjs +187 -110
- package/runtime/scripts/hfs/architecture/required-files.mjs +121 -70
- package/runtime/scripts/hfs/architecture/route-files-thin.mjs +83 -58
- package/runtime/scripts/hfs/architecture/schema-owner.mjs +182 -108
- package/runtime/scripts/hfs/architecture/source-names-shape.mjs +318 -0
- package/runtime/scripts/hfs/architecture/source-names.mjs +83 -248
- package/runtime/scripts/hfs/architecture/sql-owner.mjs +175 -118
- package/runtime/scripts/hfs/architecture/sql-returning.mjs +39 -28
- package/runtime/scripts/hfs/architecture/sql-tokens.mjs +302 -172
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +1 -1
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +55 -45
- package/runtime/scripts/hfs/architecture/supabase-results.mjs +182 -0
- package/runtime/scripts/hfs/architecture/supabase-tables.mjs +20 -12
- package/runtime/scripts/hfs/architecture/supabase.mjs +191 -263
- package/runtime/scripts/hfs/architecture/symbols.mjs +175 -99
- package/runtime/scripts/hfs/architecture/test-world-files.mjs +82 -41
- package/runtime/scripts/hfs/architecture/tiers.mjs +120 -69
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +84 -57
- package/runtime/scripts/hfs/architecture/type-context.mjs +282 -0
- package/runtime/scripts/hfs/architecture/typescript.mjs +116 -276
- package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +84 -62
- package/runtime/scripts/hfs/declaration-shape.mjs +77 -36
- package/runtime/scripts/hfs/declaration-slots.mjs +1 -1
- package/runtime/scripts/hfs/edition-slots.mjs +5 -3
- package/runtime/scripts/hfs/linear-text.mjs +30 -0
- package/runtime/scripts/hfs/manifest-shape.mjs +164 -63
- package/runtime/scripts/hfs/path-findings.mjs +58 -38
- package/runtime/scripts/hfs/project-graph.mjs +4 -1
- package/runtime/scripts/hfs/repo-identity.mjs +21 -4
- package/runtime/scripts/hfs/rule-catalog.mjs +194 -0
- package/runtime/scripts/hfs/rule-params-shape.mjs +33 -27
- package/runtime/scripts/hfs/rules/fe-no-tests.mjs +36 -16
- package/runtime/scripts/hfs/rules/secrets.mjs +4 -3
- package/runtime/scripts/hfs/slot-app-view.mjs +102 -0
- package/runtime/scripts/hfs/slot-classify.mjs +65 -0
- package/runtime/scripts/hfs/slot-errors.mjs +11 -0
- package/runtime/scripts/hfs/slot-imports.mjs +76 -0
- package/runtime/scripts/hfs/slot-manifest-shape.mjs +73 -0
- package/runtime/scripts/hfs/slot-match.mjs +153 -0
- package/runtime/scripts/hfs/slot-path.mjs +8 -0
- package/runtime/scripts/hfs/slot-required.mjs +76 -0
- package/runtime/scripts/hfs/slot-semantic-problems.mjs +148 -0
- package/runtime/scripts/hfs/slot-side-problems.mjs +28 -0
- package/runtime/scripts/hfs/slots.mjs +74 -695
- package/runtime/scripts/hfs/trailing-slashes.mjs +8 -0
- package/runtime/scripts/hfs/typescript-programs.mjs +6 -4
- package/runtime/scripts/hfs/view.mjs +1 -1
- package/runtime/scripts/lib/dockerfile.mjs +56 -33
- package/runtime/scripts/lib/git.mjs +2 -2
- package/runtime/scripts/lib/i18n.mjs +3 -2
- package/runtime/scripts/lib/language.mjs +3 -3
- package/runtime/scripts/lib/list.mjs +8 -1
- package/runtime/scripts/lib/path-key.mjs +38 -5
- package/runtime/scripts/lib/regex.mjs +1 -1
- package/runtime/scripts/lib/secret-patterns.mjs +6 -2
- package/runtime/scripts/lib/sonar-syntax-rules.mjs +225 -0
- package/runtime/scripts/lib/sops-envelope.mjs +95 -48
- package/runtime/scripts/lib/stack-services.mjs +1 -2
- package/runtime/scripts/lib/ts-ast.mjs +9 -0
- package/runtime/scripts/lib/walk.mjs +6 -1
- package/runtime/scripts/lib/yaml-cached.mjs +14 -0
- package/schema-authority.mjs +1 -1
- package/service-deps.mjs +1 -1
- package/sonar-parity.mjs +19 -0
- package/suppression.mjs +2 -6
- 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
|
-
|
|
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).
|
|
40
|
-
const forbidden = matchersOf(slot.forbids, classified.bindings).
|
|
41
|
-
return
|
|
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
|
-
|
|
9
|
-
|
|
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
|
|
14
|
+
export function normalizedSymbolValue(ts, checker, value = null) {
|
|
15
|
+
let symbol = value;
|
|
15
16
|
const seen = new Set();
|
|
16
|
-
while (symbol && (symbol.flags
|
|
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
|
|
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
|
-
|
|
110
|
-
const selected = selectedNode(ts,
|
|
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 ??
|
|
147
|
+
const direct = symbol ? (directOf ?? defaultDirectOf)(ts, checker, { symbol, selected, targets }) : null;
|
|
113
148
|
if (direct) return new Set([direct]);
|
|
114
|
-
const
|
|
115
|
-
if (ts.isArrowFunction(
|
|
116
|
-
|
|
117
|
-
|
|
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(
|
|
127
|
-
return tracedFrameworkKinds(ts, checker,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
|
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
|
+
}
|