project-tiny-context-harness 0.2.60 → 0.2.62
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 +50 -19
- package/assets/README.md +57 -19
- package/assets/README.zh-CN.md +1 -1
- package/assets/agents/AGENTS_CORE.md +8 -5
- package/assets/skills/context_full_project_export/SKILL.md +42 -27
- package/assets/skills/plan_acceptance_checklist_compiler/SKILL.md +145 -78
- package/dist/commands/export-context-args.d.ts +21 -0
- package/dist/commands/export-context-args.js +149 -0
- package/dist/commands/export-context.js +55 -87
- package/dist/commands/index.js +2 -2
- package/dist/lib/source-pack-classify.d.ts +10 -0
- package/dist/lib/source-pack-classify.js +142 -0
- package/dist/lib/source-pack-config.d.ts +7 -0
- package/dist/lib/source-pack-config.js +93 -0
- package/dist/lib/source-pack-export.d.ts +2 -0
- package/dist/lib/source-pack-export.js +223 -0
- package/dist/lib/source-pack-manifest.d.ts +25 -0
- package/dist/lib/source-pack-manifest.js +93 -0
- package/dist/lib/source-pack-records.d.ts +11 -0
- package/dist/lib/source-pack-records.js +161 -0
- package/dist/lib/source-pack-render.d.ts +12 -0
- package/dist/lib/source-pack-render.js +235 -0
- package/dist/lib/source-pack-types.d.ts +77 -0
- package/dist/lib/source-pack-types.js +1 -0
- package/package.json +1 -1
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { parseArgs, helpText } from "./export-context-args.js";
|
|
1
2
|
import { runExportContext } from "../lib/context-export.js";
|
|
3
|
+
import { runSourcePackExport } from "../lib/source-pack-export.js";
|
|
2
4
|
export async function exportContext(args) {
|
|
3
5
|
let parsed;
|
|
4
6
|
try {
|
|
@@ -9,7 +11,7 @@ export async function exportContext(args) {
|
|
|
9
11
|
process.exitCode = 1;
|
|
10
12
|
return;
|
|
11
13
|
}
|
|
12
|
-
if (parsed.help || (!parsed.full && !parsed.code && !parsed.all)) {
|
|
14
|
+
if (parsed.help || (!parsed.full && !parsed.code && !parsed.all && !parsed.sourceMode)) {
|
|
13
15
|
console.log(helpText());
|
|
14
16
|
if (!parsed.help) {
|
|
15
17
|
process.exitCode = 1;
|
|
@@ -17,32 +19,25 @@ export async function exportContext(args) {
|
|
|
17
19
|
return;
|
|
18
20
|
}
|
|
19
21
|
try {
|
|
20
|
-
if (parsed.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
full: true,
|
|
24
|
-
check: parsed.check,
|
|
25
|
-
now
|
|
26
|
-
});
|
|
27
|
-
const codeReport = await runExportContext(process.cwd(), {
|
|
28
|
-
code: true,
|
|
22
|
+
if (parsed.sourceMode) {
|
|
23
|
+
printSourcePackReport(await runSourcePackExport(process.cwd(), {
|
|
24
|
+
mode: parsed.sourceMode,
|
|
29
25
|
check: parsed.check,
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
]);
|
|
26
|
+
command: `ty-context export-context ${args.join(" ")}`.trim(),
|
|
27
|
+
profile: parsed.profile,
|
|
28
|
+
includeContext: parsed.includeContext,
|
|
29
|
+
includeCode: parsed.includeCode,
|
|
30
|
+
bundleStrategy: parsed.bundleStrategy,
|
|
31
|
+
maxPackFiles: parsed.maxPackFiles,
|
|
32
|
+
maxBundleCharacters: parsed.maxBundleCharacters,
|
|
33
|
+
redactionStrict: parsed.redactionStrict,
|
|
34
|
+
prune: parsed.prune,
|
|
35
|
+
taskName: parsed.taskName
|
|
36
|
+
}), parsed.check);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (parsed.all) {
|
|
40
|
+
await runLegacyAll(parsed.check);
|
|
46
41
|
return;
|
|
47
42
|
}
|
|
48
43
|
const report = await runExportContext(process.cwd(), {
|
|
@@ -74,57 +69,42 @@ export async function exportContext(args) {
|
|
|
74
69
|
process.exitCode = 1;
|
|
75
70
|
}
|
|
76
71
|
}
|
|
77
|
-
function
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
parsed.output = value;
|
|
107
|
-
index += 1;
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
if (arg.startsWith("--output=")) {
|
|
111
|
-
const value = arg.slice("--output=".length).trim();
|
|
112
|
-
if (!value) {
|
|
113
|
-
throw new Error("export-context --output requires a path");
|
|
114
|
-
}
|
|
115
|
-
parsed.output = value;
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
throw new Error(`unknown export-context argument: ${arg}`);
|
|
119
|
-
}
|
|
120
|
-
const modeCount = Number(parsed.full) + Number(parsed.code) + Number(parsed.all);
|
|
121
|
-
if (modeCount > 1) {
|
|
122
|
-
throw new Error("export-context accepts exactly one of --full, --code or --all");
|
|
72
|
+
async function runLegacyAll(check) {
|
|
73
|
+
const now = new Date();
|
|
74
|
+
const fullReport = await runExportContext(process.cwd(), { full: true, check, now });
|
|
75
|
+
const codeReport = await runExportContext(process.cwd(), { code: true, check, now });
|
|
76
|
+
console.log(check ? "export-context check OK" : "export-context wrote artifacts");
|
|
77
|
+
console.log("mode: all");
|
|
78
|
+
console.log("outputs:");
|
|
79
|
+
console.log(`- full: ${fullReport.outputRelativePath}`);
|
|
80
|
+
console.log(`- code: ${codeReport.outputRelativePath}`);
|
|
81
|
+
console.log(`source context count: ${fullReport.sourceContextCount}`);
|
|
82
|
+
console.log(`source code count: ${codeReport.sourceCodeCount ?? codeReport.sourceFiles.length}`);
|
|
83
|
+
console.log(`total code lines: ${codeReport.totalLines ?? 0}`);
|
|
84
|
+
console.log(`total code characters: ${codeReport.totalCharacters ?? 0}`);
|
|
85
|
+
console.log("warnings:");
|
|
86
|
+
printWarnings([
|
|
87
|
+
...fullReport.warnings.map((warning) => `full: ${warning}`),
|
|
88
|
+
...codeReport.warnings.map((warning) => `code: ${warning}`)
|
|
89
|
+
]);
|
|
90
|
+
}
|
|
91
|
+
function printSourcePackReport(report, check) {
|
|
92
|
+
console.log(check ? "export-context check OK" : "export-context wrote artifacts");
|
|
93
|
+
console.log(`mode: ${report.mode}`);
|
|
94
|
+
console.log(`output directory: ${report.outputRelativePath}`);
|
|
95
|
+
console.log(`source code count: ${report.sourceCodeCount}`);
|
|
96
|
+
console.log(`total lines: ${report.totalLines}`);
|
|
97
|
+
console.log(`total characters: ${report.totalCharacters}`);
|
|
98
|
+
console.log("artifacts:");
|
|
99
|
+
for (const artifact of report.artifacts) {
|
|
100
|
+
console.log(`- ${artifact.kind}: ${artifact.path} (${artifact.characters} chars, ${artifact.source_count} sources)`);
|
|
123
101
|
}
|
|
124
|
-
|
|
125
|
-
|
|
102
|
+
console.log("recommended upload sets:");
|
|
103
|
+
for (const [name, files] of Object.entries(report.recommendedUploadSets)) {
|
|
104
|
+
console.log(`- ${name}: ${files.join(", ")}`);
|
|
126
105
|
}
|
|
127
|
-
|
|
106
|
+
console.log("warnings:");
|
|
107
|
+
printWarnings(report.warnings);
|
|
128
108
|
}
|
|
129
109
|
function printWarnings(warnings) {
|
|
130
110
|
if (warnings.length === 0) {
|
|
@@ -135,15 +115,3 @@ function printWarnings(warnings) {
|
|
|
135
115
|
console.log(`- ${warning}`);
|
|
136
116
|
}
|
|
137
117
|
}
|
|
138
|
-
function helpText() {
|
|
139
|
-
return `ty-context export-context:
|
|
140
|
-
export-context --full [--output tmp/ty-context/context-exports/<name>.md] [--check]
|
|
141
|
-
export-context --code [--output tmp/ty-context/context-exports/<name>.md] [--check]
|
|
142
|
-
export-context --all [--check]
|
|
143
|
-
|
|
144
|
-
Creates temporary Markdown artifacts for copying or external-tool ingestion.
|
|
145
|
-
--full exports the project Context summary as a full-project-context artifact.
|
|
146
|
-
--code exports one current implementation snapshot as a code-level-implementation artifact.
|
|
147
|
-
--all exports both default artifacts in one command.
|
|
148
|
-
The artifact must stay under tmp/ty-context/context-exports/** and must not be referenced from project_context/context.toml.`;
|
|
149
|
-
}
|
package/dist/commands/index.js
CHANGED
|
@@ -30,8 +30,8 @@ export function help() {
|
|
|
30
30
|
doctor Diagnose project configuration and drift
|
|
31
31
|
check-modularity --touched|--file <path>|--base <ref> [--limit 300] [--fail-on-warning]
|
|
32
32
|
Warn when selected handwritten source files exceed a line-count limit
|
|
33
|
-
export-context --full|--code|--all
|
|
34
|
-
Export
|
|
33
|
+
export-context --full|--code|--all|--source-pack|--code-index|--task-context
|
|
34
|
+
Export temporary Context, code snapshot or bounded Source Pack artifacts
|
|
35
35
|
validate <gate> Run a Harness validation gate
|
|
36
36
|
validate-context Validate Minimal Context fact-source recoverability
|
|
37
37
|
validate-code-modularity
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ContextAreaMapping } from "./source-pack-types.js";
|
|
2
|
+
export interface CodeClassification {
|
|
3
|
+
language: string;
|
|
4
|
+
summary: string;
|
|
5
|
+
tags: string[];
|
|
6
|
+
routes: string[];
|
|
7
|
+
score: number;
|
|
8
|
+
bucket: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function classifyCodeFile(relative: string, content: string, areas: ContextAreaMapping[]): CodeClassification;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { SAFE_EXAMPLE_FILE_NAMES } from "./source-files.js";
|
|
3
|
+
const OVERSIZED_LINES = 1000;
|
|
4
|
+
const OVERSIZED_CHARACTERS = 50_000;
|
|
5
|
+
export function classifyCodeFile(relative, content, areas) {
|
|
6
|
+
const language = languageFor(relative) || "text";
|
|
7
|
+
const routes = extractRouteSummary(content);
|
|
8
|
+
const tags = tagsFor(relative, content, language, routes);
|
|
9
|
+
return {
|
|
10
|
+
language,
|
|
11
|
+
summary: summarizeCodeFile(relative, content, language),
|
|
12
|
+
tags,
|
|
13
|
+
routes,
|
|
14
|
+
score: scoreFor(tags, relative),
|
|
15
|
+
bucket: routingBucket(relative, areas)
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function tagsFor(relative, content, language, routes) {
|
|
19
|
+
const lower = relative.toLowerCase();
|
|
20
|
+
const tags = new Set();
|
|
21
|
+
const lines = content.length === 0 ? 0 : content.split(/\r\n|\r|\n/).length;
|
|
22
|
+
if (isLikelyEntrypoint(relative))
|
|
23
|
+
tags.add("entry");
|
|
24
|
+
if (routes.length > 0 || /route|controller|api/.test(lower))
|
|
25
|
+
tags.add("api");
|
|
26
|
+
if (/cli|commands|bin\//.test(lower))
|
|
27
|
+
tags.add("cli");
|
|
28
|
+
if (/worker|scheduler|queue|runtime|cron|job/.test(lower))
|
|
29
|
+
tags.add("worker");
|
|
30
|
+
if (/schema|contract|types?\.|\.d\.ts$/.test(lower) || /\b(interface|type|schema)\b/.test(content))
|
|
31
|
+
tags.add("contract");
|
|
32
|
+
if (/pages|views|screens|components|\.vue$|\.tsx$|\.jsx$/.test(lower))
|
|
33
|
+
tags.add("ui");
|
|
34
|
+
if (/test|spec|verification|makefile/.test(lower))
|
|
35
|
+
tags.add("test");
|
|
36
|
+
if (["json", "yaml", "toml", "make", "dockerfile"].includes(language))
|
|
37
|
+
tags.add("config");
|
|
38
|
+
if (lines > OVERSIZED_LINES || content.length > OVERSIZED_CHARACTERS)
|
|
39
|
+
tags.add("oversized");
|
|
40
|
+
return [...tags].sort();
|
|
41
|
+
}
|
|
42
|
+
function scoreFor(tags, relative) {
|
|
43
|
+
const weights = { entry: 50, api: 35, cli: 32, worker: 30, contract: 28, ui: 24, test: 18, config: 12, oversized: -15 };
|
|
44
|
+
return tags.reduce((sum, tag) => sum + (weights[tag] ?? 0), 0) + Math.max(0, 20 - relative.split("/").length);
|
|
45
|
+
}
|
|
46
|
+
function routingBucket(relative, areas) {
|
|
47
|
+
for (const area of areas) {
|
|
48
|
+
if (area.root === "." || relative === area.root || relative.startsWith(`${area.root}/`)) {
|
|
49
|
+
return `area:${area.id}`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const parts = relative.split("/");
|
|
53
|
+
for (const prefix of ["domains", "apps", "services", "packages", "tools"]) {
|
|
54
|
+
if (parts[0] === prefix && parts[1]) {
|
|
55
|
+
return `${prefix}:${parts[1]}`;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return parts[0] || "misc";
|
|
59
|
+
}
|
|
60
|
+
function summarizeCodeFile(relative, content, language) {
|
|
61
|
+
const base = path.posix.basename(relative);
|
|
62
|
+
const symbols = extractSymbolSummary(content, language);
|
|
63
|
+
if (base === "package.json") {
|
|
64
|
+
const packageName = /"name"\s*:\s*"([^"]+)"/.exec(content)?.[1];
|
|
65
|
+
return packageName ? `Defines npm package ${packageName} metadata, scripts and dependencies.` : "Defines npm package metadata, scripts and dependencies.";
|
|
66
|
+
}
|
|
67
|
+
if (base.toLowerCase() === "makefile") {
|
|
68
|
+
const targets = [...content.matchAll(/^([A-Za-z0-9_.-]+):/gm)].map((match) => match[1]).slice(0, 6);
|
|
69
|
+
return targets.length > 0 ? `Defines Make targets ${targets.join(", ")}.` : "Defines Make targets for local automation.";
|
|
70
|
+
}
|
|
71
|
+
if (symbols.length > 0) {
|
|
72
|
+
return `${describeFilePurpose(relative, language)}; exposes ${symbols.slice(0, 6).join(", ")}.`;
|
|
73
|
+
}
|
|
74
|
+
return `${describeFilePurpose(relative, language)}.`;
|
|
75
|
+
}
|
|
76
|
+
function extractSymbolSummary(content, language) {
|
|
77
|
+
const patterns = language === "python"
|
|
78
|
+
? [/^(?:async\s+)?def\s+([A-Za-z_][\w]*)/gm, /^class\s+([A-Za-z_][\w]*)/gm]
|
|
79
|
+
: language === "go"
|
|
80
|
+
? [/^func\s+(?:\([^)]+\)\s*)?([A-Za-z_][\w]*)/gm, /^type\s+([A-Za-z_][\w]*)/gm]
|
|
81
|
+
: [
|
|
82
|
+
/(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)/g,
|
|
83
|
+
/(?:export\s+)?class\s+([A-Za-z_$][\w$]*)/g,
|
|
84
|
+
/(?:export\s+)?interface\s+([A-Za-z_$][\w$]*)/g,
|
|
85
|
+
/(?:export\s+)?type\s+([A-Za-z_$][\w$]*)/g,
|
|
86
|
+
/(?:export\s+)?const\s+([A-Za-z_$][\w$]*)/g
|
|
87
|
+
];
|
|
88
|
+
const symbols = new Set();
|
|
89
|
+
for (const pattern of patterns) {
|
|
90
|
+
let match;
|
|
91
|
+
while ((match = pattern.exec(content)) && symbols.size < 10)
|
|
92
|
+
symbols.add(match[1]);
|
|
93
|
+
}
|
|
94
|
+
for (const route of extractRouteSummary(content)) {
|
|
95
|
+
if (symbols.size < 10)
|
|
96
|
+
symbols.add(route);
|
|
97
|
+
}
|
|
98
|
+
return [...symbols];
|
|
99
|
+
}
|
|
100
|
+
function extractRouteSummary(content) {
|
|
101
|
+
const routes = new Set();
|
|
102
|
+
const patterns = [/\.(get|post|put|patch|delete|head|options)\s*\(\s*["'`]([^"'`]+)["'`]/gi, /\bHandle(?:Func)?\s*\(\s*["'`]([^"'`]+)["'`]/g];
|
|
103
|
+
for (const pattern of patterns) {
|
|
104
|
+
let match;
|
|
105
|
+
while ((match = pattern.exec(content)) && routes.size < 8) {
|
|
106
|
+
routes.add(match.length === 3 ? `${match[1].toUpperCase()} ${match[2]}` : `route ${match[1]}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return [...routes].sort();
|
|
110
|
+
}
|
|
111
|
+
function describeFilePurpose(relative, language) {
|
|
112
|
+
const lower = relative.toLowerCase();
|
|
113
|
+
if (lower.includes("/test") || lower.includes(".test.") || lower.includes(".spec."))
|
|
114
|
+
return `Contains ${language} tests for ${path.posix.basename(relative)}`;
|
|
115
|
+
if (lower.includes("/commands/"))
|
|
116
|
+
return `Implements ${language} command handling for ${path.posix.basename(relative)}`;
|
|
117
|
+
if (lower.includes("/cli") || lower.endsWith("/cli.ts") || lower.endsWith("/cli.js"))
|
|
118
|
+
return `Implements ${language} CLI behavior for ${path.posix.basename(relative)}`;
|
|
119
|
+
if (lower.includes("/components/") || lower.includes("/pages/") || lower.includes("/views/"))
|
|
120
|
+
return `Implements ${language} UI behavior for ${path.posix.basename(relative)}`;
|
|
121
|
+
return `Contains ${language} implementation for ${path.posix.basename(relative)}`;
|
|
122
|
+
}
|
|
123
|
+
function isLikelyEntrypoint(relative) {
|
|
124
|
+
const lower = relative.toLowerCase();
|
|
125
|
+
const base = path.posix.basename(lower);
|
|
126
|
+
return ["package.json", "makefile", "dockerfile", "main.go", "app.py", "server.py", "index.ts", "index.js", "cli.ts", "cli.js"].includes(base) || /\/src\/(main|app|index)\.[tj]sx?$/.test(lower);
|
|
127
|
+
}
|
|
128
|
+
function languageFor(relative) {
|
|
129
|
+
const lower = relative.toLowerCase();
|
|
130
|
+
const base = path.posix.basename(lower);
|
|
131
|
+
if (base === "makefile")
|
|
132
|
+
return "make";
|
|
133
|
+
if (base === "dockerfile" || base.startsWith("dockerfile.") || lower.endsWith(".dockerfile"))
|
|
134
|
+
return "dockerfile";
|
|
135
|
+
if (SAFE_EXAMPLE_FILE_NAMES.has(base))
|
|
136
|
+
return "dotenv";
|
|
137
|
+
for (const [extension, language] of [[".tsx", "tsx"], [".ts", "typescript"], [".jsx", "jsx"], [".js", "javascript"], [".mjs", "javascript"], [".py", "python"], [".go", "go"], [".vue", "vue"], [".sql", "sql"], [".json", "json"], [".jsonc", "jsonc"], [".yaml", "yaml"], [".yml", "yaml"], [".toml", "toml"], [".sh", "bash"], [".ps1", "powershell"], [".cmd", "batch"], [".bat", "batch"], [".graphql", "graphql"], [".gql", "graphql"], [".proto", "protobuf"]]) {
|
|
138
|
+
if (lower.endsWith(extension))
|
|
139
|
+
return language;
|
|
140
|
+
}
|
|
141
|
+
return "";
|
|
142
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ContextAreaMapping, SourcePackProfile } from "./source-pack-types.js";
|
|
2
|
+
export declare function readSourcePackProfile(projectRoot: string, profileId?: string): Promise<SourcePackProfile>;
|
|
3
|
+
export declare function parseContextAreas(projectRoot: string): Promise<ContextAreaMapping[]>;
|
|
4
|
+
export declare function mergePatterns(...groups: Array<string[] | undefined>): string[];
|
|
5
|
+
export declare function validatePatternList(values: string[], label: string): string[];
|
|
6
|
+
export declare function matchesAny(relative: string, patterns: string[]): boolean;
|
|
7
|
+
export declare function normalizeRepoPath(value: string, label: string): string;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { harnessConfigPath } from "./harness-root.js";
|
|
3
|
+
import { pathExists, readText } from "./fs.js";
|
|
4
|
+
import { parseYaml } from "./yaml.js";
|
|
5
|
+
import { toPosix } from "./source-files.js";
|
|
6
|
+
export async function readSourcePackProfile(projectRoot, profileId) {
|
|
7
|
+
if (!profileId) {
|
|
8
|
+
return { context: [], code: [], exclude: [], verification: [] };
|
|
9
|
+
}
|
|
10
|
+
const configPath = path.join(projectRoot, await harnessConfigPath(projectRoot));
|
|
11
|
+
if (!(await pathExists(configPath))) {
|
|
12
|
+
throw new Error(`source pack profile not found: ${profileId}`);
|
|
13
|
+
}
|
|
14
|
+
const parsed = parseYaml(await readText(configPath));
|
|
15
|
+
const sourcePacks = parsed && typeof parsed === "object" ? parsed.source_packs : undefined;
|
|
16
|
+
const profileValue = sourcePacks && typeof sourcePacks === "object" ? sourcePacks[profileId] : undefined;
|
|
17
|
+
if (!profileValue || typeof profileValue !== "object" || Array.isArray(profileValue)) {
|
|
18
|
+
throw new Error(`source pack profile not found: ${profileId}`);
|
|
19
|
+
}
|
|
20
|
+
const value = profileValue;
|
|
21
|
+
return {
|
|
22
|
+
context: readPatternList(value.context, `source_packs.${profileId}.context`, "context path"),
|
|
23
|
+
code: readPatternList(value.code, `source_packs.${profileId}.code`, "code path"),
|
|
24
|
+
exclude: readPatternList(value.exclude, `source_packs.${profileId}.exclude`, "exclude path"),
|
|
25
|
+
verification: readStringList(value.verification, `source_packs.${profileId}.verification`),
|
|
26
|
+
maxBundleCharacters: readPositiveInteger(value.max_bundle_characters, `source_packs.${profileId}.max_bundle_characters`)
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export async function parseContextAreas(projectRoot) {
|
|
30
|
+
const manifestPath = path.join(projectRoot, "project_context", "context.toml");
|
|
31
|
+
if (!(await pathExists(manifestPath))) {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
const content = await readText(manifestPath);
|
|
35
|
+
const areas = [];
|
|
36
|
+
for (const block of content.split(/\[\[areas\]\]/g).slice(1)) {
|
|
37
|
+
const id = /id\s*=\s*"([^"]+)"/.exec(block)?.[1];
|
|
38
|
+
const root = /root\s*=\s*"([^"]+)"/.exec(block)?.[1];
|
|
39
|
+
const context = /context\s*=\s*"([^"]+)"/.exec(block)?.[1];
|
|
40
|
+
if (id && root && context) {
|
|
41
|
+
areas.push({ id, root: normalizeRepoPath(root, "area root"), context: normalizeRepoPath(context, "area context") });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return areas.sort((left, right) => left.id.localeCompare(right.id));
|
|
45
|
+
}
|
|
46
|
+
export function mergePatterns(...groups) {
|
|
47
|
+
return [...new Set(groups.flatMap((group) => group ?? []).map((value) => normalizeRepoPath(value, "include path")))].sort();
|
|
48
|
+
}
|
|
49
|
+
export function validatePatternList(values, label) {
|
|
50
|
+
return values.map((value) => normalizeRepoPath(value, label));
|
|
51
|
+
}
|
|
52
|
+
export function matchesAny(relative, patterns) {
|
|
53
|
+
return patterns.some((pattern) => globToRegExp(pattern).test(relative));
|
|
54
|
+
}
|
|
55
|
+
export function normalizeRepoPath(value, label) {
|
|
56
|
+
const normalized = toPosix(value.trim()).replace(/^\.\//, "");
|
|
57
|
+
if (!isAllowedPatternPath(normalized)) {
|
|
58
|
+
throw new Error(`${label} must be repo-relative and stay inside the workspace: ${value}`);
|
|
59
|
+
}
|
|
60
|
+
return normalized;
|
|
61
|
+
}
|
|
62
|
+
function readPatternList(value, label, pathLabel) {
|
|
63
|
+
return readStringList(value, label).map((item) => normalizeRepoPath(item, pathLabel));
|
|
64
|
+
}
|
|
65
|
+
function readStringList(value, label) {
|
|
66
|
+
if (value === undefined) {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.trim().length === 0)) {
|
|
70
|
+
throw new Error(`<harnessRoot>/config.yaml ${label} must be an array of non-empty strings`);
|
|
71
|
+
}
|
|
72
|
+
return value.map((item) => item.trim());
|
|
73
|
+
}
|
|
74
|
+
function readPositiveInteger(value, label) {
|
|
75
|
+
if (value === undefined) {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
if (!Number.isInteger(value) || Number(value) <= 0) {
|
|
79
|
+
throw new Error(`<harnessRoot>/config.yaml ${label} must be a positive integer`);
|
|
80
|
+
}
|
|
81
|
+
return Number(value);
|
|
82
|
+
}
|
|
83
|
+
function isAllowedPatternPath(value) {
|
|
84
|
+
return value.length > 0 && !path.posix.isAbsolute(value) && !value.split("/").includes("..");
|
|
85
|
+
}
|
|
86
|
+
function globToRegExp(pattern) {
|
|
87
|
+
const escaped = pattern
|
|
88
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
89
|
+
.replace(/\*\*/g, "\0")
|
|
90
|
+
.replace(/\*/g, "[^/]*")
|
|
91
|
+
.replace(/\0/g, ".*");
|
|
92
|
+
return new RegExp(`^${escaped}$`);
|
|
93
|
+
}
|