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
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { artifactReport, buildManifest, pruneTimestampedExports, readPackageVersion, timestampForFile, writeArtifactSet } from "./source-pack-manifest.js";
|
|
3
|
+
import { mergePatterns, parseContextAreas, readSourcePackProfile, validatePatternList } from "./source-pack-config.js";
|
|
4
|
+
import { collectCodeRecords, collectContextArtifacts, countRedactionWarnings, repoRelative } from "./source-pack-records.js";
|
|
5
|
+
import { renderBundleArtifact, renderCodeIndexArtifact, renderFullProjectContextArtifact, renderTaskContextArtifact } from "./source-pack-render.js";
|
|
6
|
+
const DEFAULT_EXPORT_DIR = "tmp/ty-context/context-exports";
|
|
7
|
+
const DEFAULT_MAX_PACK_FILES = 5;
|
|
8
|
+
const DEFAULT_MAX_BUNDLE_CHARACTERS = 800_000;
|
|
9
|
+
export async function runSourcePackExport(projectRoot, options) {
|
|
10
|
+
const maxPackFiles = options.maxPackFiles ?? DEFAULT_MAX_PACK_FILES;
|
|
11
|
+
validateMaxPackFiles(options.mode, maxPackFiles);
|
|
12
|
+
const now = options.now ?? new Date();
|
|
13
|
+
const timestamp = timestampForFile(now);
|
|
14
|
+
const timestampDir = path.join(projectRoot, ...DEFAULT_EXPORT_DIR.split("/"), timestamp);
|
|
15
|
+
const generatedAt = now.toISOString();
|
|
16
|
+
const warnings = [];
|
|
17
|
+
const areas = await parseContextAreas(projectRoot);
|
|
18
|
+
const profile = await readSourcePackProfile(projectRoot, options.profile);
|
|
19
|
+
const includeCode = mergePatterns(profile.code, validatePatternList(options.includeCode ?? [], "include code path"));
|
|
20
|
+
const includeContext = mergePatterns(profile.context, validatePatternList(options.includeContext ?? [], "include context path"));
|
|
21
|
+
const exclude = mergePatterns(profile.exclude);
|
|
22
|
+
const maxBundleCharacters = options.maxBundleCharacters ?? profile.maxBundleCharacters ?? DEFAULT_MAX_BUNDLE_CHARACTERS;
|
|
23
|
+
const bundleStrategy = options.bundleStrategy ?? "auto";
|
|
24
|
+
const records = await collectCodeRecords(projectRoot, warnings, {
|
|
25
|
+
include: includeCode.length > 0 ? includeCode : undefined,
|
|
26
|
+
exclude,
|
|
27
|
+
areas
|
|
28
|
+
});
|
|
29
|
+
const contexts = await collectContextArtifacts(projectRoot, warnings, options.mode === "task-context" && includeContext.length > 0 ? includeContext : undefined);
|
|
30
|
+
const taskName = options.taskName ?? "";
|
|
31
|
+
const command = options.command ?? commandFor(options);
|
|
32
|
+
const toolVersion = await readPackageVersion();
|
|
33
|
+
const recommendedUploadSets = uploadSets(options.mode, timestamp, taskName);
|
|
34
|
+
const artifacts = [];
|
|
35
|
+
if (options.mode === "code-index" || options.mode === "source-pack" || options.mode === "code-bundles" || options.mode === "task-context") {
|
|
36
|
+
assignBundles(records, maxBundleCharacters, bundleStrategy);
|
|
37
|
+
artifacts.push(markdownArtifact("code-index", "code-index.md", renderCodeIndexArtifact(records, areas, warnings, meta(generatedAt, "code-index.md", command, toolVersion)), records.length, sumLines(records), warnings.length));
|
|
38
|
+
}
|
|
39
|
+
if (options.mode === "source-pack" || options.mode === "task-context") {
|
|
40
|
+
artifacts.splice(0, 0, markdownArtifact("full-project-context", "full-project-context.md", renderFullProjectContextArtifact(contexts, warnings, meta(generatedAt, "full-project-context.md", command, toolVersion)), contexts.length, sumContextLines(contexts), warnings.length));
|
|
41
|
+
}
|
|
42
|
+
if (options.mode === "source-pack" || options.mode === "code-bundles") {
|
|
43
|
+
addDefaultBundles(artifacts, records, contexts, maxPackFiles, warnings, generatedAt, command, toolVersion);
|
|
44
|
+
}
|
|
45
|
+
if (options.mode === "task-context") {
|
|
46
|
+
addTaskArtifacts(artifacts, records, contexts, profile.verification, maxPackFiles, warnings, generatedAt, command, toolVersion, taskName, maxBundleCharacters, bundleStrategy);
|
|
47
|
+
}
|
|
48
|
+
const omitted = omittedSummary(records);
|
|
49
|
+
const nonManifestArtifacts = artifacts.slice();
|
|
50
|
+
const manifestPath = `${DEFAULT_EXPORT_DIR}/${timestamp}/source-pack-manifest.json`;
|
|
51
|
+
const manifest = await buildManifest({
|
|
52
|
+
projectRoot,
|
|
53
|
+
generatedAt,
|
|
54
|
+
command,
|
|
55
|
+
maxPackFiles,
|
|
56
|
+
artifacts: nonManifestArtifacts.map((artifact) => ({ ...artifact, relativePath: `${DEFAULT_EXPORT_DIR}/${timestamp}/${artifact.name}` })),
|
|
57
|
+
warnings,
|
|
58
|
+
omitted,
|
|
59
|
+
recommendedUploadSets
|
|
60
|
+
});
|
|
61
|
+
artifacts.unshift({
|
|
62
|
+
kind: "manifest",
|
|
63
|
+
name: "source-pack-manifest.json",
|
|
64
|
+
relativePath: manifestPath,
|
|
65
|
+
content: manifest,
|
|
66
|
+
sourceCount: 0,
|
|
67
|
+
sourceLineCount: 0,
|
|
68
|
+
warningCount: warnings.length
|
|
69
|
+
});
|
|
70
|
+
for (const artifact of artifacts) {
|
|
71
|
+
if (!artifact.relativePath) {
|
|
72
|
+
artifact.relativePath = `${DEFAULT_EXPORT_DIR}/${timestamp}/${artifact.name}`;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (artifacts.length > maxPackFiles) {
|
|
76
|
+
throw new Error(`Source Pack mode ${options.mode} planned ${artifacts.length} files; max is ${maxPackFiles}`);
|
|
77
|
+
}
|
|
78
|
+
const redactionCount = countRedactionWarnings(warnings);
|
|
79
|
+
if (options.redactionStrict && redactionCount > 0) {
|
|
80
|
+
throw new Error(`export-context --redaction-strict found ${redactionCount} redaction warning(s)`);
|
|
81
|
+
}
|
|
82
|
+
if (!options.check) {
|
|
83
|
+
await writeArtifactSet(projectRoot, timestampDir, artifacts);
|
|
84
|
+
if (options.prune !== undefined) {
|
|
85
|
+
await pruneTimestampedExports(projectRoot, options.prune);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
mode: options.mode,
|
|
90
|
+
outputDirectory: timestampDir,
|
|
91
|
+
outputRelativePath: repoRelative(projectRoot, timestampDir),
|
|
92
|
+
artifacts: artifacts.map(artifactReport),
|
|
93
|
+
sourceFiles: records.map((record) => record.relative),
|
|
94
|
+
sourceCodeCount: records.length,
|
|
95
|
+
totalLines: sumLines(records),
|
|
96
|
+
totalCharacters: records.reduce((sum, record) => sum + record.characters, 0),
|
|
97
|
+
redactionCount,
|
|
98
|
+
warnings,
|
|
99
|
+
omitted,
|
|
100
|
+
recommendedUploadSets,
|
|
101
|
+
wrote: !options.check
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function addDefaultBundles(artifacts, records, contexts, maxPackFiles, warnings, generatedAt, command, toolVersion) {
|
|
105
|
+
const omitted = omittedSummary(records);
|
|
106
|
+
const slots = maxPackFiles - artifacts.length - 1;
|
|
107
|
+
const core = records.filter((record) => record.bundle === "core");
|
|
108
|
+
const extended = records.filter((record) => record.bundle === "extended");
|
|
109
|
+
if (slots >= 1 && core.length > 0) {
|
|
110
|
+
artifacts.push(markdownArtifact("code-bundle-core", "code-bundle-core.md", renderBundleArtifact("Code Bundle Core", core, contexts, omitted, warnings, meta(generatedAt, "code-bundle-core.md", command, toolVersion), "Deterministic score-first selection of entry/API/CLI/contract/UI/test files; routing buckets are export routing only."), core.length, sumLines(core), warnings.length));
|
|
111
|
+
}
|
|
112
|
+
if (slots >= 2 && extended.length > 0) {
|
|
113
|
+
artifacts.push(markdownArtifact("code-bundle-extended", "code-bundle-extended.md", renderBundleArtifact("Code Bundle Extended", extended, contexts, omitted, warnings, meta(generatedAt, "code-bundle-extended.md", command, toolVersion), "Next highest deterministic scores after core bundle until the bundle character budget is reached."), extended.length, sumLines(extended), warnings.length));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function addTaskArtifacts(artifacts, records, contexts, verification, maxPackFiles, warnings, generatedAt, command, toolVersion, taskName, maxBundleCharacters, bundleStrategy) {
|
|
117
|
+
const slug = taskSlug(taskName);
|
|
118
|
+
const sorted = sortedRecords(records, bundleStrategy);
|
|
119
|
+
assignTaskBundles(sorted, maxBundleCharacters);
|
|
120
|
+
const taskRecords = sorted.filter((record) => record.bundle === "task");
|
|
121
|
+
const supportRecords = sorted.filter((record) => record.bundle === "task-support");
|
|
122
|
+
const omitted = omittedSummary(sorted);
|
|
123
|
+
artifacts.push(markdownArtifact("task-context", `task-contexts/task-context-${slug}.md`, renderTaskContextArtifact(taskName, contexts, taskRecords, verification, omitted, warnings, meta(generatedAt, `task-contexts/task-context-${slug}.md`, command, toolVersion)), contexts.length + taskRecords.length, sumContextLines(contexts) + sumLines(taskRecords), warnings.length));
|
|
124
|
+
const slots = maxPackFiles - artifacts.length - 1;
|
|
125
|
+
if (slots >= 1 && supportRecords.length > 0) {
|
|
126
|
+
artifacts.push(markdownArtifact("code-bundle-task-support", "code-bundle-task-support.md", renderBundleArtifact("Code Bundle Task Support", supportRecords, contexts, omitted, warnings, meta(generatedAt, "code-bundle-task-support.md", command, toolVersion), "Deterministic support files selected after the main task-context body."), supportRecords.length, sumLines(supportRecords), warnings.length));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function assignBundles(records, maxCharacters, bundleStrategy) {
|
|
130
|
+
let coreCharacters = 0;
|
|
131
|
+
let extendedCharacters = 0;
|
|
132
|
+
for (const record of sortedRecords(records, bundleStrategy)) {
|
|
133
|
+
if (coreCharacters + record.characters <= maxCharacters) {
|
|
134
|
+
record.bundle = "core";
|
|
135
|
+
coreCharacters += record.characters;
|
|
136
|
+
}
|
|
137
|
+
else if (extendedCharacters + record.characters <= maxCharacters) {
|
|
138
|
+
record.bundle = "extended";
|
|
139
|
+
extendedCharacters += record.characters;
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
record.bundle = "omitted";
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function assignTaskBundles(records, maxCharacters) {
|
|
147
|
+
let taskCharacters = 0;
|
|
148
|
+
let supportCharacters = 0;
|
|
149
|
+
for (const record of records) {
|
|
150
|
+
if (taskCharacters + record.characters <= maxCharacters) {
|
|
151
|
+
record.bundle = "task";
|
|
152
|
+
taskCharacters += record.characters;
|
|
153
|
+
}
|
|
154
|
+
else if (supportCharacters + record.characters <= maxCharacters) {
|
|
155
|
+
record.bundle = "task-support";
|
|
156
|
+
supportCharacters += record.characters;
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
record.bundle = "omitted";
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function sortedRecords(records, bundleStrategy) {
|
|
164
|
+
return [...records].sort((left, right) => {
|
|
165
|
+
if (bundleStrategy === "area" || bundleStrategy === "config") {
|
|
166
|
+
return left.bucket.localeCompare(right.bucket) || right.score - left.score || left.relative.localeCompare(right.relative);
|
|
167
|
+
}
|
|
168
|
+
if (bundleStrategy === "topdir") {
|
|
169
|
+
return topDir(left).localeCompare(topDir(right)) || right.score - left.score || left.relative.localeCompare(right.relative);
|
|
170
|
+
}
|
|
171
|
+
return right.score - left.score || left.bucket.localeCompare(right.bucket) || left.relative.localeCompare(right.relative);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
function topDir(record) {
|
|
175
|
+
return record.relative.split("/")[0] || ".";
|
|
176
|
+
}
|
|
177
|
+
function omittedSummary(records) {
|
|
178
|
+
const omitted = records.filter((record) => record.bundle === "omitted");
|
|
179
|
+
return { source_file_count: omitted.length, reason_counts: omitted.length > 0 ? { bundle_budget: omitted.length } : {} };
|
|
180
|
+
}
|
|
181
|
+
function markdownArtifact(kind, name, content, sourceCount, sourceLineCount, warningCount) {
|
|
182
|
+
return { kind, name, relativePath: "", content, sourceCount, sourceLineCount, warningCount };
|
|
183
|
+
}
|
|
184
|
+
function meta(generatedAt, artifactName, command, toolVersion) {
|
|
185
|
+
return { generatedAt, outputPath: `tmp/ty-context/context-exports/latest/${artifactName}`, command, toolVersion };
|
|
186
|
+
}
|
|
187
|
+
function uploadSets(mode, timestamp, taskName) {
|
|
188
|
+
const base = `${DEFAULT_EXPORT_DIR}/${timestamp}`;
|
|
189
|
+
const sets = {
|
|
190
|
+
daily_planning: [`${base}/full-project-context.md`, `${base}/code-index.md`],
|
|
191
|
+
cross_module_review: [`${base}/full-project-context.md`, `${base}/code-index.md`, `${base}/code-bundle-core.md`, `${base}/code-bundle-extended.md`],
|
|
192
|
+
full_fallback: [`${base}/full-project-context.md`, `${base}/code-index.md`, `${DEFAULT_EXPORT_DIR}/code-level-implementation-<timestamp>/code-level-implementation.md`]
|
|
193
|
+
};
|
|
194
|
+
if (mode === "task-context") {
|
|
195
|
+
sets.focused_task_handoff = [`${base}/full-project-context.md`, `${base}/code-index.md`, `${base}/task-contexts/task-context-${taskSlug(taskName)}.md`];
|
|
196
|
+
}
|
|
197
|
+
return sets;
|
|
198
|
+
}
|
|
199
|
+
function validateMaxPackFiles(mode, maxPackFiles) {
|
|
200
|
+
if (!Number.isInteger(maxPackFiles) || maxPackFiles <= 0) {
|
|
201
|
+
throw new Error("export-context --max-pack-files requires a positive integer");
|
|
202
|
+
}
|
|
203
|
+
if (maxPackFiles > DEFAULT_MAX_PACK_FILES) {
|
|
204
|
+
throw new Error("export-context --max-pack-files cannot exceed 5 for Source Pack modes");
|
|
205
|
+
}
|
|
206
|
+
const required = mode === "code-index" ? 2 : mode === "task-context" ? 4 : mode === "code-bundles" ? 3 : 3;
|
|
207
|
+
if (maxPackFiles < required) {
|
|
208
|
+
throw new Error(`export-context ${mode} requires --max-pack-files >= ${required}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function commandFor(options) {
|
|
212
|
+
const flag = options.mode === "task-context" ? `--task-context ${options.taskName ?? ""}` : `--${options.mode}`;
|
|
213
|
+
return `ty-context export-context ${flag}`;
|
|
214
|
+
}
|
|
215
|
+
function taskSlug(value) {
|
|
216
|
+
return value.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "task";
|
|
217
|
+
}
|
|
218
|
+
function sumLines(records) {
|
|
219
|
+
return records.reduce((sum, record) => sum + record.lines, 0);
|
|
220
|
+
}
|
|
221
|
+
function sumContextLines(contexts) {
|
|
222
|
+
return contexts.reduce((sum, context) => sum + context.lines, 0);
|
|
223
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { SourcePackArtifactReport, SourcePackOmitted } from "./source-pack-types.js";
|
|
2
|
+
export interface PendingArtifact {
|
|
3
|
+
kind: string;
|
|
4
|
+
name: string;
|
|
5
|
+
relativePath: string;
|
|
6
|
+
content: string;
|
|
7
|
+
sourceCount: number;
|
|
8
|
+
sourceLineCount: number;
|
|
9
|
+
warningCount: number;
|
|
10
|
+
}
|
|
11
|
+
export declare function buildManifest(params: {
|
|
12
|
+
projectRoot: string;
|
|
13
|
+
generatedAt: string;
|
|
14
|
+
command: string;
|
|
15
|
+
maxPackFiles: number;
|
|
16
|
+
artifacts: PendingArtifact[];
|
|
17
|
+
warnings: string[];
|
|
18
|
+
omitted: SourcePackOmitted;
|
|
19
|
+
recommendedUploadSets: Record<string, string[]>;
|
|
20
|
+
}): Promise<string>;
|
|
21
|
+
export declare function artifactReport(artifact: PendingArtifact): SourcePackArtifactReport;
|
|
22
|
+
export declare function writeArtifactSet(projectRoot: string, timestampDir: string, artifacts: PendingArtifact[]): Promise<void>;
|
|
23
|
+
export declare function pruneTimestampedExports(projectRoot: string, keepCount: number): Promise<void>;
|
|
24
|
+
export declare function timestampForFile(now: Date): string;
|
|
25
|
+
export declare function readPackageVersion(): Promise<string>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import { promises as fs } from "node:fs";
|
|
5
|
+
import { ensureDir, writeTextIfChanged } from "./fs.js";
|
|
6
|
+
import { repoRelative, sha256 } from "./source-pack-records.js";
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
export async function buildManifest(params) {
|
|
9
|
+
const git = await gitInfo(params.projectRoot);
|
|
10
|
+
const manifest = {
|
|
11
|
+
schema_version: "source-pack-v1",
|
|
12
|
+
generated_at: params.generatedAt,
|
|
13
|
+
tool: "ty-context export-context",
|
|
14
|
+
tool_version: await readPackageVersion(),
|
|
15
|
+
git_sha: git.sha,
|
|
16
|
+
git_dirty: git.dirty,
|
|
17
|
+
command: params.command,
|
|
18
|
+
max_pack_files: params.maxPackFiles,
|
|
19
|
+
artifacts: params.artifacts.map((artifact) => artifactReport(artifact)),
|
|
20
|
+
warnings: params.warnings,
|
|
21
|
+
omitted: params.omitted,
|
|
22
|
+
recommended_upload_sets: params.recommendedUploadSets
|
|
23
|
+
};
|
|
24
|
+
return `${JSON.stringify(manifest, null, 2)}\n`;
|
|
25
|
+
}
|
|
26
|
+
export function artifactReport(artifact) {
|
|
27
|
+
return {
|
|
28
|
+
kind: artifact.kind,
|
|
29
|
+
name: artifact.name,
|
|
30
|
+
path: artifact.relativePath,
|
|
31
|
+
sha256: sha256(artifact.content),
|
|
32
|
+
characters: artifact.content.length,
|
|
33
|
+
source_count: artifact.sourceCount,
|
|
34
|
+
source_line_count: artifact.sourceLineCount,
|
|
35
|
+
warning_count: artifact.warningCount
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export async function writeArtifactSet(projectRoot, timestampDir, artifacts) {
|
|
39
|
+
const latestDir = path.join(projectRoot, "tmp", "ty-context", "context-exports", "latest");
|
|
40
|
+
await writeArtifacts(projectRoot, timestampDir, artifacts);
|
|
41
|
+
await fs.rm(latestDir, { recursive: true, force: true });
|
|
42
|
+
await writeArtifacts(projectRoot, latestDir, artifacts);
|
|
43
|
+
}
|
|
44
|
+
export async function pruneTimestampedExports(projectRoot, keepCount) {
|
|
45
|
+
if (!Number.isInteger(keepCount) || keepCount < 0) {
|
|
46
|
+
throw new Error("export-context --prune requires a non-negative integer");
|
|
47
|
+
}
|
|
48
|
+
const exportsRoot = path.join(projectRoot, "tmp", "ty-context", "context-exports");
|
|
49
|
+
let entries;
|
|
50
|
+
try {
|
|
51
|
+
entries = await fs.readdir(exportsRoot);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const timestampDirs = entries.filter((entry) => /^\d{8}T\d{6}Z$/.test(entry)).sort().reverse();
|
|
57
|
+
for (const entry of timestampDirs.slice(keepCount)) {
|
|
58
|
+
await fs.rm(path.join(exportsRoot, entry), { recursive: true, force: true });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export function timestampForFile(now) {
|
|
62
|
+
return now.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
|
|
63
|
+
}
|
|
64
|
+
async function writeArtifacts(projectRoot, outputDir, artifacts) {
|
|
65
|
+
for (const artifact of artifacts) {
|
|
66
|
+
const target = path.join(outputDir, ...artifact.name.split("/"));
|
|
67
|
+
const relative = repoRelative(projectRoot, target);
|
|
68
|
+
if (!relative.startsWith("tmp/ty-context/context-exports/")) {
|
|
69
|
+
throw new Error("Source Pack artifacts must stay under tmp/ty-context/context-exports/**");
|
|
70
|
+
}
|
|
71
|
+
await ensureDir(path.dirname(target));
|
|
72
|
+
await writeTextIfChanged(target, artifact.content);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async function gitInfo(projectRoot) {
|
|
76
|
+
try {
|
|
77
|
+
const shaResult = await execFileAsync("git", ["-C", projectRoot, "rev-parse", "HEAD"], { encoding: "utf8" });
|
|
78
|
+
const dirtyResult = await execFileAsync("git", ["-C", projectRoot, "status", "--porcelain"], { encoding: "utf8" });
|
|
79
|
+
return { sha: shaResult.stdout.trim() || null, dirty: dirtyResult.stdout.trim().length > 0 };
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return { sha: null, dirty: false };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export async function readPackageVersion() {
|
|
86
|
+
try {
|
|
87
|
+
const packageJson = JSON.parse(await fs.readFile(new URL("../../package.json", import.meta.url), "utf8"));
|
|
88
|
+
return packageJson.version ?? "unknown";
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return "unknown";
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ContextAreaMapping, ContextArtifact, SourcePackRecord } from "./source-pack-types.js";
|
|
2
|
+
export declare function collectCodeRecords(projectRoot: string, warnings: string[], options?: {
|
|
3
|
+
include?: string[];
|
|
4
|
+
exclude?: string[];
|
|
5
|
+
areas?: ContextAreaMapping[];
|
|
6
|
+
}): Promise<SourcePackRecord[]>;
|
|
7
|
+
export declare function collectContextArtifacts(projectRoot: string, warnings: string[], includePatterns?: string[]): Promise<ContextArtifact[]>;
|
|
8
|
+
export declare function countRedactionWarnings(warnings: string[]): number;
|
|
9
|
+
export declare function countLines(content: string): number;
|
|
10
|
+
export declare function sha256(content: string): string;
|
|
11
|
+
export declare function repoRelative(root: string, file: string): string;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { promises as fs } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { listFiles, pathExists, readText } from "./fs.js";
|
|
7
|
+
import { shouldExcludeRelativePath, shouldIncludeCodeFile, toPosix } from "./source-files.js";
|
|
8
|
+
import { classifyCodeFile } from "./source-pack-classify.js";
|
|
9
|
+
import { matchesAny } from "./source-pack-config.js";
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
11
|
+
const GIT_LS_MAX_BUFFER = 64 * 1024 * 1024;
|
|
12
|
+
const SENSITIVE_ASSIGNMENT_PATTERN = /^(\s*(?:[-*]\s*)?(?:[`"']?[\w.-]*(?:secret|token|cookie|password|api[_-]?key|credential|bearer|authorization)[\w.-]*[`"']?\s*[:=]\s*))(.+?)\s*$/i;
|
|
13
|
+
export async function collectCodeRecords(projectRoot, warnings, options = {}) {
|
|
14
|
+
const gitCandidates = await listGitCandidateFiles(projectRoot);
|
|
15
|
+
const candidates = gitCandidates ?? (await listCandidateFiles(projectRoot));
|
|
16
|
+
const records = [];
|
|
17
|
+
for (const file of candidates) {
|
|
18
|
+
const relative = repoRelative(projectRoot, file);
|
|
19
|
+
if (!shouldIncludeCodeFile(relative) || matchesAny(relative, options.exclude ?? [])) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (options.include && options.include.length > 0 && !matchesAny(relative, options.include)) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (!(await isRegularFile(file))) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const rawContent = await readText(file);
|
|
29
|
+
const redacted = redactSensitiveAssignments(rawContent);
|
|
30
|
+
if (redacted.count > 0) {
|
|
31
|
+
warnings.push(`${relative}: redacted ${redacted.count} sensitive assignment line(s)`);
|
|
32
|
+
}
|
|
33
|
+
const content = redacted.content;
|
|
34
|
+
const classification = classifyCodeFile(relative, content, options.areas ?? []);
|
|
35
|
+
records.push({
|
|
36
|
+
relative,
|
|
37
|
+
language: classification.language,
|
|
38
|
+
lines: countLines(content),
|
|
39
|
+
characters: content.length,
|
|
40
|
+
sha256: sha256(content),
|
|
41
|
+
summary: classification.summary,
|
|
42
|
+
content,
|
|
43
|
+
tags: classification.tags,
|
|
44
|
+
routes: classification.routes,
|
|
45
|
+
score: classification.score,
|
|
46
|
+
bucket: classification.bucket,
|
|
47
|
+
bundle: "omitted"
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return records.sort((left, right) => left.relative.localeCompare(right.relative));
|
|
51
|
+
}
|
|
52
|
+
export async function collectContextArtifacts(projectRoot, warnings, includePatterns) {
|
|
53
|
+
const files = new Set();
|
|
54
|
+
for (const relative of ["AGENTS.md", "README.md", "DESIGN.md", "project_context/global.md", "project_context/architecture.md", "project_context/context.toml"]) {
|
|
55
|
+
await addIfExists(projectRoot, files, warnings, relative, relative.startsWith("project_context/"));
|
|
56
|
+
}
|
|
57
|
+
for (const file of await listFiles(path.join(projectRoot, "project_context"))) {
|
|
58
|
+
const relative = repoRelative(projectRoot, file);
|
|
59
|
+
if ((relative.endsWith(".md") || relative.endsWith(".toml")) && !shouldExcludeRelativePath(relative)) {
|
|
60
|
+
files.add(file);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const artifacts = [];
|
|
64
|
+
for (const file of [...files].sort((a, b) => repoRelative(projectRoot, a).localeCompare(repoRelative(projectRoot, b)))) {
|
|
65
|
+
const relative = repoRelative(projectRoot, file);
|
|
66
|
+
if (includePatterns && includePatterns.length > 0 && !matchesAny(relative, includePatterns)) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const rawContent = await readText(file);
|
|
70
|
+
const redacted = redactSensitiveAssignments(rawContent);
|
|
71
|
+
if (redacted.count > 0) {
|
|
72
|
+
warnings.push(`${relative}: redacted ${redacted.count} sensitive assignment line(s)`);
|
|
73
|
+
}
|
|
74
|
+
artifacts.push({
|
|
75
|
+
relative,
|
|
76
|
+
content: redacted.content,
|
|
77
|
+
lines: countLines(redacted.content),
|
|
78
|
+
characters: redacted.content.length
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return artifacts;
|
|
82
|
+
}
|
|
83
|
+
export function countRedactionWarnings(warnings) {
|
|
84
|
+
return warnings.filter((warning) => /redacted \d+ sensitive assignment line/i.test(warning)).length;
|
|
85
|
+
}
|
|
86
|
+
export function countLines(content) {
|
|
87
|
+
return content.length === 0 ? 0 : content.split(/\r\n|\r|\n/).length;
|
|
88
|
+
}
|
|
89
|
+
export function sha256(content) {
|
|
90
|
+
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
91
|
+
}
|
|
92
|
+
export function repoRelative(root, file) {
|
|
93
|
+
return toPosix(path.relative(root, file));
|
|
94
|
+
}
|
|
95
|
+
async function addIfExists(projectRoot, files, warnings, relative, required) {
|
|
96
|
+
const target = path.join(projectRoot, ...relative.split("/"));
|
|
97
|
+
if (await pathExists(target)) {
|
|
98
|
+
files.add(target);
|
|
99
|
+
}
|
|
100
|
+
else if (required) {
|
|
101
|
+
warnings.push(`${relative}: missing expected source file`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async function listGitCandidateFiles(projectRoot) {
|
|
105
|
+
try {
|
|
106
|
+
const result = await execFileAsync("git", ["-C", projectRoot, "ls-files", "--cached", "--others", "--exclude-standard", "-z"], {
|
|
107
|
+
encoding: "utf8",
|
|
108
|
+
maxBuffer: GIT_LS_MAX_BUFFER
|
|
109
|
+
});
|
|
110
|
+
return result.stdout.split("\0").filter(Boolean).map((relative) => path.join(projectRoot, ...toPosix(relative).split("/")));
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function listCandidateFiles(projectRoot) {
|
|
117
|
+
const files = [];
|
|
118
|
+
await walkCandidates(projectRoot, projectRoot, files);
|
|
119
|
+
return files;
|
|
120
|
+
}
|
|
121
|
+
async function walkCandidates(projectRoot, current, files) {
|
|
122
|
+
let entries;
|
|
123
|
+
try {
|
|
124
|
+
entries = await fs.readdir(current, { withFileTypes: true });
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
130
|
+
const fullPath = path.join(current, entry.name);
|
|
131
|
+
const relative = repoRelative(projectRoot, fullPath);
|
|
132
|
+
if (entry.isDirectory()) {
|
|
133
|
+
if (!shouldExcludeRelativePath(relative)) {
|
|
134
|
+
await walkCandidates(projectRoot, fullPath, files);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else if (entry.isFile() && !shouldExcludeRelativePath(relative)) {
|
|
138
|
+
files.push(fullPath);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async function isRegularFile(target) {
|
|
143
|
+
try {
|
|
144
|
+
return (await fs.stat(target)).isFile();
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function redactSensitiveAssignments(content) {
|
|
151
|
+
let count = 0;
|
|
152
|
+
const lines = content.split(/\r?\n/).map((line) => {
|
|
153
|
+
const match = SENSITIVE_ASSIGNMENT_PATTERN.exec(line);
|
|
154
|
+
if (!match) {
|
|
155
|
+
return line;
|
|
156
|
+
}
|
|
157
|
+
count += 1;
|
|
158
|
+
return `${match[1]}[REDACTED]`;
|
|
159
|
+
});
|
|
160
|
+
return { content: lines.join("\n"), count };
|
|
161
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ContextAreaMapping, ContextArtifact, SourcePackOmitted, SourcePackRecord } from "./source-pack-types.js";
|
|
2
|
+
export declare const SOURCE_PACK_EXPORT_HEADER = "Export artifact. Do not reference from project_context/context.toml.";
|
|
3
|
+
export interface RenderMeta {
|
|
4
|
+
generatedAt: string;
|
|
5
|
+
outputPath: string;
|
|
6
|
+
command: string;
|
|
7
|
+
toolVersion: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function renderFullProjectContextArtifact(contexts: ContextArtifact[], warnings: string[], meta: RenderMeta): string;
|
|
10
|
+
export declare function renderCodeIndexArtifact(records: SourcePackRecord[], areas: ContextAreaMapping[], warnings: string[], meta: RenderMeta): string;
|
|
11
|
+
export declare function renderBundleArtifact(title: string, records: SourcePackRecord[], contexts: ContextArtifact[], omitted: SourcePackOmitted, warnings: string[], meta: RenderMeta, policy: string): string;
|
|
12
|
+
export declare function renderTaskContextArtifact(taskName: string, contexts: ContextArtifact[], records: SourcePackRecord[], verification: string[], omitted: SourcePackOmitted, warnings: string[], meta: RenderMeta): string;
|