project-tiny-context-harness 0.2.52 → 0.2.54

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.
@@ -9,6 +9,10 @@ export function defaultConfig(root) {
9
9
  package: CANONICAL_CORE_PACKAGE,
10
10
  schema_version: CURRENT_SCHEMA_VERSION
11
11
  },
12
+ modularity: {
13
+ limit: 300,
14
+ policy: "strict_except_generated"
15
+ },
12
16
  managed_files: [
13
17
  { path: "AGENTS.md", strategy: "merge-block" },
14
18
  { path: "Makefile", strategy: "merge-block" },
@@ -46,6 +50,7 @@ export function normalizeConfig(value, root = ".agent") {
46
50
  package: value.core?.package ?? fallback.core.package,
47
51
  schema_version: value.core?.schema_version ?? fallback.core.schema_version
48
52
  },
53
+ modularity: value.modularity,
49
54
  managed_files: value.managed_files ?? fallback.managed_files,
50
55
  never_overwrite: value.never_overwrite ?? fallback.never_overwrite
51
56
  };
@@ -5,6 +5,7 @@ import path from "node:path";
5
5
  import { promisify } from "node:util";
6
6
  import { ensureDir, listFiles, pathExists, readText, writeTextIfChanged } from "./fs.js";
7
7
  import { harnessRoot } from "./harness-root.js";
8
+ import { SAFE_EXAMPLE_FILE_NAMES, shouldExcludeRelativePath, shouldIncludeCodeFile, toPosix } from "./source-files.js";
8
9
  const execFileAsync = promisify(execFile);
9
10
  const EXPORT_HEADER = "Export artifact. Do not reference from project_context/context.toml.";
10
11
  const DEFAULT_EXPORT_DIR = "tmp/sdlc/context-exports";
@@ -13,91 +14,6 @@ const MAX_TREE_ENTRIES = 300;
13
14
  const MAX_TREE_DEPTH = 4;
14
15
  const GIT_LS_MAX_BUFFER = 64 * 1024 * 1024;
15
16
  const APPROX_TEXT_TOKEN_LIMIT_CHARS = 8_000_000;
