project-tiny-context-harness 0.2.60 → 0.2.61

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.
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ import type { SourcePackOptions, SourcePackReport } from "./source-pack-types.js";
2
+ export declare function runSourcePackExport(projectRoot: string, options: SourcePackOptions): Promise<SourcePackReport>;
@@ -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;