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.
- package/README.md +25 -23
- package/assets/README.md +36 -23
- package/assets/README.zh-CN.md +36 -21
- package/assets/agents/AGENTS_CORE.md +11 -9
- package/assets/context_templates/architecture.md +11 -8
- package/assets/context_templates/area.md +2 -2
- package/assets/context_templates/context.toml +9 -4
- package/assets/context_templates/verification.md +8 -6
- package/assets/skills/context_development_engineer/SKILL.md +46 -31
- package/assets/skills/long-task-workflow/SKILL.md +8 -6
- package/assets/skills/long-task-workflow/references/authority-lifecycle.md +1 -1
- package/assets/skills/long-task-workflow/references/contract-authoring.md +13 -11
- package/dist/commands/check-modularity.js +16 -3
- package/dist/lib/context-manifest.js +9 -4
- package/dist/lib/migrations.js +2 -0
- package/dist/lib/modularity-capability-migration.d.ts +2 -0
- package/dist/lib/modularity-capability-migration.js +165 -0
- package/dist/lib/modularity-python.d.ts +7 -0
- package/dist/lib/modularity-python.js +191 -0
- package/dist/lib/modularity.d.ts +12 -5
- package/dist/lib/modularity.js +110 -44
- package/dist/lib/source-files.d.ts +2 -0
- package/dist/lib/source-files.js +21 -0
- package/package.json +1 -1
|
@@ -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,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
|
+
}
|
package/dist/lib/modularity.d.ts
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/dist/lib/modularity.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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 ??
|
|
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
|
|
148
|
-
baseline: baseline.metrics.responsibilities
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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;
|
package/dist/lib/source-files.js
CHANGED
|
@@ -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("/");
|