@xamukavila/pxpipe 0.8.0
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/LICENSE +21 -0
- package/README.md +312 -0
- package/bin/cli.js +7 -0
- package/dist/core/applicability.d.ts +31 -0
- package/dist/core/applicability.js +96 -0
- package/dist/core/atlas-gray.d.ts +26 -0
- package/dist/core/atlas-gray.js +64 -0
- package/dist/core/atlas.d.ts +34 -0
- package/dist/core/atlas.js +71 -0
- package/dist/core/baseline.d.ts +80 -0
- package/dist/core/baseline.js +101 -0
- package/dist/core/caveman.d.ts +38 -0
- package/dist/core/caveman.js +183 -0
- package/dist/core/export.d.ts +128 -0
- package/dist/core/export.js +390 -0
- package/dist/core/factsheet.d.ts +78 -0
- package/dist/core/factsheet.js +216 -0
- package/dist/core/gpt-model-profiles.d.ts +60 -0
- package/dist/core/gpt-model-profiles.js +161 -0
- package/dist/core/history.d.ts +141 -0
- package/dist/core/history.js +553 -0
- package/dist/core/index.d.ts +8 -0
- package/dist/core/index.js +8 -0
- package/dist/core/library.d.ts +74 -0
- package/dist/core/library.js +133 -0
- package/dist/core/measurement.d.ts +22 -0
- package/dist/core/measurement.js +213 -0
- package/dist/core/openai-history.d.ts +124 -0
- package/dist/core/openai-history.js +494 -0
- package/dist/core/openai-savings.d.ts +44 -0
- package/dist/core/openai-savings.js +75 -0
- package/dist/core/openai.d.ts +24 -0
- package/dist/core/openai.js +839 -0
- package/dist/core/png.d.ts +11 -0
- package/dist/core/png.js +132 -0
- package/dist/core/proxy.d.ts +81 -0
- package/dist/core/proxy.js +730 -0
- package/dist/core/render.d.ts +188 -0
- package/dist/core/render.js +785 -0
- package/dist/core/schema-strip.d.ts +29 -0
- package/dist/core/schema-strip.js +160 -0
- package/dist/core/tracker.d.ts +154 -0
- package/dist/core/tracker.js +216 -0
- package/dist/core/transform.d.ts +362 -0
- package/dist/core/transform.js +1828 -0
- package/dist/core/types.d.ts +77 -0
- package/dist/core/types.js +8 -0
- package/dist/dashboard/fragments.d.ts +36 -0
- package/dist/dashboard/fragments.js +938 -0
- package/dist/dashboard/types.d.ts +154 -0
- package/dist/dashboard/types.js +3 -0
- package/dist/dashboard/vendor.d.ts +3 -0
- package/dist/dashboard/vendor.js +6 -0
- package/dist/dashboard.d.ts +245 -0
- package/dist/dashboard.js +1140 -0
- package/dist/export-collect.d.ts +36 -0
- package/dist/export-collect.js +59 -0
- package/dist/node.d.ts +9 -0
- package/dist/node.js +9038 -0
- package/dist/sessions.d.ts +172 -0
- package/dist/sessions.js +510 -0
- package/dist/stats.d.ts +74 -0
- package/dist/stats.js +248 -0
- package/dist/worker.d.ts +53 -0
- package/dist/worker.js +102 -0
- package/package.json +96 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core logic for `pxpipe export` — renders a source text to PNG pages, extracts
|
|
3
|
+
* a verbatim factsheet, builds a manifest and paste-ready prompt, and returns a
|
|
4
|
+
* token-cost report + list of artifacts to write.
|
|
5
|
+
*
|
|
6
|
+
* Pure-ish: no argv, no stdout, no process.exit, no fs calls — all I/O is
|
|
7
|
+
* delegated to the thin CLI runner in src/node.ts.
|
|
8
|
+
*/
|
|
9
|
+
import { DENSE_CONTENT_CHARS_PER_IMAGE, DENSE_CONTENT_COLS, MAX_HEIGHT_PX, PAD_X, CELL_W, } from './render.js';
|
|
10
|
+
// Dogfood the public SDK: render via the same `./transform` entry external
|
|
11
|
+
// consumers import (pxpipe-proxy/transform → renderTextToImages), not the
|
|
12
|
+
// internal leaf renderer.
|
|
13
|
+
import { renderTextToImages } from './library.js';
|
|
14
|
+
import { estimateImageCount, ANTHROPIC_PIXELS_PER_TOKEN, IMAGE_COST_SAFETY_MARGIN, REPORT_CHARS_PER_TOKEN } from './transform.js';
|
|
15
|
+
import { openAIVisionTokens } from './openai.js';
|
|
16
|
+
import { factSheetTextFromEntries, extractFactSheetTokensAllPages, extractFactSheetEntriesAllPages, } from './factsheet.js';
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Public constants
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
export const DEFAULT_EXPORT_MODEL = 'claude-sonnet-4-5';
|
|
21
|
+
// Chars-per-token for the reporting estimate now lives in transform.ts as
|
|
22
|
+
// REPORT_CHARS_PER_TOKEN (single source of truth for all token-estimate constants).
|
|
23
|
+
/** Default column width — dense content mode (312 cols = 1568 px). */
|
|
24
|
+
export const DEFAULT_EXPORT_COLS = DENSE_CONTENT_COLS;
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Glob matching (no external glob library — node:fs only per convention)
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
/**
|
|
29
|
+
* Match a relative file path against a glob pattern.
|
|
30
|
+
* Supported wildcards: `*` (non-separator), `**` (any including `/`), `?` (single non-sep).
|
|
31
|
+
* When the pattern contains no `/`, matching is against the basename only
|
|
32
|
+
* (e.g. `*.ts` matches `src/foo.ts`).
|
|
33
|
+
*/
|
|
34
|
+
export function matchGlob(pattern, filePath) {
|
|
35
|
+
const pat = pattern.replace(/\\/g, '/');
|
|
36
|
+
const fp = filePath.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
37
|
+
if (!pat.includes('/')) {
|
|
38
|
+
// No separator → match basename only
|
|
39
|
+
const sep = fp.lastIndexOf('/');
|
|
40
|
+
const basename = sep >= 0 ? fp.slice(sep + 1) : fp;
|
|
41
|
+
return _globTest(pat, basename);
|
|
42
|
+
}
|
|
43
|
+
return _globTest(pat, fp);
|
|
44
|
+
}
|
|
45
|
+
function _globTest(pattern, str) {
|
|
46
|
+
const SPECIAL = new Set([
|
|
47
|
+
'.', '+', '^', '$', '{', '}', '(', ')', '|', '[', ']', '\\',
|
|
48
|
+
]);
|
|
49
|
+
let re = '';
|
|
50
|
+
let i = 0;
|
|
51
|
+
while (i < pattern.length) {
|
|
52
|
+
const ch = pattern[i];
|
|
53
|
+
if (ch === undefined)
|
|
54
|
+
break;
|
|
55
|
+
const next = pattern[i + 1];
|
|
56
|
+
const nextNext = pattern[i + 2];
|
|
57
|
+
if (ch === '*' && next === '*') {
|
|
58
|
+
if (nextNext === '/') {
|
|
59
|
+
re += '(?:.*/)?';
|
|
60
|
+
i += 3;
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
re += '.*';
|
|
64
|
+
i += 2;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else if (ch === '*') {
|
|
68
|
+
re += '[^/]*';
|
|
69
|
+
i++;
|
|
70
|
+
}
|
|
71
|
+
else if (ch === '?') {
|
|
72
|
+
re += '[^/]';
|
|
73
|
+
i++;
|
|
74
|
+
}
|
|
75
|
+
else if (SPECIAL.has(ch)) {
|
|
76
|
+
re += '\\' + ch;
|
|
77
|
+
i++;
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
re += ch;
|
|
81
|
+
i++;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
return new RegExp(`^${re}$`).test(str);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Decide whether a relative file path should be included given include/exclude glob lists.
|
|
93
|
+
* - Exclude patterns are checked first; any match → exclude.
|
|
94
|
+
* - If include patterns are given, at least one must match.
|
|
95
|
+
* - If no include patterns are given, everything passes (unless excluded).
|
|
96
|
+
*/
|
|
97
|
+
export function shouldIncludeFile(filePath, include, exclude) {
|
|
98
|
+
if (exclude.some((pat) => matchGlob(pat, filePath)))
|
|
99
|
+
return false;
|
|
100
|
+
if (include.length > 0)
|
|
101
|
+
return include.some((pat) => matchGlob(pat, filePath));
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Parse the argv array for the `export` subcommand.
|
|
106
|
+
* Returns a discriminated union so the caller (node.ts) decides
|
|
107
|
+
* whether to exit, print help, or proceed.
|
|
108
|
+
*
|
|
109
|
+
* @param defaultOut Base output directory (default: $TMPDIR or /tmp).
|
|
110
|
+
*/
|
|
111
|
+
export function parseExportArgv(argv, defaultOut) {
|
|
112
|
+
const targets = [];
|
|
113
|
+
const include = [];
|
|
114
|
+
const exclude = [];
|
|
115
|
+
let git = false;
|
|
116
|
+
let diff;
|
|
117
|
+
let stdin = false;
|
|
118
|
+
// Locked to the proxy's density — NO CLI knob. Export must render exactly what the
|
|
119
|
+
// proxy ships to the model; the proxy has no width flag, so neither does export.
|
|
120
|
+
const cols = DENSE_CONTENT_COLS;
|
|
121
|
+
let out = defaultOut ??
|
|
122
|
+
(typeof process !== 'undefined'
|
|
123
|
+
? (process.env['TMPDIR'] ?? process.env['TEMP'] ?? '/tmp')
|
|
124
|
+
: '/tmp');
|
|
125
|
+
let model = DEFAULT_EXPORT_MODEL;
|
|
126
|
+
let json = false;
|
|
127
|
+
let open = false;
|
|
128
|
+
for (let i = 0; i < argv.length; i++) {
|
|
129
|
+
const a = argv[i];
|
|
130
|
+
if (a === undefined)
|
|
131
|
+
break;
|
|
132
|
+
if (a === '-h' || a === '--help') {
|
|
133
|
+
return { kind: 'help' };
|
|
134
|
+
}
|
|
135
|
+
else if (a === '--include' || a === '--exclude') {
|
|
136
|
+
i++;
|
|
137
|
+
const val = argv[i];
|
|
138
|
+
if (val === undefined || val.startsWith('-')) {
|
|
139
|
+
return { kind: 'error', message: `${a} requires a value` };
|
|
140
|
+
}
|
|
141
|
+
if (a === '--include')
|
|
142
|
+
include.push(val);
|
|
143
|
+
else
|
|
144
|
+
exclude.push(val);
|
|
145
|
+
}
|
|
146
|
+
else if (a.startsWith('--include=')) {
|
|
147
|
+
const v = a.slice('--include='.length);
|
|
148
|
+
if (!v)
|
|
149
|
+
return { kind: 'error', message: '--include= requires a non-empty value' };
|
|
150
|
+
include.push(v);
|
|
151
|
+
}
|
|
152
|
+
else if (a.startsWith('--exclude=')) {
|
|
153
|
+
const v = a.slice('--exclude='.length);
|
|
154
|
+
if (!v)
|
|
155
|
+
return { kind: 'error', message: '--exclude= requires a non-empty value' };
|
|
156
|
+
exclude.push(v);
|
|
157
|
+
}
|
|
158
|
+
else if (a === '--git') {
|
|
159
|
+
git = true;
|
|
160
|
+
}
|
|
161
|
+
else if (a === '--diff') {
|
|
162
|
+
i++;
|
|
163
|
+
const val = argv[i];
|
|
164
|
+
if (val === undefined || val.startsWith('-')) {
|
|
165
|
+
return { kind: 'error', message: '--diff requires a value' };
|
|
166
|
+
}
|
|
167
|
+
diff = val;
|
|
168
|
+
}
|
|
169
|
+
else if (a.startsWith('--diff=')) {
|
|
170
|
+
const v = a.slice('--diff='.length);
|
|
171
|
+
if (!v)
|
|
172
|
+
return { kind: 'error', message: '--diff= requires a non-empty value' };
|
|
173
|
+
diff = v;
|
|
174
|
+
}
|
|
175
|
+
else if (a === '--stdin') {
|
|
176
|
+
stdin = true;
|
|
177
|
+
}
|
|
178
|
+
else if (a === '--out') {
|
|
179
|
+
i++;
|
|
180
|
+
const val = argv[i];
|
|
181
|
+
if (val === undefined)
|
|
182
|
+
return { kind: 'error', message: '--out requires a value' };
|
|
183
|
+
out = val;
|
|
184
|
+
}
|
|
185
|
+
else if (a.startsWith('--out=')) {
|
|
186
|
+
const v = a.slice('--out='.length);
|
|
187
|
+
if (!v)
|
|
188
|
+
return { kind: 'error', message: '--out= requires a non-empty value' };
|
|
189
|
+
out = v;
|
|
190
|
+
}
|
|
191
|
+
else if (a === '--model') {
|
|
192
|
+
i++;
|
|
193
|
+
const val = argv[i];
|
|
194
|
+
if (val === undefined)
|
|
195
|
+
return { kind: 'error', message: '--model requires a value' };
|
|
196
|
+
model = val;
|
|
197
|
+
}
|
|
198
|
+
else if (a.startsWith('--model=')) {
|
|
199
|
+
const v = a.slice('--model='.length);
|
|
200
|
+
if (!v)
|
|
201
|
+
return { kind: 'error', message: '--model= requires a non-empty value' };
|
|
202
|
+
model = v;
|
|
203
|
+
}
|
|
204
|
+
else if (a === '--json') {
|
|
205
|
+
json = true;
|
|
206
|
+
}
|
|
207
|
+
else if (a === '--open') {
|
|
208
|
+
open = true;
|
|
209
|
+
}
|
|
210
|
+
else if (a.startsWith('-')) {
|
|
211
|
+
return { kind: 'error', message: `unknown option: ${a}` };
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
targets.push(a);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
kind: 'opts',
|
|
219
|
+
parsed: { targets, include, exclude, git, diff, stdin, cols, out, model, json, open },
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
// Model-routing image-token helper
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
/**
|
|
226
|
+
* Per-image vision-token cost for a rendered PNG at the given pixel dimensions.
|
|
227
|
+
*
|
|
228
|
+
* - **Claude / Anthropic models** (`model.startsWith('claude')` or
|
|
229
|
+
* `model.includes('anthropic')`): uses Anthropic's billing formula
|
|
230
|
+
* `ceil(width × height / 750 × 1.10)` (the same formula and constants as
|
|
231
|
+
* `imageTokensForRows` in transform.ts, reusing the exported
|
|
232
|
+
* `ANTHROPIC_PIXELS_PER_TOKEN` / `IMAGE_COST_SAFETY_MARGIN` consts).
|
|
233
|
+
* - **GPT / o-series models**: delegates to `openAIVisionTokens` which uses the
|
|
234
|
+
* GPT-4o tile-pricing formula (85 + 170 × tiles after scaling).
|
|
235
|
+
*/
|
|
236
|
+
export function exportImageTokens(model, width, height) {
|
|
237
|
+
if (model.startsWith('claude') || model.includes('anthropic')) {
|
|
238
|
+
return Math.ceil((width * height / ANTHROPIC_PIXELS_PER_TOKEN) * IMAGE_COST_SAFETY_MARGIN);
|
|
239
|
+
}
|
|
240
|
+
return openAIVisionTokens(model, width, height);
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Estimate text vs image token cost for `sourceText` without rendering.
|
|
244
|
+
* Uses the same formula as the internal gate:
|
|
245
|
+
* stripW = 2·PAD_X + cols·CELL_W
|
|
246
|
+
* imageTokens = estimateImageCount(text, cols) × exportImageTokens(model, stripW, MAX_HEIGHT_PX)
|
|
247
|
+
* textTokens = sourceText.length / REPORT_CHARS_PER_TOKEN
|
|
248
|
+
*
|
|
249
|
+
* `exportImageTokens` routes to the Anthropic billing formula (width×height/750×1.10)
|
|
250
|
+
* for Claude models, and to the GPT tile-pricing formula for GPT/o-series models.
|
|
251
|
+
*
|
|
252
|
+
* `factsheetItemCount` is the number of unique precision-critical identifier strings
|
|
253
|
+
* extracted across all pages (paths, SHAs, ids, …); it is NOT an LLM token count.
|
|
254
|
+
* `factsheetDropped` is the count of extracted identifiers that did not fit within
|
|
255
|
+
* the 64-item per-export budget.
|
|
256
|
+
*/
|
|
257
|
+
export function computeTokenReport(sourceText, cols, model) {
|
|
258
|
+
const stripW = 2 * PAD_X + cols * CELL_W;
|
|
259
|
+
const estImages = estimateImageCount(sourceText, cols, 1, DENSE_CONTENT_CHARS_PER_IMAGE);
|
|
260
|
+
const perStrip = exportImageTokens(model, stripW, MAX_HEIGHT_PX);
|
|
261
|
+
const imageTokens = Math.round(estImages * perStrip);
|
|
262
|
+
const textTokens = Math.round(sourceText.length / REPORT_CHARS_PER_TOKEN);
|
|
263
|
+
const percentSaved = textTokens > 0
|
|
264
|
+
? Math.round(((textTokens - imageTokens) / textTokens) * 1000) / 10
|
|
265
|
+
: 0;
|
|
266
|
+
const { kept, dropped } = extractFactSheetTokensAllPages(sourceText, DENSE_CONTENT_CHARS_PER_IMAGE);
|
|
267
|
+
return {
|
|
268
|
+
textTokens,
|
|
269
|
+
imageTokens,
|
|
270
|
+
percentSaved,
|
|
271
|
+
factsheetItemCount: kept.length,
|
|
272
|
+
factsheetDropped: dropped,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
// Prompt template
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
export function buildPromptText(pageCount, factsheet, files, droppedItems = 0) {
|
|
279
|
+
const lastPageStr = String(pageCount).padStart(3, '0');
|
|
280
|
+
const fileSection = files.length > 0
|
|
281
|
+
? `Source files (${files.length}):\n${files.map((f) => ` ${f}`).join('\n')}\n\n`
|
|
282
|
+
: '';
|
|
283
|
+
const factsheetNote = droppedItems > 0
|
|
284
|
+
? `2. For any path, file name, identifier, hash, version number, or other precision-critical\n` +
|
|
285
|
+
` value: check factsheet.txt first — identifiers listed there are verbatim exact.\n` +
|
|
286
|
+
` Note: ${droppedItems} identifier(s) were extracted but not captured due to the 64-item\n` +
|
|
287
|
+
` budget — OCR carefully for any exact values not listed in factsheet.txt.\n`
|
|
288
|
+
: `2. For any path, file name, identifier, hash, version number, or other precision-critical\n` +
|
|
289
|
+
` value: use the verbatim text from factsheet.txt, NOT what you read off the image.\n` +
|
|
290
|
+
` factsheet.txt is the authoritative source of truth for all exact strings.\n`;
|
|
291
|
+
return (`These ${pageCount} image${pageCount !== 1 ? 's' : ''} contain source code/text rendered as PNG pages by pxpipe.\n\n` +
|
|
292
|
+
fileSection +
|
|
293
|
+
`Instructions for the reading agent:\n` +
|
|
294
|
+
`1. Read the images in order: page-001.png through page-${lastPageStr}.png.\n` +
|
|
295
|
+
factsheetNote +
|
|
296
|
+
`3. Treat the content as if you had read the source files directly.\n\n` +
|
|
297
|
+
`Factsheet (verbatim identifiers — quote from here, not from image pixels):\n` +
|
|
298
|
+
(factsheet || '(none)') +
|
|
299
|
+
'\n');
|
|
300
|
+
}
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
302
|
+
// Simple hash for output directory naming
|
|
303
|
+
// ---------------------------------------------------------------------------
|
|
304
|
+
/** FNV-1a 32-bit hash — deterministic, no crypto dep. */
|
|
305
|
+
function fnv32a(str) {
|
|
306
|
+
let h = 0x811c9dc5;
|
|
307
|
+
for (let i = 0; i < str.length; i++) {
|
|
308
|
+
h ^= str.charCodeAt(i) & 0xff;
|
|
309
|
+
h = (h * 0x01000193) >>> 0;
|
|
310
|
+
}
|
|
311
|
+
return h;
|
|
312
|
+
}
|
|
313
|
+
/** 8-char hex hash of the first 512 chars + length of `text`. */
|
|
314
|
+
export function sourceShortHash(text) {
|
|
315
|
+
const sample = text.slice(0, 512) + '\x00' + String(text.length);
|
|
316
|
+
return fnv32a(sample).toString(16).padStart(8, '0');
|
|
317
|
+
}
|
|
318
|
+
// ---------------------------------------------------------------------------
|
|
319
|
+
// Core export runner (pure-ish: no fs, no argv, no stdout)
|
|
320
|
+
// ---------------------------------------------------------------------------
|
|
321
|
+
export async function runExportCore(sourceText, opts) {
|
|
322
|
+
const generatedAt = new Date().toISOString();
|
|
323
|
+
const enc = new TextEncoder();
|
|
324
|
+
// Render to PNG pages via the public SDK primitive. shrink=true sizes the canvas
|
|
325
|
+
// to the widest line so short-line code isn't padded to full width; multiCol='auto'
|
|
326
|
+
// packs as many columns side-by-side as fit. Same render surface shipped to external
|
|
327
|
+
// SDK consumers (pxpipe-proxy/transform).
|
|
328
|
+
const { pages: images } = await renderTextToImages(sourceText, {
|
|
329
|
+
cols: opts.cols,
|
|
330
|
+
shrink: true,
|
|
331
|
+
multiCol: 'auto',
|
|
332
|
+
reflow: true,
|
|
333
|
+
});
|
|
334
|
+
// Compute token costs using actual rendered image dimensions (more accurate than estimate).
|
|
335
|
+
// exportImageTokens routes to the Anthropic billing formula for claude-* models and to
|
|
336
|
+
// the GPT tile-pricing formula for GPT/o-series models.
|
|
337
|
+
const textTokens = Math.round(sourceText.length / REPORT_CHARS_PER_TOKEN);
|
|
338
|
+
let imageTokens = 0;
|
|
339
|
+
for (const img of images) {
|
|
340
|
+
imageTokens += exportImageTokens(opts.model, img.width, img.height);
|
|
341
|
+
}
|
|
342
|
+
imageTokens = Math.round(imageTokens);
|
|
343
|
+
const percentSaved = textTokens > 0
|
|
344
|
+
? Math.round(((textTokens - imageTokens) / textTokens) * 1000) / 10
|
|
345
|
+
: 0;
|
|
346
|
+
// Extract factsheet tokens across ALL pages so identifiers from page 3+ are covered.
|
|
347
|
+
const { kept: fsKept, dropped: fsDropped } = extractFactSheetEntriesAllPages(sourceText, DENSE_CONTENT_CHARS_PER_IMAGE);
|
|
348
|
+
const fsText = factSheetTextFromEntries(fsKept);
|
|
349
|
+
const tokenReport = {
|
|
350
|
+
textTokens,
|
|
351
|
+
imageTokens,
|
|
352
|
+
percentSaved,
|
|
353
|
+
factsheetItemCount: fsKept.length,
|
|
354
|
+
factsheetDropped: fsDropped,
|
|
355
|
+
};
|
|
356
|
+
// Build artifacts
|
|
357
|
+
const artifacts = [];
|
|
358
|
+
const pages = [];
|
|
359
|
+
for (const [i, img] of images.entries()) {
|
|
360
|
+
const filename = `page-${String(i + 1).padStart(3, '0')}.png`;
|
|
361
|
+
artifacts.push({ filename, data: img.png });
|
|
362
|
+
pages.push({
|
|
363
|
+
filename,
|
|
364
|
+
bytes: img.png.byteLength,
|
|
365
|
+
width: img.width,
|
|
366
|
+
height: img.height,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
// factsheet.txt
|
|
370
|
+
artifacts.push({ filename: 'factsheet.txt', data: enc.encode(fsText) });
|
|
371
|
+
// manifest.json
|
|
372
|
+
const manifest = {
|
|
373
|
+
sourceChars: sourceText.length,
|
|
374
|
+
files: opts.sourceFiles,
|
|
375
|
+
pages,
|
|
376
|
+
cols: opts.cols,
|
|
377
|
+
model: opts.model,
|
|
378
|
+
generatedAt,
|
|
379
|
+
tokenReport,
|
|
380
|
+
};
|
|
381
|
+
artifacts.push({
|
|
382
|
+
filename: 'manifest.json',
|
|
383
|
+
data: enc.encode(JSON.stringify(manifest, null, 2)),
|
|
384
|
+
});
|
|
385
|
+
// prompt.txt
|
|
386
|
+
const promptText = buildPromptText(images.length, fsText, opts.sourceFiles, fsDropped);
|
|
387
|
+
artifacts.push({ filename: 'prompt.txt', data: enc.encode(promptText) });
|
|
388
|
+
return { manifest, artifacts };
|
|
389
|
+
}
|
|
390
|
+
//# sourceMappingURL=export.js.map
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verbatim fact-sheet for imaged content.
|
|
3
|
+
*
|
|
4
|
+
* When pxpipe renders a block (system slab, history, tool_result, reminder) to a PNG,
|
|
5
|
+
* the precision-critical, hard-to-OCR strings inside it — file paths, URLs, SHAs/UUIDs,
|
|
6
|
+
* version numbers, CLI flags, large numbers, CONST_IDS — are exactly what a model is
|
|
7
|
+
* most likely to misread off the image yet most likely to need quoted verbatim. This
|
|
8
|
+
* module extracts those tokens so they ride next to the image as plain text: the model
|
|
9
|
+
* quotes them without re-reading the PNG, and they stay in the cached prefix.
|
|
10
|
+
*
|
|
11
|
+
* Deterministic by construction (fixed pattern order, length-desc/lexical sort, no
|
|
12
|
+
* Date/random) → the emitted text is byte-stable across turns and never busts the
|
|
13
|
+
* Anthropic prompt cache. Empirically ~5% of source chars on production history
|
|
14
|
+
* (median 4.9%, max 12.1%, N=10), which preserves the imaging token win.
|
|
15
|
+
*/
|
|
16
|
+
/** Budget cap: highest-priority tokens kept first. Exported so consumers can report drops. */
|
|
17
|
+
export declare const MAX_TOKENS = 64;
|
|
18
|
+
/**
|
|
19
|
+
* Extract deduped, precision-critical tokens from `text`. Substrings of a longer kept
|
|
20
|
+
* token are dropped (so `/github.com` inside the full URL, `lib/x.ts` inside
|
|
21
|
+
* `src/lib/x.ts`, etc. collapse to the most specific form); the 64-token budget is then
|
|
22
|
+
* filled by priority tier (see `priorityTier`) so short, high-consequence tokens are never
|
|
23
|
+
* evicted by long low-risk URLs.
|
|
24
|
+
*
|
|
25
|
+
* Every token class is whitespace-free, so we split on whitespace first and skip
|
|
26
|
+
* blob-length chunks. That bounds each regex to a short chunk and keeps extraction
|
|
27
|
+
* strictly O(n) — no quadratic backtracking on delimiter-heavy input like base64 or
|
|
28
|
+
* minified bundles (which embed `/` and would otherwise make the path patterns blow up).
|
|
29
|
+
*/
|
|
30
|
+
export declare function extractFactSheetTokens(text: string): string[];
|
|
31
|
+
/** A kept fact-sheet token plus how many times it occurs in the scanned text.
|
|
32
|
+
* Counts are advisory (occurrences, not lines) but deterministic → cache-stable. */
|
|
33
|
+
export interface FactSheetEntry {
|
|
34
|
+
readonly token: string;
|
|
35
|
+
readonly count: number;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Like `extractFactSheetTokens`, but each kept token carries its occurrence count.
|
|
39
|
+
* Counts make the fact sheet a *quantitative* index: tally questions over imaged
|
|
40
|
+
* content ("how many lines mention CODE-X?") become answerable from text instead
|
|
41
|
+
* of from counting rows of 5×8 px glyphs — the one operation page images are worst
|
|
42
|
+
* at. The kept-token SET and its order are byte-identical to the pre-count
|
|
43
|
+
* behaviour; only counts are new. Same-token spans matched by two patterns are
|
|
44
|
+
* deduped by offset so a token is never double-counted.
|
|
45
|
+
*/
|
|
46
|
+
export declare function extractFactSheetEntries(text: string): FactSheetEntry[];
|
|
47
|
+
/**
|
|
48
|
+
* Page-aware variant of `extractFactSheetTokens` for large source texts.
|
|
49
|
+
*
|
|
50
|
+
* Splits `text` into chunks of `charsPerPage` (use `DENSE_CONTENT_CHARS_PER_IMAGE`
|
|
51
|
+
* from render.ts for the export pipeline), calls `extractFactSheetTokens` on each
|
|
52
|
+
* chunk (each chunk is smaller than MAX_SCAN so no truncation occurs), merges the
|
|
53
|
+
* results across all chunks with first-seen deduplication, then applies a single
|
|
54
|
+
* global priority-budget pass to select the best MAX_TOKENS identifiers.
|
|
55
|
+
*
|
|
56
|
+
* Returns `{ kept, dropped }` where `dropped` is the count of identifiers that
|
|
57
|
+
* survived extraction across all pages but did not fit in the MAX_TOKENS budget.
|
|
58
|
+
*
|
|
59
|
+
* Does NOT mutate the behaviour of `extractFactSheetTokens` or `factSheetText`.
|
|
60
|
+
*/
|
|
61
|
+
export declare function extractFactSheetTokensAllPages(text: string, charsPerPage: number): {
|
|
62
|
+
kept: string[];
|
|
63
|
+
dropped: number;
|
|
64
|
+
};
|
|
65
|
+
/** Entry-carrying variant of `extractFactSheetTokensAllPages`: same kept set and
|
|
66
|
+
* order, with per-token occurrence counts summed across all pages. */
|
|
67
|
+
export declare function extractFactSheetEntriesAllPages(text: string, charsPerPage: number): {
|
|
68
|
+
kept: FactSheetEntry[];
|
|
69
|
+
dropped: number;
|
|
70
|
+
};
|
|
71
|
+
/** Build the one-line fact-sheet string from a pre-extracted token list. */
|
|
72
|
+
export declare function factSheetTextFromTokens(tokens: string[]): string;
|
|
73
|
+
/** Build the one-line fact-sheet string from token+count entries. Byte-identical to
|
|
74
|
+
* `factSheetTextFromTokens` when no token repeats, so existing sheets stay cache-stable. */
|
|
75
|
+
export declare function factSheetTextFromEntries(entries: readonly FactSheetEntry[]): string;
|
|
76
|
+
/** One-line fact-sheet string for `text`, or `''` when nothing notable was found. */
|
|
77
|
+
export declare function factSheetText(text: string): string;
|
|
78
|
+
//# sourceMappingURL=factsheet.d.ts.map
|