16
- const EXCLUDED_DIR_NAMES = new Set([
17
- ".git",
18
- ".artifacts",
19
- ".cache",
20
- ".next",
21
- ".nuxt",
22
- ".runtime",
23
- ".turbo",
24
- "artifacts",
25
- "build",
26
- "cache",
27
- "captures",
28
- "coverage",
29
- "dist",
30
- "logs",
31
- "node_modules",
32
- "out",
33
- "playwright-report",
34
- "raw-captures",
35
- "reports",
36
- "target",
37
- "temp",
38
- "test-reports",
39
- "test-results",
40
- "tmp"
41
- ]);
42
- const SAFE_EXAMPLE_FILE_NAMES = new Set([".env.example", ".env.sample", ".env.template", "example.env", "sample.env"]);
43
- const EXCLUDED_FILE_PATTERNS = [
44
- /^\.env(?:\.|$)/i,
45
- /\.log$/i,
46
- /\.min\.(?:css|js|mjs)$/i,
47
- /(^|[-_.])(secret|secrets|cookie|cookies|credential|credentials|api-key|apikey|access-token|refresh-token|auth-token|private-key)([-_.]|$)/i,
48
- /(^|[-_.])(raw-capture|capture-dump|licensed-payload|license-payload|test-report)([-_.]|$)/i,
49
- /^(?:package-lock\.json|npm-shrinkwrap\.json|pnpm-lock\.yaml|yarn\.lock|poetry\.lock|pipfile\.lock|cargo\.lock)$/i,
50
- /full-project-context-\d{8}T\d{6}Z\.md$/i,
51
- /当前项目context-\d{8}T\d{6}Z\.md$/i,
52
- /当前项目代码实现\.md$/i,
53
- /(^|[-_.])(code-level-implementation|context-export|context-bundle)([-_.]|$)/i
54
- ];
55
- const CODE_FILE_EXTENSIONS = [
56
- ".bat",
57
- ".cjs",
58
- ".cmd",
59
- ".go",
60
- ".gql",
61
- ".graphql",
62
- ".js",
63
- ".jsx",
64
- ".jsonc",
65
- ".mjs",
66
- ".proto",
67
- ".ps1",
68
- ".py",
69
- ".sh",
70
- ".sql",
71
- ".toml",
72
- ".ts",
73
- ".tsx",
74
- ".vue",
75
- ".yaml",
76
- ".yml"
77
- ];
78
- const CODE_FILE_BASE_NAMES = new Set([
79
- ".env.example",
80
- ".env.sample",
81
- ".env.template",
82
- "dockerfile",
83
- "makefile",
84
- "package.json",
85
- "pyproject.toml",
86
- "requirements.txt",
87
- "setup.cfg",
88
- "tsconfig.json"
89
- ]);
90
- const CONFIG_JSON_NAMES = new Set([
91
- "babel.config.json",
92
- "biome.json",
93
- "composer.json",
94
- "deno.json",
95
- "eslint.config.json",
96
- "jsconfig.json",
97
- "package.json",
98
- "tsconfig.json",
99
- "vite.config.json"
100
- ]);
101
17
  const SENSITIVE_ASSIGNMENT_PATTERN = /^(\s*(?:[-*]\s*)?(?:[`"']?[\w.-]*(?:secret|token|cookie|password|api[_-]?key)[\w.-]*[`"']?\s*[:=]\s*))(.+?)\s*$/i;
102
18
  export async function runExportContext(projectRoot, options = {}) {
103
19
  const requestedModeCount = Number(options.full === true) + Number(options.code === true);
@@ -519,55 +435,6 @@ function buildImplementationGuide(records) {
519
435
  "- This file is a temporary implementation snapshot, not durable Context; durable project facts still belong in project_context/**."
520
436
  ].join("\n");
521
437
  }
522
- function shouldIncludeCodeFile(relative) {
523
- if (shouldExcludeRelativePath(relative)) {
524
- return false;
525
- }
526
- const normalized = toPosix(relative);
527
- const lower = normalized.toLowerCase();
528
- const base = path.posix.basename(lower);
529
- if (CODE_FILE_BASE_NAMES.has(base) || base.startsWith("dockerfile.")) {
530
- return true;
531
- }
532
- if (lower.endsWith(".dockerfile")) {
533
- return true;
534
- }
535
- if (lower.endsWith(".json")) {
536
- return isConfigJson(lower);
537
- }
538
- return CODE_FILE_EXTENSIONS.some((extension) => lower.endsWith(extension));
539
- }
540
- function shouldExcludeRelativePath(relative) {
541
- const normalized = toPosix(relative);
542
- const segments = normalized.split("/");
543
- if (segments.some((segment) => EXCLUDED_DIR_NAMES.has(segment))) {
544
- return true;
545
- }
546
- const base = segments[segments.length - 1] ?? "";
547
- const lowerBase = base.toLowerCase();
548
- return EXCLUDED_FILE_PATTERNS.some((pattern) => {
549
- if (SAFE_EXAMPLE_FILE_NAMES.has(lowerBase) && pattern.source.startsWith("^\\.env")) {
550
- return false;
551
- }
552
- return pattern.test(base) || pattern.test(normalized);
553
- });
554
- }
555
- function isConfigJson(lowerRelative) {
556
- const base = path.posix.basename(lowerRelative);
557
- return (CONFIG_JSON_NAMES.has(base) ||
558
- lowerRelative.endsWith(".schema.json") ||
559
- lowerRelative.includes("/schema/") ||
560
- lowerRelative.includes("/schemas/") ||
561
- lowerRelative.includes("/config/") ||
562
- lowerRelative.includes("/configs/") ||
563
- lowerRelative.includes("/examples/") ||
564
- lowerRelative.includes("/sample/") ||
565
- lowerRelative.includes("/samples/") ||
566
- base.includes("config") ||
567
- base.includes("schema") ||
568
- base.includes("example") ||
569
- base.includes("sample"));
570
- }
571
438
  function summarizeCodeFile(relative, content, language) {
572
439
  const lower = relative.toLowerCase();
573
440
  const base = path.posix.basename(relative);
@@ -837,9 +704,6 @@ function timestampForFile(now) {
837
704
  function repoRelative(root, file) {
838
705
  return toPosix(path.relative(root, file));
839
706
  }
840
- function toPosix(value) {
841
- return value.replace(/\\/g, "/");
842
- }
843
707
  function escapeTableCell(value) {
844
708
  return value.replace(/\r?\n/g, " ").replace(/\|/g, "\\|");
845
709
  }
@@ -36,4 +36,4 @@ export declare function createUpgradePlan(projectRoot: string): Promise<UpgradeP
36
36
  export declare function hasUpgradePlanWork(plan: UpgradePlan): boolean;
37
37
  export declare function updateModeForPlan(plan: UpgradePlan): ReleaseUpdateMode;
38
38
  export declare function formatUpgradePlan(plan: UpgradePlan): string[];
39
- export declare function runMigrations(projectRoot: string): Promise<MigrationReport>;
39
+ export declare function runMigrations(projectRoot: string, existingPlan?: UpgradePlan): Promise<MigrationReport>;
@@ -112,12 +112,15 @@ export function formatUpgradePlan(plan) {
112
112
  }
113
113
  return lines;
114
114
  }
115
- export async function runMigrations(projectRoot) {
115
+ export async function runMigrations(projectRoot, existingPlan) {
116
116
  const report = { changed: [], skipped: [], manualRequired: [], blocked: [] };
117
117
  const root = await harnessRoot(projectRoot);
118
- const plan = await createUpgradePlan(projectRoot);
118
+ const plan = existingPlan ?? (await createUpgradePlan(projectRoot));
119
119
  report.manualRequired.push(...plan.manual_required);
120
120
  report.blocked.push(...plan.blocked);
121
+ if (plan.blocked.length > 0) {
122
+ return report;
123
+ }
121
124
  for (const migration of migrations) {
122
125
  if (!migration.apply) {
123
126
  continue;
@@ -137,10 +140,14 @@ async function migrateBaseProjectContext(projectRoot, report) {
137
140
  await ensureDir(path.join(projectRoot, "project_context", "areas"));
138
141
  const files = [
139
142
  ["project_context/global.md", globalContextTemplate()],
140
- ["project_context/architecture.md", architectureContextTemplate()],
141
- ["project_context/areas/main.md", areaContextTemplate("main")],
142
- ["project_context/areas/main/verification.md", verificationContextTemplate("main")]
143
+ ["project_context/architecture.md", architectureContextTemplate()]
143
144
  ];
145
+ if (await contextManifestReferences(projectRoot, "project_context/areas/main.md")) {
146
+ files.push(["project_context/areas/main.md", areaContextTemplate("main")]);
147
+ }
148
+ if (await contextManifestReferences(projectRoot, "project_context/areas/main/verification.md")) {
149
+ files.push(["project_context/areas/main/verification.md", verificationContextTemplate("main")]);
150
+ }
144
151
  for (const [relative, content] of files) {
145
152
  const target = path.join(projectRoot, ...relative.split("/"));
146
153
  if (await pathExists(target)) {
@@ -155,6 +162,13 @@ async function migrateBaseProjectContext(projectRoot, report) {
155
162
  }
156
163
  }
157
164
  }
165
+ async function contextManifestReferences(projectRoot, relative) {
166
+ const manifestPath = path.join(projectRoot, CONTEXT_MANIFEST_PATH);
167
+ if (!(await pathExists(manifestPath))) {
168
+ return true;
169
+ }
170
+ return manifestReferencesPath(await readText(manifestPath), relative);
171
+ }
158
172
  async function detectGlobalContextSections(projectRoot, _root, migration) {
159
173
  const relative = "project_context/global.md";
160
174
  const target = path.join(projectRoot, ...relative.split("/"));
@@ -427,3 +441,7 @@ function ensureManifestDefaultArea(content) {
427
441
  lines.splice(nextTableIndex, 0, "default = true");
428
442
  return lines.join("\n");
429
443
  }
444
+ function manifestReferencesPath(content, relative) {
445
+ const escaped = relative.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
446
+ return new RegExp(`^(?:\\s*)(?:context|path)\\s*=\\s*["']${escaped}["']\\s*$`, "im").test(content);
447
+ }
@@ -0,0 +1,27 @@
1
+ export interface ModularityCheckOptions {
2
+ touched?: boolean;
3
+ base?: string;
4
+ files?: string[];
5
+ limit?: number;
6
+ }
7
+ export interface ModularityFileReport {
8
+ relativePath: string;
9
+ lines: number;
10
+ overLimit: boolean;
11
+ waived?: ModularityWaiver;
12
+ }
13
+ export interface ModularityCheckReport {
14
+ limit: number;
15
+ files: ModularityFileReport[];
16
+ warnings: string[];
17
+ waivedWarnings: string[];
18
+ errors: string[];
19
+ }
20
+ export interface ModularityWaiver {
21
+ relativePath: string;
22
+ category: string;
23
+ reason: string;
24
+ futureSplitBoundary: string;
25
+ }
26
+ export declare function runModularityCheck(projectRoot: string, options: ModularityCheckOptions): Promise<ModularityCheckReport>;
27
+ export declare function countPhysicalLines(content: string): number;
@@ -0,0 +1,233 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promises as fs } from "node:fs";
3
+ import path from "node:path";
4
+ import { promisify } from "node:util";
5
+ import { readConfig } from "./config.js";
6
+ import { shouldIncludeCodeFile, toPosix } from "./source-files.js";
7
+ const execFileAsync = promisify(execFile);
8
+ const GIT_MAX_BUFFER = 16 * 1024 * 1024;
9
+ const DEFAULT_LINE_LIMIT = 300;
10
+ const DEFAULT_MODULARITY_POLICY = "scoped_waivers";
11
+ const MODULARITY_POLICIES = new Set([DEFAULT_MODULARITY_POLICY, "strict_except_generated"]);
12
+ const GENERATED_FILE_PATTERNS = [
13
+ /^\s*(?:\/\/|#|--|;)\s*@generated\b/im,
14
+ /^\s*(?:\/\/|#|--|;)\s*code generated .*do not edit\.?\s*$/im,
15
+ /^\s*(?:\/\/|#|--|;)\s*do not edit\.?\s*$/im,
16
+ /^\s*(?:\/\/|#|--|;|<!--)\s*<auto-generated\b/im,
17
+ /^\s*(?:\/\/|#|--|;)\s*this file was generated\b/im,
18
+ /^\s*(?:\/\/|#|--|;)\s*generated by\b/im
19
+ ];
20
+ const WAIVER_CATEGORIES = new Set([
21
+ "generated",
22
+ "third_party_reference",
23
+ "legacy_migration",
24
+ "aggregate_styles",
25
+ "fixture_snapshot"
26
+ ]);
27
+ export async function runModularityCheck(projectRoot, options) {
28
+ const config = await readConfig(projectRoot);
29
+ const { limit: configuredLimit, waiverValues, errors: configErrors } = validateModularityConfig(config.modularity);
30
+ const { waivers, errors: waiverErrors } = validateWaivers(projectRoot, waiverValues);
31
+ const limit = options.limit ?? configuredLimit ?? DEFAULT_LINE_LIMIT;
32
+ const candidates = new Set();
33
+ for (const file of options.files ?? []) {
34
+ candidates.add(normalizeExplicitPath(projectRoot, file));
35
+ }
36
+ if (options.touched) {
37
+ for (const relative of await gitTouchedFiles(projectRoot)) {
38
+ candidates.add(normalizeGitPath(relative));
39
+ }
40
+ }
41
+ if (options.base) {
42
+ for (const relative of await gitDiffFiles(projectRoot, options.base)) {
43
+ candidates.add(normalizeGitPath(relative));
44
+ }
45
+ }
46
+ const files = [];
47
+ for (const relativePath of [...candidates].sort()) {
48
+ if (!shouldIncludeCodeFile(relativePath)) {
49
+ continue;
50
+ }
51
+ const absolutePath = path.join(projectRoot, ...relativePath.split("/"));
52
+ if (!(await isRegularFile(absolutePath)) || (await isLikelyGeneratedFile(absolutePath))) {
53
+ continue;
54
+ }
55
+ const lines = countPhysicalLines(await fs.readFile(absolutePath, "utf8"));
56
+ const waiver = waivers.get(relativePath);
57
+ files.push({ relativePath, lines, overLimit: lines > limit, waived: waiver });
58
+ }
59
+ const warnings = files
60
+ .filter((file) => file.overLimit && !file.waived)
61
+ .map((file) => `${file.relativePath}: ${file.lines} physical lines exceeds limit ${limit}`);
62
+ const waivedWarnings = files
63
+ .filter((file) => file.overLimit && file.waived)
64
+ .map((file) => `${file.relativePath}: ${file.lines} physical lines exceeds limit ${limit} but is waived as ${file.waived?.category}`);
65
+ return { limit, files, warnings, waivedWarnings, errors: [...configErrors, ...waiverErrors] };
66
+ }
67
+ export function countPhysicalLines(content) {
68
+ if (content.length === 0) {
69
+ return 0;
70
+ }
71
+ const lines = content.split(/\r\n|\n|\r/);
72
+ if (lines[lines.length - 1] === "") {
73
+ lines.pop();
74
+ }
75
+ return lines.length;
76
+ }
77
+ async function gitTouchedFiles(projectRoot) {
78
+ let result;
79
+ try {
80
+ result = await execFileAsync("git", ["-C", projectRoot, "status", "--porcelain", "-z"], {
81
+ encoding: "utf8",
82
+ maxBuffer: GIT_MAX_BUFFER
83
+ });
84
+ }
85
+ catch (error) {
86
+ if (isNotGitRepositoryError(error)) {
87
+ return [];
88
+ }
89
+ throw error;
90
+ }
91
+ const records = result.stdout.split("\0").filter(Boolean);
92
+ const files = [];
93
+ for (let index = 0; index < records.length; index += 1) {
94
+ const record = records[index];
95
+ const status = record.slice(0, 2);
96
+ const relative = record.slice(3);
97
+ if (relative) {
98
+ files.push(relative);
99
+ }
100
+ if (status.includes("R") || status.includes("C")) {
101
+ index += 1;
102
+ }
103
+ }
104
+ return files;
105
+ }
106
+ async function gitDiffFiles(projectRoot, base) {
107
+ const result = await execFileAsync("git", ["-C", projectRoot, "diff", "--name-only", "-z", base, "--"], {
108
+ encoding: "utf8",
109
+ maxBuffer: GIT_MAX_BUFFER
110
+ });
111
+ return result.stdout.split("\0").filter(Boolean);
112
+ }
113
+ function isNotGitRepositoryError(error) {
114
+ if (!error || typeof error !== "object") {
115
+ return false;
116
+ }
117
+ const stderr = "stderr" in error && typeof error.stderr === "string" ? error.stderr : "";
118
+ return /not a git repository/i.test(stderr);
119
+ }
120
+ function normalizeExplicitPath(projectRoot, value) {
121
+ const absolute = path.isAbsolute(value) ? path.resolve(value) : path.resolve(projectRoot, value);
122
+ const relative = toPosix(path.relative(projectRoot, absolute));
123
+ if (relative.startsWith("../") || relative === ".." || path.isAbsolute(relative)) {
124
+ throw new Error(`check-modularity --file must stay inside the project root: ${value}`);
125
+ }
126
+ return normalizeGitPath(relative);
127
+ }
128
+ function normalizeGitPath(value) {
129
+ return toPosix(value).replace(/^\.\//, "");
130
+ }
131
+ function validateModularityConfig(modularity) {
132
+ const errors = [];
133
+ if (modularity === undefined) {
134
+ return { errors };
135
+ }
136
+ if (!modularity || typeof modularity !== "object" || Array.isArray(modularity)) {
137
+ errors.push("<harnessRoot>/config.yaml modularity must be an object");
138
+ return { errors };
139
+ }
140
+ const value = modularity;
141
+ let limit;
142
+ let policy = DEFAULT_MODULARITY_POLICY;
143
+ if (value.limit !== undefined) {
144
+ if (!Number.isInteger(value.limit) || Number(value.limit) <= 0) {
145
+ errors.push("<harnessRoot>/config.yaml modularity.limit must be a positive integer");
146
+ }
147
+ else {
148
+ limit = Number(value.limit);
149
+ }
150
+ }
151
+ if (value.policy !== undefined) {
152
+ if (typeof value.policy !== "string" || !MODULARITY_POLICIES.has(value.policy)) {
153
+ errors.push("<harnessRoot>/config.yaml modularity.policy must be one of scoped_waivers, strict_except_generated");
154
+ }
155
+ else {
156
+ policy = value.policy;
157
+ }
158
+ }
159
+ if (policy === "strict_except_generated" && value.waivers !== undefined) {
160
+ errors.push("<harnessRoot>/config.yaml modularity.waivers is not allowed when modularity.policy is strict_except_generated");
161
+ }
162
+ return { limit, waiverValues: policy === "scoped_waivers" ? value.waivers : undefined, errors };
163
+ }
164
+ function validateWaivers(projectRoot, waiverValues) {
165
+ const waivers = new Map();
166
+ const errors = [];
167
+ if (waiverValues === undefined) {
168
+ return { waivers, errors };
169
+ }
170
+ if (!Array.isArray(waiverValues)) {
171
+ errors.push("<harnessRoot>/config.yaml modularity.waivers must be an array");
172
+ return { waivers, errors };
173
+ }
174
+ for (const [index, waiver] of waiverValues.entries()) {
175
+ const label = `<harnessRoot>/config.yaml modularity.waivers[${index}]`;
176
+ if (!waiver || typeof waiver !== "object") {
177
+ errors.push(`${label} must be an object`);
178
+ continue;
179
+ }
180
+ const relativePath = requiredWaiverString(waiver, "path", label, errors);
181
+ const category = requiredWaiverString(waiver, "category", label, errors);
182
+ const reason = requiredWaiverString(waiver, "reason", label, errors);
183
+ const futureSplitBoundary = requiredWaiverString(waiver, "future_split_boundary", label, errors);
184
+ if (!relativePath || !category || !reason || !futureSplitBoundary) {
185
+ continue;
186
+ }
187
+ if (!WAIVER_CATEGORIES.has(category)) {
188
+ errors.push(`${label}.category must be one of generated, third_party_reference, legacy_migration, aggregate_styles, fixture_snapshot`);
189
+ continue;
190
+ }
191
+ let normalized;
192
+ try {
193
+ normalized = normalizeExplicitPath(projectRoot, relativePath);
194
+ }
195
+ catch {
196
+ errors.push(`${label}.path must stay inside the project root: ${relativePath}`);
197
+ continue;
198
+ }
199
+ const existing = waivers.get(normalized);
200
+ if (existing) {
201
+ errors.push(`${label}.path duplicates an existing modularity waiver for ${normalized}`);
202
+ continue;
203
+ }
204
+ waivers.set(normalized, {
205
+ relativePath: normalized,
206
+ category,
207
+ reason,
208
+ futureSplitBoundary
209
+ });
210
+ }
211
+ return { waivers, errors };
212
+ }
213
+ function requiredWaiverString(waiver, field, label, errors) {
214
+ const value = waiver[field];
215
+ if (typeof value !== "string" || value.trim().length === 0) {
216
+ errors.push(`${label}.${field} must be a non-empty string`);
217
+ return undefined;
218
+ }
219
+ return value.trim();
220
+ }
221
+ async function isRegularFile(target) {
222
+ try {
223
+ return (await fs.stat(target)).isFile();
224
+ }
225
+ catch {
226
+ return false;
227
+ }
228
+ }
229
+ async function isLikelyGeneratedFile(target) {
230
+ const content = await fs.readFile(target, "utf8");
231
+ const sample = content.slice(0, 8192);
232
+ return GENERATED_FILE_PATTERNS.some((pattern) => pattern.test(sample));
233
+ }
@@ -0,0 +1,4 @@
1
+ export declare const SAFE_EXAMPLE_FILE_NAMES: Set<string>;
2
+ export declare function shouldIncludeCodeFile(relative: string): boolean;
3
+ export declare function shouldExcludeRelativePath(relative: string): boolean;
4
+ export declare function toPosix(value: string): string;
@@ -0,0 +1,138 @@
1
+ import path from "node:path";
2
+ const EXCLUDED_DIR_NAMES = new Set([
3
+ ".git",
4
+ ".artifacts",
5
+ ".cache",
6
+ ".next",
7
+ ".nuxt",
8
+ ".runtime",
9
+ ".turbo",
10
+ "artifacts",
11
+ "build",
12
+ "cache",
13
+ "captures",
14
+ "coverage",
15
+ "dist",
16
+ "logs",
17
+ "node_modules",
18
+ "out",
19
+ "playwright-report",
20
+ "raw-captures",
21
+ "reports",
22
+ "target",
23
+ "temp",
24
+ "test-reports",
25
+ "test-results",
26
+ "tmp"
27
+ ]);
28
+ export const SAFE_EXAMPLE_FILE_NAMES = new Set([".env.example", ".env.sample", ".env.template", "example.env", "sample.env"]);
29
+ const EXCLUDED_FILE_PATTERNS = [
30
+ /^\.env(?:\.|$)/i,
31
+ /\.log$/i,
32
+ /\.min\.(?:css|js|mjs)$/i,
33
+ /(^|[-_.])(secret|secrets|cookie|cookies|credential|credentials|api-key|apikey|access-token|refresh-token|auth-token|private-key)([-_.]|$)/i,
34
+ /(^|[-_.])(raw-capture|capture-dump|licensed-payload|license-payload|test-report)([-_.]|$)/i,
35
+ /^(?:package-lock\.json|npm-shrinkwrap\.json|pnpm-lock\.yaml|yarn\.lock|poetry\.lock|pipfile\.lock|cargo\.lock)$/i,
36
+ /full-project-context-\d{8}T\d{6}Z\.md$/i,
37
+ /当前项目context-\d{8}T\d{6}Z\.md$/i,
38
+ /当前项目代码实现\.md$/i,
39
+ /(^|[-_.])(code-level-implementation|context-export|context-bundle)([-_.]|$)/i
40
+ ];
41
+ const CODE_FILE_EXTENSIONS = [
42
+ ".bat",
43
+ ".cjs",
44
+ ".cmd",
45
+ ".go",
46
+ ".gql",
47
+ ".graphql",
48
+ ".js",
49
+ ".jsx",
50
+ ".jsonc",
51
+ ".mjs",
52
+ ".proto",
53
+ ".ps1",
54
+ ".py",
55
+ ".sh",
56
+ ".sql",
57
+ ".toml",
58
+ ".ts",
59
+ ".tsx",
60
+ ".vue",
61
+ ".yaml",
62
+ ".yml"
63
+ ];
64
+ const CODE_FILE_BASE_NAMES = new Set([
65
+ ".env.example",
66
+ ".env.sample",
67
+ ".env.template",
68
+ "dockerfile",
69
+ "makefile",
70
+ "package.json",
71
+ "pyproject.toml",
72
+ "requirements.txt",
73
+ "setup.cfg",
74
+ "tsconfig.json"
75
+ ]);
76
+ const CONFIG_JSON_NAMES = new Set([
77
+ "babel.config.json",
78
+ "biome.json",
79
+ "composer.json",
80
+ "deno.json",
81
+ "eslint.config.json",
82
+ "jsconfig.json",
83
+ "package.json",
84
+ "tsconfig.json",
85
+ "vite.config.json"
86
+ ]);
87
+ export function shouldIncludeCodeFile(relative) {
88
+ if (shouldExcludeRelativePath(relative)) {
89
+ return false;
90
+ }
91
+ const normalized = toPosix(relative);
92
+ const lower = normalized.toLowerCase();
93
+ const base = path.posix.basename(lower);
94
+ if (CODE_FILE_BASE_NAMES.has(base) || base.startsWith("dockerfile.")) {
95
+ return true;
96
+ }
97
+ if (lower.endsWith(".dockerfile")) {
98
+ return true;
99
+ }
100
+ if (lower.endsWith(".json")) {
101
+ return isConfigJson(lower);
102
+ }
103
+ return CODE_FILE_EXTENSIONS.some((extension) => lower.endsWith(extension));
104
+ }
105
+ export function shouldExcludeRelativePath(relative) {
106
+ const normalized = toPosix(relative);
107
+ const segments = normalized.split("/");
108
+ if (segments.some((segment) => EXCLUDED_DIR_NAMES.has(segment))) {
109
+ return true;
110
+ }
111
+ const base = segments[segments.length - 1] ?? "";
112
+ const lowerBase = base.toLowerCase();
113
+ return EXCLUDED_FILE_PATTERNS.some((pattern) => {
114
+ if (SAFE_EXAMPLE_FILE_NAMES.has(lowerBase) && pattern.source.startsWith("^\\.env")) {
115
+ return false;
116
+ }
117
+ return pattern.test(base) || pattern.test(normalized);
118
+ });
119
+ }
120
+ export function toPosix(value) {
121
+ return value.replace(/\\/g, "/");
122
+ }
123
+ function isConfigJson(lowerRelative) {
124
+ const base = path.posix.basename(lowerRelative);
125
+ return (CONFIG_JSON_NAMES.has(base) ||
126
+ lowerRelative.endsWith(".schema.json") ||
127
+ lowerRelative.includes("/schema/") ||
128
+ lowerRelative.includes("/schemas/") ||
129
+ lowerRelative.includes("/config/") ||
130
+ lowerRelative.includes("/configs/") ||
131
+ lowerRelative.includes("/examples/") ||
132
+ lowerRelative.includes("/sample/") ||
133
+ lowerRelative.includes("/samples/") ||
134
+ base.includes("config") ||
135
+ base.includes("schema") ||
136
+ base.includes("example") ||
137
+ base.includes("sample"));
138
+ }
@@ -3,9 +3,21 @@ export interface HarnessConfig {
3
3
  package: string;
4
4
  schema_version: string;
5
5
  };
6
+ modularity?: HarnessModularityConfig;
6
7
  managed_files: ManagedFile[];
7
8
  never_overwrite: string[];
8
9
  }
10
+ export interface HarnessModularityConfig {
11
+ limit?: number;
12
+ policy?: "scoped_waivers" | "strict_except_generated";
13
+ waivers?: ModularityWaiverConfig[];
14
+ }
15
+ export interface ModularityWaiverConfig {
16
+ path?: string;
17
+ category?: string;
18
+ reason?: string;
19
+ future_split_boundary?: string;
20
+ }
9
21
  export interface ManagedFile {
10
22
  path: string;
11
23
  strategy: "merge-block" | "generated" | "generated-compat" | "managed" | "merge-with-local" | "create-if-missing";
@@ -1 +1,10 @@
1
+ export interface UpgradeRunReport {
2
+ lines: string[];
3
+ blocked: boolean;
4
+ }
5
+ export declare class UpgradeBlockedError extends Error {
6
+ readonly lines: string[];
7
+ constructor(lines: string[]);
8
+ }
1
9
  export declare function runUpgrade(projectRoot: string): Promise<string[]>;
10
+ export declare function runUpgradeReport(projectRoot: string): Promise<UpgradeRunReport>;