project-tiny-context-harness 0.8.6 → 0.8.8

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.
@@ -0,0 +1,165 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import { pathExists, readText, writeTextIfChanged } from "./fs.js";
4
+ import { harnessConfigPath } from "./harness-root.js";
5
+ import { DEFAULT_MODULARITY_LINE_LIMIT, hasLegacyHeuristicOnlyWaiverRisk, isLifecycleCompleteModularityWaiverConfig, isLikelyGeneratedSourceContent, } from "./modularity.js";
6
+ import { toPosix } from "./source-files.js";
7
+ import { parseYaml, stringifyYaml } from "./yaml.js";
8
+ const MIGRATION_ID = "modularity-capability-waiver-cleanup";
9
+ const INTRODUCED_IN = "0.8.8";
10
+ const DESCRIPTION = "Remove scoped modularity waivers that existed only for unsupported cross-language heuristic metrics.";
11
+ const SCOPE = "<harnessRoot>/config.yaml modularity.waivers";
12
+ export const modularityCapabilityWaiverMigration = {
13
+ id: MIGRATION_ID,
14
+ introducedIn: INTRODUCED_IN,
15
+ description: DESCRIPTION,
16
+ scope: SCOPE,
17
+ risk: "safe",
18
+ manualMessage: "Remove only the waiver identified as legacy-heuristic-only; retain any waiver that still covers physical-line or supported language metrics.",
19
+ detect: detectModularityCapabilityWaivers,
20
+ apply: migrateModularityCapabilityWaivers,
21
+ verify: verifyModularityCapabilityWaivers,
22
+ };
23
+ async function detectModularityCapabilityWaivers(projectRoot) {
24
+ const config = await readRawConfig(projectRoot);
25
+ if (!config) {
26
+ return [];
27
+ }
28
+ const candidates = await obsoleteWaiverIndexes(projectRoot, config.value);
29
+ return candidates.map(({ index, relativePath }) => ({
30
+ id: MIGRATION_ID,
31
+ introducedIn: INTRODUCED_IN,
32
+ description: DESCRIPTION,
33
+ scope: SCOPE,
34
+ status: "safe_pending",
35
+ path: `${config.relativePath}#modularity.waivers[${index}]`,
36
+ message: `${relativePath} exceeded only metrics from the retired cross-language JS heuristic; upgrade can remove this now-inapplicable waiver without changing supported line-risk coverage.`,
37
+ }));
38
+ }
39
+ async function migrateModularityCapabilityWaivers(projectRoot, _root, report) {
40
+ const config = await readRawConfig(projectRoot);
41
+ if (!config) {
42
+ report.skipped.push(MIGRATION_ID);
43
+ return;
44
+ }
45
+ const candidates = await obsoleteWaiverIndexes(projectRoot, config.value);
46
+ if (candidates.length === 0) {
47
+ report.skipped.push(MIGRATION_ID);
48
+ return;
49
+ }
50
+ const modularity = row(config.value.modularity);
51
+ const waivers = Array.isArray(modularity?.waivers) ? modularity.waivers : [];
52
+ const removed = new Set(candidates.map((candidate) => candidate.index));
53
+ const retained = waivers.filter((_waiver, index) => !removed.has(index));
54
+ if (retained.length > 0) {
55
+ modularity.waivers = retained;
56
+ }
57
+ else {
58
+ delete modularity.waivers;
59
+ }
60
+ if (await writeTextIfChanged(config.absolutePath, stringifyYaml(config.value))) {
61
+ report.changed.push(`${config.relativePath}#modularity.waivers removed=${removed.size}`);
62
+ }
63
+ else {
64
+ report.skipped.push(MIGRATION_ID);
65
+ }
66
+ }
67
+ async function verifyModularityCapabilityWaivers(projectRoot) {
68
+ const remaining = await detectModularityCapabilityWaivers(projectRoot);
69
+ if (remaining.length > 0) {
70
+ throw new Error("modularity capability waiver cleanup migration verification failed");
71
+ }
72
+ }
73
+ async function obsoleteWaiverIndexes(projectRoot, config) {
74
+ const modularity = row(config.modularity);
75
+ if (!modularity ||
76
+ (modularity.policy ?? "scoped_waivers") !== "scoped_waivers") {
77
+ return [];
78
+ }
79
+ if (modularity.limit !== undefined &&
80
+ (typeof modularity.limit !== "number" ||
81
+ !Number.isInteger(modularity.limit) ||
82
+ modularity.limit <= 0)) {
83
+ return [];
84
+ }
85
+ const lineLimit = typeof modularity.limit === "number"
86
+ ? modularity.limit
87
+ : DEFAULT_MODULARITY_LINE_LIMIT;
88
+ if (!Array.isArray(modularity.waivers)) {
89
+ return [];
90
+ }
91
+ const targets = modularity.waivers.map((value) => {
92
+ const waiver = row(value);
93
+ return safeProjectTarget(projectRoot, waiver?.path);
94
+ });
95
+ const targetCounts = new Map();
96
+ for (const target of targets) {
97
+ if (target) {
98
+ const identity = targetIdentity(target.relativePath);
99
+ targetCounts.set(identity, (targetCounts.get(identity) ?? 0) + 1);
100
+ }
101
+ }
102
+ const candidates = [];
103
+ for (const [index, value] of modularity.waivers.entries()) {
104
+ if (!isLifecycleCompleteModularityWaiverConfig(value)) {
105
+ continue;
106
+ }
107
+ const target = targets[index];
108
+ if (!target ||
109
+ targetCounts.get(targetIdentity(target.relativePath)) !== 1) {
110
+ continue;
111
+ }
112
+ let content;
113
+ try {
114
+ const stat = await fs.stat(target.absolutePath);
115
+ if (!stat.isFile()) {
116
+ continue;
117
+ }
118
+ content = await fs.readFile(target.absolutePath, "utf8");
119
+ }
120
+ catch {
121
+ continue;
122
+ }
123
+ if (isLikelyGeneratedSourceContent(content)) {
124
+ continue;
125
+ }
126
+ if (hasLegacyHeuristicOnlyWaiverRisk(content, target.relativePath, lineLimit)) {
127
+ candidates.push({ index, relativePath: target.relativePath });
128
+ }
129
+ }
130
+ return candidates;
131
+ }
132
+ async function readRawConfig(projectRoot) {
133
+ const relativePath = await harnessConfigPath(projectRoot);
134
+ const absolutePath = path.join(projectRoot, relativePath);
135
+ if (!(await pathExists(absolutePath))) {
136
+ return undefined;
137
+ }
138
+ const value = row(parseYaml(await readText(absolutePath)));
139
+ return value
140
+ ? { absolutePath, relativePath: toPosix(relativePath), value }
141
+ : undefined;
142
+ }
143
+ function safeProjectTarget(projectRoot, value) {
144
+ if (typeof value !== "string" || value.trim().length === 0) {
145
+ return undefined;
146
+ }
147
+ const absolutePath = path.resolve(projectRoot, value);
148
+ const relativePath = toPosix(path.relative(projectRoot, absolutePath));
149
+ if (relativePath === ".." ||
150
+ relativePath.startsWith("../") ||
151
+ path.isAbsolute(relativePath)) {
152
+ return undefined;
153
+ }
154
+ return { absolutePath, relativePath };
155
+ }
156
+ function row(value) {
157
+ return value && typeof value === "object" && !Array.isArray(value)
158
+ ? value
159
+ : undefined;
160
+ }
161
+ function targetIdentity(relativePath) {
162
+ return process.platform === "win32"
163
+ ? relativePath.toLowerCase()
164
+ : relativePath;
165
+ }
@@ -0,0 +1,7 @@
1
+ export interface PythonFunctionMetric {
2
+ symbol: string;
3
+ line: number;
4
+ statements: number;
5
+ branches: number;
6
+ }
7
+ export declare function analyzePythonFunctions(content: string): PythonFunctionMetric[];
@@ -0,0 +1,191 @@
1
+ export function analyzePythonFunctions(content) {
2
+ const code = sanitizePython(content);
3
+ return pythonFunctionBodies(code).map((body) => ({
4
+ symbol: body.symbol,
5
+ line: body.line,
6
+ statements: pythonStatementCount(body.body),
7
+ branches: pythonBranchComplexity(body.body),
8
+ }));
9
+ }
10
+ function sanitizePython(content) {
11
+ let result = "";
12
+ let quote;
13
+ for (let index = 0; index < content.length; index += 1) {
14
+ const character = content[index];
15
+ if (quote) {
16
+ if (character === "\\") {
17
+ result += " ";
18
+ if (index + 1 < content.length) {
19
+ index += 1;
20
+ result += isLineBreak(content[index]) ? content[index] : " ";
21
+ }
22
+ continue;
23
+ }
24
+ if (content.startsWith(quote, index)) {
25
+ result += " ".repeat(quote.length);
26
+ index += quote.length - 1;
27
+ quote = undefined;
28
+ continue;
29
+ }
30
+ result += isLineBreak(character) ? character : " ";
31
+ continue;
32
+ }
33
+ if (character === "#") {
34
+ while (index < content.length && !isLineBreak(content[index])) {
35
+ result += " ";
36
+ index += 1;
37
+ }
38
+ if (index < content.length) {
39
+ result += content[index];
40
+ }
41
+ continue;
42
+ }
43
+ if (content.startsWith("'''", index) || content.startsWith('"""', index)) {
44
+ quote = content.slice(index, index + 3);
45
+ result += " ";
46
+ index += 2;
47
+ continue;
48
+ }
49
+ if (character === "'" || character === '"') {
50
+ quote = character;
51
+ result += " ";
52
+ continue;
53
+ }
54
+ result += character;
55
+ }
56
+ return result;
57
+ }
58
+ function pythonFunctionBodies(code) {
59
+ const lines = code.split(/\r\n|\n|\r/u);
60
+ const bodies = [];
61
+ for (let index = 0; index < lines.length; index += 1) {
62
+ const match = /^(\s*)(?:async\s+)?def\s+([A-Za-z_][A-Za-z0-9_]*)\b/u.exec(lines[index]);
63
+ if (!match) {
64
+ continue;
65
+ }
66
+ const indentation = indentationWidth(match[1]);
67
+ const header = findHeaderEnd(lines, index);
68
+ if (!header) {
69
+ continue;
70
+ }
71
+ const bodyLines = [];
72
+ const inlineBody = lines[header.line].slice(header.colon + 1).trim();
73
+ if (inlineBody) {
74
+ bodyLines.push(inlineBody);
75
+ }
76
+ for (let cursor = header.line + 1; cursor < lines.length; cursor += 1) {
77
+ const line = lines[cursor];
78
+ if (!line.trim()) {
79
+ bodyLines.push(line);
80
+ continue;
81
+ }
82
+ const leading = /^\s*/u.exec(line)?.[0] ?? "";
83
+ if (indentationWidth(leading) <= indentation) {
84
+ break;
85
+ }
86
+ bodyLines.push(line);
87
+ }
88
+ bodies.push({
89
+ symbol: match[2],
90
+ line: index + 1,
91
+ body: bodyLines.join("\n"),
92
+ });
93
+ }
94
+ return bodies;
95
+ }
96
+ function findHeaderEnd(lines, start) {
97
+ let delimiterDepth = 0;
98
+ for (let line = start; line < lines.length; line += 1) {
99
+ for (let column = 0; column < lines[line].length; column += 1) {
100
+ const character = lines[line][column];
101
+ if (character === "(" || character === "[" || character === "{") {
102
+ delimiterDepth += 1;
103
+ }
104
+ else if (character === ")" || character === "]" || character === "}") {
105
+ delimiterDepth = Math.max(0, delimiterDepth - 1);
106
+ }
107
+ else if (character === ":" && delimiterDepth === 0) {
108
+ return { line, colon: column };
109
+ }
110
+ }
111
+ }
112
+ return undefined;
113
+ }
114
+ function pythonStatementCount(body) {
115
+ let statements = 0;
116
+ let delimiterDepth = 0;
117
+ let explicitContinuation = false;
118
+ for (const line of body.split(/\r\n|\n|\r/u)) {
119
+ const trimmed = line.trim();
120
+ if (!trimmed) {
121
+ continue;
122
+ }
123
+ const startsLogicalStatement = delimiterDepth === 0 && !explicitContinuation;
124
+ if (startsLogicalStatement) {
125
+ statements += 1 + topLevelSemicolonCount(line);
126
+ if (hasInlineSuite(line)) {
127
+ statements += 1;
128
+ }
129
+ }
130
+ delimiterDepth = delimiterDepthAfter(line, delimiterDepth);
131
+ explicitContinuation = delimiterDepth === 0 && /\\\s*$/u.test(line);
132
+ }
133
+ return statements;
134
+ }
135
+ function pythonBranchComplexity(body) {
136
+ const branches = body.match(/\b(?:if|elif|for|while|except|case|and|or)\b/gu)?.length ?? 0;
137
+ return 1 + branches;
138
+ }
139
+ function topLevelSemicolonCount(line) {
140
+ let count = 0;
141
+ let depth = 0;
142
+ for (const character of line) {
143
+ if (character === "(" || character === "[" || character === "{") {
144
+ depth += 1;
145
+ }
146
+ else if (character === ")" || character === "]" || character === "}") {
147
+ depth = Math.max(0, depth - 1);
148
+ }
149
+ else if (character === ";" && depth === 0) {
150
+ count += 1;
151
+ }
152
+ }
153
+ return count;
154
+ }
155
+ function hasInlineSuite(line) {
156
+ if (!/^\s*(?:async\s+)?(?:if|elif|else|for|while|try|except|finally|with|match|case)\b/u.test(line)) {
157
+ return false;
158
+ }
159
+ let depth = 0;
160
+ for (let index = 0; index < line.length; index += 1) {
161
+ const character = line[index];
162
+ if (character === "(" || character === "[" || character === "{") {
163
+ depth += 1;
164
+ }
165
+ else if (character === ")" || character === "]" || character === "}") {
166
+ depth = Math.max(0, depth - 1);
167
+ }
168
+ else if (character === ":" && depth === 0) {
169
+ return line.slice(index + 1).trim().length > 0;
170
+ }
171
+ }
172
+ return false;
173
+ }
174
+ function delimiterDepthAfter(line, initialDepth) {
175
+ let depth = initialDepth;
176
+ for (const character of line) {
177
+ if (character === "(" || character === "[" || character === "{") {
178
+ depth += 1;
179
+ }
180
+ else if (character === ")" || character === "]" || character === "}") {
181
+ depth = Math.max(0, depth - 1);
182
+ }
183
+ }
184
+ return depth;
185
+ }
186
+ function indentationWidth(value) {
187
+ return [...value].reduce((width, character) => width + (character === "\t" ? 4 : 1), 0);
188
+ }
189
+ function isLineBreak(character) {
190
+ return character === "\n" || character === "\r";
191
+ }
@@ -1,3 +1,6 @@
1
+ import { type ModularityAnalysisCapability } from "./source-files.js";
2
+ import type { ModularityWaiverConfig } from "./types.js";
3
+ export declare const DEFAULT_MODULARITY_LINE_LIMIT = 300;
1
4
  export interface ModularityCheckOptions {
2
5
  touched?: boolean;
3
6
  base?: string;
@@ -24,13 +27,14 @@ export interface ModularityMetricLocation {
24
27
  line: number;
25
28
  }
26
29
  export interface ModularityMetrics {
27
- maxFunctionStatements: number;
30
+ analysis: ModularityAnalysisCapability;
31
+ maxFunctionStatements: number | null;
28
32
  maxFunctionStatementsLocation?: ModularityMetricLocation;
29
- maxBranchComplexity: number;
33
+ maxBranchComplexity: number | null;
30
34
  maxBranchComplexityLocation?: ModularityMetricLocation;
31
- exports: number;
32
- stateTransitions: number;
33
- responsibilities: string[];
35
+ exports: number | null;
36
+ stateTransitions: number | null;
37
+ responsibilities: string[] | null;
34
38
  }
35
39
  export interface ModularityCheckReport {
36
40
  limit: number;
@@ -48,6 +52,9 @@ export interface ModularityWaiver {
48
52
  trackingIssue: string;
49
53
  expiryCondition: string;
50
54
  }
55
+ export declare function isLifecycleCompleteModularityWaiverConfig(value: unknown): value is ModularityWaiverConfig;
51
56
  export declare function runModularityCheck(projectRoot: string, options: ModularityCheckOptions): Promise<ModularityCheckReport>;
52
57
  export declare function analyzeModularity(content: string, relativePath?: string): ModularityMetrics;
58
+ export declare function hasLegacyHeuristicOnlyWaiverRisk(content: string, relativePath: string, lineLimit: number): boolean;
53
59
  export declare function countPhysicalLines(content: string): number;
60
+ export declare function isLikelyGeneratedSourceContent(content: string): boolean;
@@ -3,10 +3,11 @@ import { promises as fs } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { promisify } from "node:util";
5
5
  import { readConfig } from "./config.js";
6
- import { shouldIncludeCodeFile, toPosix } from "./source-files.js";
6
+ import { analyzePythonFunctions } from "./modularity-python.js";
7
+ import { modularityAnalysisCapability, shouldIncludeCodeFile, toPosix, } from "./source-files.js";
7
8
  const execFileAsync = promisify(execFile);
8
9
  const GIT_MAX_BUFFER = 16 * 1024 * 1024;
9
- const DEFAULT_LINE_LIMIT = 300;
10
+ export const DEFAULT_MODULARITY_LINE_LIMIT = 300;
10
11
  const DEFAULT_MODULARITY_POLICY = "scoped_waivers";
11
12
  const MODULARITY_POLICIES = new Set([
12
13
  DEFAULT_MODULARITY_POLICY,
@@ -34,11 +35,32 @@ const WAIVER_CATEGORIES = new Set([
34
35
  "aggregate_styles",
35
36
  "fixture_snapshot",
36
37
  ]);
38
+ const MODULARITY_WAIVER_REQUIRED_FIELDS = [
39
+ "path",
40
+ "category",
41
+ "owner",
42
+ "introduced_at",
43
+ "reason",
44
+ "tracking_issue",
45
+ "expiry_condition",
46
+ ];
47
+ export function isLifecycleCompleteModularityWaiverConfig(value) {
48
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
49
+ return false;
50
+ }
51
+ const waiver = value;
52
+ if (MODULARITY_WAIVER_REQUIRED_FIELDS.some((field) => typeof waiver[field] !== "string" || waiver[field].trim().length === 0)) {
53
+ return false;
54
+ }
55
+ return (WAIVER_CATEGORIES.has(waiver.category.trim()) &&
56
+ /^\d{4}-\d{2}-\d{2}$/u.test(waiver.introduced_at.trim()) &&
57
+ !Number.isNaN(Date.parse(`${waiver.introduced_at.trim()}T00:00:00Z`)));
58
+ }
37
59
  export async function runModularityCheck(projectRoot, options) {
38
60
  const config = await readConfig(projectRoot);
39
61
  const { limit: configuredLimit, waiverValues, errors: configErrors, } = validateModularityConfig(config.modularity);
40
62
  const { waivers, errors: waiverErrors } = validateWaivers(projectRoot, waiverValues);
41
- const limit = options.limit ?? configuredLimit ?? DEFAULT_LINE_LIMIT;
63
+ const limit = options.limit ?? configuredLimit ?? DEFAULT_MODULARITY_LINE_LIMIT;
42
64
  const waiverTargetErrors = await validateWaiverTargets(projectRoot, waivers, limit);
43
65
  const candidates = new Set();
44
66
  const baselineRef = options.base ?? (options.touched ? "HEAD" : undefined);
@@ -144,21 +166,71 @@ function modularityRegressions(current, baseline) {
144
166
  message: `${current.metrics.stateTransitions} state transitions exceeds limit ${COMPLEXITY_LIMITS.stateTransitions}`,
145
167
  },
146
168
  {
147
- current: current.metrics.responsibilities.length,
148
- baseline: baseline.metrics.responsibilities.length,
169
+ current: current.metrics.responsibilities?.length ?? null,
170
+ baseline: baseline.metrics.responsibilities?.length ?? null,
149
171
  limit: COMPLEXITY_LIMITS.responsibilities,
150
- message: `${current.metrics.responsibilities.length} module responsibilities exceeds limit ${COMPLEXITY_LIMITS.responsibilities}`,
172
+ message: `${current.metrics.responsibilities?.length ?? 0} module responsibilities exceeds limit ${COMPLEXITY_LIMITS.responsibilities}`,
151
173
  },
152
174
  ];
153
175
  return checks
154
- .filter((check) => check.current > check.limit && check.current > check.baseline)
176
+ .filter((check) => check.current !== null &&
177
+ check.baseline !== null &&
178
+ check.current > check.limit &&
179
+ check.current > check.baseline)
155
180
  .map((check) => check.message);
156
181
  }
157
182
  export function analyzeModularity(content, relativePath = "") {
183
+ const analysis = (relativePath
184
+ ? modularityAnalysisCapability(relativePath)
185
+ : "js-ts-heuristic") ?? "line-only";
186
+ if (analysis === "line-only") {
187
+ return unavailableMetrics(analysis);
188
+ }
189
+ if (analysis === "python-heuristic") {
190
+ const bodies = analyzePythonFunctions(content);
191
+ const statementPeak = maxMetricBody(bodies, "statements");
192
+ const branchPeak = maxMetricBody(bodies, "branches");
193
+ return {
194
+ analysis,
195
+ maxFunctionStatements: statementPeak?.statements ?? 0,
196
+ ...(statementPeak
197
+ ? {
198
+ maxFunctionStatementsLocation: {
199
+ symbol: statementPeak.symbol,
200
+ line: statementPeak.line,
201
+ },
202
+ }
203
+ : {}),
204
+ maxBranchComplexity: branchPeak?.branches ?? 0,
205
+ ...(branchPeak
206
+ ? {
207
+ maxBranchComplexityLocation: {
208
+ symbol: branchPeak.symbol,
209
+ line: branchPeak.line,
210
+ },
211
+ }
212
+ : {}),
213
+ exports: null,
214
+ stateTransitions: null,
215
+ responsibilities: null,
216
+ };
217
+ }
218
+ return analyzeJsTsModularity(content);
219
+ }
220
+ export function hasLegacyHeuristicOnlyWaiverRisk(content, relativePath, lineLimit) {
221
+ if (modularityAnalysisCapability(relativePath) !== "line-only") {
222
+ return false;
223
+ }
224
+ const lines = countPhysicalLines(content);
225
+ if (modularityRisks(lines, lineLimit, analyzeModularity(content, relativePath))
226
+ .length > 0) {
227
+ return false;
228
+ }
229
+ return (modularityRisks(lines, lineLimit, analyzeJsTsModularity(content)).length > 0);
230
+ }
231
+ function analyzeJsTsModularity(content) {
158
232
  const code = sanitizeCode(content);
159
- const bodies = relativePath.toLowerCase().endsWith(".py")
160
- ? pythonFunctionBodies(code)
161
- : functionBodies(code);
233
+ const bodies = functionBodies(code);
162
234
  const analyzedBodies = bodies.map((body) => ({
163
235
  ...body,
164
236
  statements: statementCount(body.body),
@@ -167,6 +239,7 @@ export function analyzeModularity(content, relativePath = "") {
167
239
  const statementPeak = maxMetricBody(analyzedBodies, "statements");
168
240
  const branchPeak = maxMetricBody(analyzedBodies, "branches");
169
241
  return {
242
+ analysis: "js-ts-heuristic",
170
243
  maxFunctionStatements: statementPeak?.statements ?? 0,
171
244
  ...(statementPeak
172
245
  ? {
@@ -190,23 +263,37 @@ export function analyzeModularity(content, relativePath = "") {
190
263
  responsibilities: inferResponsibilities(code),
191
264
  };
192
265
  }
266
+ function unavailableMetrics(analysis) {
267
+ return {
268
+ analysis,
269
+ maxFunctionStatements: null,
270
+ maxBranchComplexity: null,
271
+ exports: null,
272
+ stateTransitions: null,
273
+ responsibilities: null,
274
+ };
275
+ }
193
276
  function modularityRisks(lines, lineLimit, metrics) {
194
277
  const risks = [];
195
278
  if (lines > lineLimit)
196
279
  risks.push(`${lines} physical lines exceeds limit ${lineLimit}`);
197
- if (metrics.maxFunctionStatements > COMPLEXITY_LIMITS.functionStatements) {
280
+ if (metrics.maxFunctionStatements !== null &&
281
+ metrics.maxFunctionStatements > COMPLEXITY_LIMITS.functionStatements) {
198
282
  risks.push(`${metrics.maxFunctionStatements} statements in one function exceeds limit ${COMPLEXITY_LIMITS.functionStatements}${formatMetricLocation(metrics.maxFunctionStatementsLocation)}`);
199
283
  }
200
- if (metrics.maxBranchComplexity > COMPLEXITY_LIMITS.branchComplexity) {
284
+ if (metrics.maxBranchComplexity !== null &&
285
+ metrics.maxBranchComplexity > COMPLEXITY_LIMITS.branchComplexity) {
201
286
  risks.push(`${metrics.maxBranchComplexity} branch complexity exceeds limit ${COMPLEXITY_LIMITS.branchComplexity}${formatMetricLocation(metrics.maxBranchComplexityLocation)}`);
202
287
  }
203
- if (metrics.exports > COMPLEXITY_LIMITS.exports) {
288
+ if (metrics.exports !== null && metrics.exports > COMPLEXITY_LIMITS.exports) {
204
289
  risks.push(`${metrics.exports} exports exceeds limit ${COMPLEXITY_LIMITS.exports}`);
205
290
  }
206
- if (metrics.stateTransitions > COMPLEXITY_LIMITS.stateTransitions) {
291
+ if (metrics.stateTransitions !== null &&
292
+ metrics.stateTransitions > COMPLEXITY_LIMITS.stateTransitions) {
207
293
  risks.push(`${metrics.stateTransitions} state transitions exceeds limit ${COMPLEXITY_LIMITS.stateTransitions}`);
208
294
  }
209
- if (metrics.responsibilities.length > COMPLEXITY_LIMITS.responsibilities) {
295
+ if (metrics.responsibilities !== null &&
296
+ metrics.responsibilities.length > COMPLEXITY_LIMITS.responsibilities) {
210
297
  risks.push(`${metrics.responsibilities.length} module responsibilities exceeds limit ${COMPLEXITY_LIMITS.responsibilities}`);
211
298
  }
212
299
  return risks;
@@ -372,30 +459,6 @@ function functionBodies(code) {
372
459
  })
373
460
  .filter((body) => body !== undefined);
374
461
  }
375
- function pythonFunctionBodies(code) {
376
- const lines = code.split(/\r\n|\n|\r/u);
377
- const bodies = [];
378
- for (let index = 0; index < lines.length; index += 1) {
379
- const match = /^(\s*)(?:async\s+)?def\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/u.exec(lines[index]);
380
- if (!match)
381
- continue;
382
- const indentation = indentationWidth(match[1]);
383
- const body = [];
384
- for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
385
- const line = lines[cursor];
386
- if (!line.trim()) {
387
- body.push(line);
388
- continue;
389
- }
390
- const leading = /^\s*/u.exec(line)?.[0] ?? "";
391
- if (indentationWidth(leading) <= indentation)
392
- break;
393
- body.push(line);
394
- }
395
- bodies.push({ body: body.join("\n"), symbol: match[2], line: index + 1 });
396
- }
397
- return bodies;
398
- }
399
462
  function inferArrowSymbol(code, arrowIndex) {
400
463
  const prefix = code.slice(Math.max(0, arrowIndex - 240), arrowIndex);
401
464
  const assignment = /(?:\b(?:const|let|var)\s+|\b)([A-Za-z_$][\w$]*)\s*=\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*$/u.exec(prefix);
@@ -420,9 +483,6 @@ function formatMetricLocation(location) {
420
483
  ? ` in function ${location.symbol} at line ${location.line}`
421
484
  : "";
422
485
  }
423
- function indentationWidth(value) {
424
- return [...value].reduce((width, character) => width + (character === "\t" ? 4 : 1), 0);
425
- }
426
486
  function balancedBody(code, openingBrace) {
427
487
  let depth = 0;
428
488
  for (let index = openingBrace; index < code.length; index += 1) {
@@ -685,9 +745,12 @@ async function validateWaiverTargets(projectRoot, waivers, limit) {
685
745
  errors.push(`${label} is unnecessary because generated files are excluded`);
686
746
  continue;
687
747
  }
688
- const analyzed = analyzeFile(await fs.readFile(target, "utf8"), limit, waiver.relativePath);
748
+ const content = await fs.readFile(target, "utf8");
749
+ const analyzed = analyzeFile(content, limit, waiver.relativePath);
689
750
  if (analyzed.risks.length === 0) {
690
- errors.push(`${label} is unnecessary because the source does not currently exceed a modularity threshold`);
751
+ errors.push(hasLegacyHeuristicOnlyWaiverRisk(content, waiver.relativePath, limit)
752
+ ? `${label} is obsolete because this target now has line-only analysis and exceeded only unsupported legacy heuristic metrics; run ty-context upgrade to remove the waiver`
753
+ : `${label} is unnecessary because the source does not currently exceed a modularity threshold`);
691
754
  }
692
755
  }
693
756
  return errors;
@@ -710,6 +773,9 @@ async function isRegularFile(target) {
710
773
  }
711
774
  async function isLikelyGeneratedFile(target) {
712
775
  const content = await fs.readFile(target, "utf8");
776
+ return isLikelyGeneratedSourceContent(content);
777
+ }
778
+ export function isLikelyGeneratedSourceContent(content) {
713
779
  const sample = content.slice(0, 8192);
714
780
  return GENERATED_FILE_PATTERNS.some((pattern) => pattern.test(sample));
715
781
  }
@@ -1,4 +1,6 @@
1
1
  export declare const SAFE_EXAMPLE_FILE_NAMES: Set<string>;
2
+ export type ModularityAnalysisCapability = "js-ts-heuristic" | "python-heuristic" | "line-only";
2
3
  export declare function shouldIncludeCodeFile(relative: string): boolean;
4
+ export declare function modularityAnalysisCapability(relative: string): ModularityAnalysisCapability | undefined;
3
5
  export declare function shouldExcludeRelativePath(relative: string): boolean;
4
6
  export declare function toPosix(value: string): string;
@@ -90,6 +90,14 @@ const CONFIG_JSON_NAMES = new Set([
90
90
  "tsconfig.json",
91
91
  "vite.config.json",
92
92
  ]);
93
+ const JS_TS_HEURISTIC_EXTENSIONS = new Set([
94
+ ".cjs",
95
+ ".js",
96
+ ".jsx",
97
+ ".mjs",
98
+ ".ts",
99
+ ".tsx",
100
+ ]);
93
101
  export function shouldIncludeCodeFile(relative) {
94
102
  if (shouldExcludeRelativePath(relative)) {
95
103
  return false;
@@ -108,6 +116,19 @@ export function shouldIncludeCodeFile(relative) {
108
116
  }
109
117
  return CODE_FILE_EXTENSIONS.some((extension) => lower.endsWith(extension));
110
118
  }
119
+ export function modularityAnalysisCapability(relative) {
120
+ if (!shouldIncludeCodeFile(relative)) {
121
+ return undefined;
122
+ }
123
+ const extension = path.posix.extname(toPosix(relative).toLowerCase());
124
+ if (JS_TS_HEURISTIC_EXTENSIONS.has(extension)) {
125
+ return "js-ts-heuristic";
126
+ }
127
+ if (extension === ".py") {
128
+ return "python-heuristic";
129
+ }
130
+ return "line-only";
131
+ }
111
132
  export function shouldExcludeRelativePath(relative) {
112
133
  const normalized = toPosix(relative);
113
134
  const segments = normalized.split("/");