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.
- package/README.md +49 -14
- package/assets/README.md +42 -11
- package/assets/agents/AGENTS_CORE.md +2 -2
- package/assets/github/harness.yml +13 -9
- package/assets/make/sdlc-harness.mk +21 -12
- package/assets/skills/context_development_engineer/SKILL.md +7 -0
- package/assets/skills/context_harness_upgrade/SKILL.md +3 -2
- package/dist/commands/check-modularity.d.ts +1 -0
- package/dist/commands/check-modularity.js +145 -0
- package/dist/commands/index.js +13 -6
- package/dist/commands/upgrade.js +10 -4
- package/dist/lib/config.js +5 -0
- package/dist/lib/context-export.js +1 -137
- package/dist/lib/migrations.d.ts +1 -1
- package/dist/lib/migrations.js +23 -5
- package/dist/lib/modularity.d.ts +27 -0
- package/dist/lib/modularity.js +233 -0
- package/dist/lib/source-files.d.ts +4 -0
- package/dist/lib/source-files.js +138 -0
- package/dist/lib/types.d.ts +12 -0
- package/dist/lib/upgrade.d.ts +9 -0
- package/dist/lib/upgrade.js +27 -7
- package/dist/lib/validators.js +35 -2
- package/package.json +1 -1
package/dist/lib/config.js
CHANGED
|
@@ -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
|
}
|
package/dist/lib/migrations.d.ts
CHANGED
|
@@ -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>;
|
package/dist/lib/migrations.js
CHANGED
|
@@ -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,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
|
+
}
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -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";
|
package/dist/lib/upgrade.d.ts
CHANGED
|
@@ -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>;
|