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,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;
@@ -0,0 +1,235 @@
1
+ export const SOURCE_PACK_EXPORT_HEADER = "Export artifact. Do not reference from project_context/context.toml.";
2
+ export function renderFullProjectContextArtifact(contexts, warnings, meta) {
3
+ return [
4
+ "# Full Project Context Export",
5
+ "",
6
+ `> ${SOURCE_PACK_EXPORT_HEADER}`,
7
+ "",
8
+ "## Export Metadata",
9
+ "",
10
+ `- generated_at: ${meta.generatedAt}`,
11
+ `- output_path: ${meta.outputPath}`,
12
+ `- command: ${meta.command}`,
13
+ `- source_context_count: ${contexts.filter((context) => context.relative.startsWith("project_context/")).length}`,
14
+ "- warnings:",
15
+ ...renderWarningList(warnings),
16
+ "",
17
+ "## Export Boundary",
18
+ "",
19
+ "- This temporary artifact is for external planning and handoff only.",
20
+ "- Do not register it in project_context/context.toml.",
21
+ "",
22
+ "## Context Sources",
23
+ "",
24
+ ...contexts.map(renderContextArtifact),
25
+ ""
26
+ ].join("\n");
27
+ }
28
+ export function renderCodeIndexArtifact(records, areas, warnings, meta) {
29
+ const totalLines = records.reduce((sum, record) => sum + record.lines, 0);
30
+ const totalCharacters = records.reduce((sum, record) => sum + record.characters, 0);
31
+ return [
32
+ "# Code Index Export",
33
+ "",
34
+ `> ${SOURCE_PACK_EXPORT_HEADER}`,
35
+ "",
36
+ "## Export Metadata",
37
+ "",
38
+ `- generated_at: ${meta.generatedAt}`,
39
+ `- output_path: ${meta.outputPath}`,
40
+ `- command: ${meta.command}`,
41
+ `- tool_version: ${meta.toolVersion}`,
42
+ `- source_file_count: ${records.length}`,
43
+ `- total_lines: ${totalLines}`,
44
+ `- total_characters: ${totalCharacters}`,
45
+ "- warnings:",
46
+ ...renderWarningList(warnings),
47
+ "",
48
+ "## Repository Shape",
49
+ "",
50
+ renderRepositoryShape(records),
51
+ "",
52
+ "## Context Area Mapping",
53
+ "",
54
+ "Inferred buckets below are export routing only; they are not durable architecture or product ownership facts.",
55
+ areas.length > 0 ? areas.map((area) => `- ${area.id}: root=${area.root}; context=${area.context}`).join("\n") : "- No Context area mappings found.",
56
+ "",
57
+ "## Key Entry Points",
58
+ "",
59
+ renderRecordList(records.filter((record) => record.tags.includes("entry")).slice(0, 30)),
60
+ "",
61
+ "## API / Route Index",
62
+ "",
63
+ renderRecordList(records.filter((record) => record.tags.includes("api")).slice(0, 30), true),
64
+ "",
65
+ "## UI Surface Index",
66
+ "",
67
+ renderRecordList(records.filter((record) => record.tags.includes("ui")).slice(0, 30)),
68
+ "",
69
+ "## CLI / Worker / Script Index",
70
+ "",
71
+ renderRecordList(records.filter((record) => record.tags.includes("cli") || record.tags.includes("worker")).slice(0, 30)),
72
+ "",
73
+ "## Test / Verification Index",
74
+ "",
75
+ renderRecordList(records.filter((record) => record.tags.includes("test")).slice(0, 30)),
76
+ "",
77
+ "## Oversized Files",
78
+ "",
79
+ renderRecordList(records.filter((record) => record.tags.includes("oversized")).slice(0, 30)),
80
+ "",
81
+ "## Source File Index",
82
+ "",
83
+ renderSourceFileIndex(records),
84
+ ""
85
+ ].join("\n");
86
+ }
87
+ export function renderBundleArtifact(title, records, contexts, omitted, warnings, meta, policy) {
88
+ return [
89
+ `# ${title}`,
90
+ "",
91
+ `> ${SOURCE_PACK_EXPORT_HEADER}`,
92
+ "",
93
+ "## Export Metadata",
94
+ "",
95
+ `- generated_at: ${meta.generatedAt}`,
96
+ `- output_path: ${meta.outputPath}`,
97
+ `- command: ${meta.command}`,
98
+ `- source_file_count: ${records.length}`,
99
+ "- warnings:",
100
+ ...renderWarningList(warnings),
101
+ "",
102
+ "## Bundle Selection Policy",
103
+ "",
104
+ policy,
105
+ "",
106
+ "## Bundle Index",
107
+ "",
108
+ renderSourceFileIndex(records),
109
+ "",
110
+ "## Related Context Paths",
111
+ "",
112
+ contexts.length > 0 ? contexts.map((context) => `- ${context.relative} (${context.lines} lines)`).join("\n") : "- No related Context files selected.",
113
+ "",
114
+ "## Source Files",
115
+ "",
116
+ records.map(renderSourceRecord).join("\n\n") || "- No source files selected for this bundle.",
117
+ "",
118
+ "## Omitted Files Summary",
119
+ "",
120
+ renderOmittedSummary(omitted),
121
+ ""
122
+ ].join("\n");
123
+ }
124
+ export function renderTaskContextArtifact(taskName, contexts, records, verification, omitted, warnings, meta) {
125
+ return [
126
+ `# Task Context: ${taskName}`,
127
+ "",
128
+ `> ${SOURCE_PACK_EXPORT_HEADER}`,
129
+ "",
130
+ "## Export Metadata",
131
+ "",
132
+ `- generated_at: ${meta.generatedAt}`,
133
+ `- output_path: ${meta.outputPath}`,
134
+ `- command: ${meta.command}`,
135
+ "- warnings:",
136
+ ...renderWarningList(warnings),
137
+ "",
138
+ "## Boundary",
139
+ "",
140
+ "- This task pack is a temporary export selector output, not a durable fact source.",
141
+ "- Profile verification commands are listed only; export does not execute them.",
142
+ "",
143
+ "## Selected Context",
144
+ "",
145
+ ...contexts.map(renderContextArtifact),
146
+ "",
147
+ "## Selected Source Index",
148
+ "",
149
+ renderSourceFileIndex(records),
150
+ "",
151
+ "## Selected Source Files",
152
+ "",
153
+ records.map(renderSourceRecord).join("\n\n") || "- No selected source files matched the task filters.",
154
+ "",
155
+ "## Verification Entry Points",
156
+ "",
157
+ verification.length > 0 ? verification.map((entry) => `- ${entry}`).join("\n") : "- No profile verification entries selected.",
158
+ "",
159
+ "## Omitted / Support Notes",
160
+ "",
161
+ renderOmittedSummary(omitted),
162
+ ""
163
+ ].join("\n");
164
+ }
165
+ function renderContextArtifact(context) {
166
+ const fence = fenceFor(context.content);
167
+ return [`### ${context.relative}`, "", `${fence}markdown`, context.content.trimEnd(), fence, ""].join("\n");
168
+ }
169
+ function renderSourceRecord(record) {
170
+ const fence = fenceFor(record.content);
171
+ return [
172
+ `### ${record.relative}`,
173
+ "",
174
+ `Summary: ${record.summary}`,
175
+ "",
176
+ "Metadata:",
177
+ `- type: ${record.language}`,
178
+ `- lines: ${record.lines}`,
179
+ `- characters: ${record.characters}`,
180
+ `- sha256: ${record.sha256}`,
181
+ `- tags: ${record.tags.join(", ") || "none"}`,
182
+ `- routing_bucket: ${record.bucket} (export routing only)`,
183
+ "",
184
+ `${fence}${record.language}`,
185
+ record.content.trimEnd(),
186
+ fence
187
+ ].join("\n");
188
+ }
189
+ function renderSourceFileIndex(records) {
190
+ if (records.length === 0) {
191
+ return "- No source files matched.";
192
+ }
193
+ return [
194
+ "| Path | Type | Lines | Characters | SHA256 | Summary | Bundle | Tags |",
195
+ "|---|---:|---:|---:|---|---|---|---|",
196
+ ...records.map((record) => `| ${escapeTableCell(record.relative)} | ${escapeTableCell(record.language)} | ${record.lines} | ${record.characters} | ${record.sha256.slice(0, 12)} | ${escapeTableCell(record.summary)} | ${record.bundle} | ${escapeTableCell(record.tags.join(", "))} |`)
197
+ ].join("\n");
198
+ }
199
+ function renderRepositoryShape(records) {
200
+ const counts = new Map();
201
+ for (const record of records) {
202
+ const top = record.relative.split("/")[0] || ".";
203
+ counts.set(top, (counts.get(top) ?? 0) + 1);
204
+ }
205
+ return [...counts.entries()]
206
+ .sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]))
207
+ .map(([name, count]) => `- ${name}: ${count} source file(s)`)
208
+ .join("\n") || "- No source files matched.";
209
+ }
210
+ function renderRecordList(records, includeRoutes = false) {
211
+ if (records.length === 0) {
212
+ return "- None detected.";
213
+ }
214
+ return records
215
+ .map((record) => `- ${record.relative}: ${record.summary}${includeRoutes && record.routes.length > 0 ? ` Routes: ${record.routes.join(", ")}` : ""}`)
216
+ .join("\n");
217
+ }
218
+ function renderWarningList(warnings) {
219
+ return warnings.length > 0 ? warnings.map((warning) => ` - ${warning}`) : [" - none"];
220
+ }
221
+ function renderOmittedSummary(omitted) {
222
+ const reasons = Object.entries(omitted.reason_counts)
223
+ .sort((left, right) => left[0].localeCompare(right[0]))
224
+ .map(([reason, count]) => ` - ${reason}: ${count}`);
225
+ return [`- omitted_source_file_count: ${omitted.source_file_count}`, "- reason_counts:", ...(reasons.length > 0 ? reasons : [" - none: 0"])].join("\n");
226
+ }
227
+ function fenceFor(content) {
228
+ let fence = "```";
229
+ while (content.includes(fence))
230
+ fence += "`";
231
+ return fence;
232
+ }
233
+ function escapeTableCell(value) {
234
+ return value.replace(/\r?\n/g, " ").replace(/\|/g, "\\|");
235
+ }
@@ -0,0 +1,77 @@
1
+ export type SourcePackMode = "code-index" | "source-pack" | "code-bundles" | "task-context";
2
+ export interface SourcePackOptions {
3
+ mode: SourcePackMode;
4
+ check?: boolean;
5
+ now?: Date;
6
+ command?: string;
7
+ profile?: string;
8
+ includeContext?: string[];
9
+ includeCode?: string[];
10
+ bundleStrategy?: "auto" | "area" | "topdir" | "config";
11
+ maxPackFiles?: number;
12
+ maxBundleCharacters?: number;
13
+ redactionStrict?: boolean;
14
+ prune?: number;
15
+ taskName?: string;
16
+ }
17
+ export interface SourcePackArtifactReport {
18
+ kind: string;
19
+ name: string;
20
+ path: string;
21
+ sha256: string;
22
+ characters: number;
23
+ source_count: number;
24
+ source_line_count: number;
25
+ warning_count: number;
26
+ }
27
+ export interface SourcePackReport {
28
+ mode: SourcePackMode;
29
+ outputDirectory: string;
30
+ outputRelativePath: string;
31
+ artifacts: SourcePackArtifactReport[];
32
+ sourceFiles: string[];
33
+ sourceCodeCount: number;
34
+ totalLines: number;
35
+ totalCharacters: number;
36
+ redactionCount: number;
37
+ warnings: string[];
38
+ omitted: SourcePackOmitted;
39
+ recommendedUploadSets: Record<string, string[]>;
40
+ wrote: boolean;
41
+ }
42
+ export interface SourcePackOmitted {
43
+ source_file_count: number;
44
+ reason_counts: Record<string, number>;
45
+ }
46
+ export interface SourcePackRecord {
47
+ relative: string;
48
+ language: string;
49
+ lines: number;
50
+ characters: number;
51
+ sha256: string;
52
+ summary: string;
53
+ content: string;
54
+ tags: string[];
55
+ routes: string[];
56
+ score: number;
57
+ bucket: string;
58
+ bundle: "core" | "extended" | "task" | "task-support" | "omitted";
59
+ }
60
+ export interface SourcePackProfile {
61
+ context: string[];
62
+ code: string[];
63
+ exclude: string[];
64
+ verification: string[];
65
+ maxBundleCharacters?: number;
66
+ }
67
+ export interface ContextArtifact {
68
+ relative: string;
69
+ content: string;
70
+ lines: number;
71
+ characters: number;
72
+ }
73
+ export interface ContextAreaMapping {
74
+ id: string;
75
+ root: string;
76
+ context: string;
77
+ }
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-tiny-context-harness",
3
- "version": "0.2.60",
3
+ "version": "0.2.61",
4
4
  "description": "Minimal project memory and validation harness for AI coding agents.",
5
5
  "license": "MIT",
6
6
  "author": "Seven128",