@inflexa-ai/harness 0.28.0 → 0.29.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/dist/app/data-profile-orientation.d.ts +52 -27
- package/dist/app/data-profile-orientation.js +331 -92
- package/dist/app/data-profile-view.d.ts +70 -0
- package/dist/app/data-profile-view.js +123 -0
- package/dist/app/synthesize-run.js +1 -1
- package/dist/contracts/data-profile.d.ts +276 -25
- package/dist/contracts/index.d.ts +3 -1
- package/dist/contracts/index.js +1 -0
- package/dist/contracts/profile-vocabulary.d.ts +103 -0
- package/dist/contracts/profile-vocabulary.js +449 -0
- package/dist/contracts/target-dossier.d.ts +21 -21
- package/dist/execution/artifact-registration.js +9 -0
- package/dist/execution/artifact-registry.d.ts +14 -0
- package/dist/execution/input-signature.d.ts +16 -3
- package/dist/execution/input-signature.js +0 -0
- package/dist/execution/post-step-pipeline.d.ts +6 -4
- package/dist/execution/post-step-pipeline.js +18 -9
- package/dist/execution/staged-input.d.ts +4 -4
- package/dist/index.d.ts +5 -1
- package/dist/index.js +7 -0
- package/dist/input-scan/clustering.d.ts +63 -0
- package/dist/input-scan/clustering.js +208 -0
- package/dist/input-scan/companions.d.ts +30 -0
- package/dist/input-scan/companions.js +138 -0
- package/dist/input-scan/decoder/container_readout.py +171 -0
- package/dist/input-scan/detect-sets.d.ts +24 -0
- package/dist/input-scan/detect-sets.js +279 -0
- package/dist/input-scan/enrich.d.ts +62 -19
- package/dist/input-scan/enrich.js +132 -187
- package/dist/input-scan/formats.d.ts +13 -5
- package/dist/input-scan/formats.js +13 -5
- package/dist/input-scan/index.d.ts +10 -4
- package/dist/input-scan/index.js +7 -4
- package/dist/input-scan/markers.d.ts +21 -0
- package/dist/input-scan/markers.js +89 -0
- package/dist/input-scan/menu.d.ts +127 -0
- package/dist/input-scan/menu.js +0 -0
- package/dist/input-scan/quarantine.d.ts +17 -0
- package/dist/input-scan/quarantine.js +69 -0
- package/dist/input-scan/readout-budget.d.ts +32 -0
- package/dist/input-scan/readout-budget.js +61 -0
- package/dist/input-scan/readout.d.ts +33 -0
- package/dist/input-scan/readout.js +336 -0
- package/dist/input-scan/scan.d.ts +5 -4
- package/dist/input-scan/scan.js +5 -5
- package/dist/input-scan/set-types.d.ts +209 -0
- package/dist/input-scan/set-types.js +15 -0
- package/dist/input-scan/templates.d.ts +93 -0
- package/dist/input-scan/templates.js +369 -0
- package/dist/input-scan/tokens.d.ts +94 -0
- package/dist/input-scan/tokens.js +267 -0
- package/dist/input-scan/tuning.d.ts +55 -0
- package/dist/input-scan/tuning.js +55 -0
- package/dist/input-scan/types.d.ts +2 -2
- package/dist/prompts/conversation.d.ts +1 -1
- package/dist/prompts/conversation.js +8 -6
- package/dist/prompts/report-session.d.ts +1 -1
- package/dist/prompts/report-session.js +3 -2
- package/dist/prompts/sandbox/data-profiler.d.ts +10 -1
- package/dist/prompts/sandbox/data-profiler.js +197 -53
- package/dist/prompts/sandbox-standards.d.ts +1 -1
- package/dist/prompts/sandbox-standards.js +10 -7
- package/dist/schemas/data-profile-schemas.d.ts +943 -80
- package/dist/schemas/data-profile-schemas.js +300 -115
- package/dist/state/data-profile.d.ts +1 -1
- package/dist/state/index.d.ts +1 -1
- package/dist/state/vector-store.d.ts +13 -0
- package/dist/state/vector-store.js +8 -0
- package/dist/tasks/data-profile-absorb.d.ts +78 -0
- package/dist/tasks/data-profile-absorb.js +285 -0
- package/dist/tasks/data-profile-index.d.ts +34 -25
- package/dist/tasks/data-profile-index.js +160 -59
- package/dist/tasks/data-profile-resolve.d.ts +72 -0
- package/dist/tasks/data-profile-resolve.js +639 -0
- package/dist/tasks/data-profile.d.ts +16 -5
- package/dist/tasks/data-profile.js +270 -131
- package/dist/tools/research/inspect-data-profile.d.ts +48 -22
- package/dist/tools/research/inspect-data-profile.js +73 -54
- package/dist/tools/workspace/scan-inputs.d.ts +4 -4
- package/dist/tools/workspace/scan-inputs.js +13 -14
- package/dist/tools/workspace/workspace-search.d.ts +11 -2
- package/dist/tools/workspace/workspace-search.js +44 -16
- package/dist/workflows/sandbox-step.js +36 -6
- package/dist/workflows/target-assessment/phase4-assemble.d.ts +21 -21
- package/package.json +5 -3
- package/dist/input-scan/coverage.d.ts +0 -22
- package/dist/input-scan/coverage.js +0 -85
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The detected-set pipeline: quarantine → markers → sibling clustering → template
|
|
3
|
+
* mining → assembly.
|
|
4
|
+
*
|
|
5
|
+
* Pure over paths, sizes, and the format already detected — no read, no container, no
|
|
6
|
+
* model. Each stage narrows what the next one has to explain: junk leaves before
|
|
7
|
+
* structure is observed, a marker-claimed subtree never reaches inference, and a
|
|
8
|
+
* clustered directory arrives at mining as one template with its entity already a
|
|
9
|
+
* slot.
|
|
10
|
+
*
|
|
11
|
+
* Assembly carries variance rather than splitting on it. Members that differ only in
|
|
12
|
+
* compression are one set whose wrapper census says so, and a token appearing in both
|
|
13
|
+
* a directory segment and the stem is cross-checked and reported as ONE identity slot,
|
|
14
|
+
* because reporting it twice would invent a second thing that varies.
|
|
15
|
+
*/
|
|
16
|
+
import type { ContentSimilarity } from "./clustering.js";
|
|
17
|
+
import type { DetectedSets } from "./set-types.js";
|
|
18
|
+
import type { ScannedFile } from "./types.js";
|
|
19
|
+
export interface DetectSetsOptions {
|
|
20
|
+
/** Read-based agreement between candidate clusters, when the caller has header readouts. */
|
|
21
|
+
readonly contentSimilarity?: ContentSimilarity;
|
|
22
|
+
}
|
|
23
|
+
/** Observe the detected sets of a scanned tree. Pure — no I/O, no container, no model. */
|
|
24
|
+
export declare function detectSets(files: readonly ScannedFile[], options?: DetectSetsOptions): DetectedSets;
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The detected-set pipeline: quarantine → markers → sibling clustering → template
|
|
3
|
+
* mining → assembly.
|
|
4
|
+
*
|
|
5
|
+
* Pure over paths, sizes, and the format already detected — no read, no container, no
|
|
6
|
+
* model. Each stage narrows what the next one has to explain: junk leaves before
|
|
7
|
+
* structure is observed, a marker-claimed subtree never reaches inference, and a
|
|
8
|
+
* clustered directory arrives at mining as one template with its entity already a
|
|
9
|
+
* slot.
|
|
10
|
+
*
|
|
11
|
+
* Assembly carries variance rather than splitting on it. Members that differ only in
|
|
12
|
+
* compression are one set whose wrapper census says so, and a token appearing in both
|
|
13
|
+
* a directory segment and the stem is cross-checked and reported as ONE identity slot,
|
|
14
|
+
* because reporting it twice would invent a second thing that varies.
|
|
15
|
+
*/
|
|
16
|
+
import { assessCompleteness, attachCompanions } from "./companions.js";
|
|
17
|
+
import { buildTree, clusterChildren, computeSignatures } from "./clustering.js";
|
|
18
|
+
import { applyMarkers } from "./markers.js";
|
|
19
|
+
import { quarantine } from "./quarantine.js";
|
|
20
|
+
import { selectReadouts } from "./readout-budget.js";
|
|
21
|
+
import { catchAll, describeGroup, describeSlot, mineContext, slotToken } from "./templates.js";
|
|
22
|
+
import { IDENTITY_CROSS_CHECK_RATIO, MAX_EXAMPLE_PATHS, MAX_LEFTOVER_SAMPLE, MAX_SETS_PER_CONTEXT, MIN_IDENTITY_TOKEN_LENGTH } from "./tuning.js";
|
|
23
|
+
function fileCountOf(members) {
|
|
24
|
+
return members.reduce((total, member) => total + 1 + member.companions.length, 0);
|
|
25
|
+
}
|
|
26
|
+
function bytesOf(members) {
|
|
27
|
+
return members.reduce((total, member) => total + member.size + member.companions.reduce((sum, companion) => sum + companion.size, 0), 0);
|
|
28
|
+
}
|
|
29
|
+
function formatCensus(members) {
|
|
30
|
+
const counts = new Map();
|
|
31
|
+
for (const member of members)
|
|
32
|
+
counts.set(member.format, (counts.get(member.format) ?? 0) + 1);
|
|
33
|
+
return [...counts.entries()].map(([format, count]) => ({ format, count })).sort((a, b) => b.count - a.count || a.format.localeCompare(b.format, "en"));
|
|
34
|
+
}
|
|
35
|
+
function wrapperCensus(members) {
|
|
36
|
+
const counts = new Map();
|
|
37
|
+
for (const member of members) {
|
|
38
|
+
const wrapper = member.wrapper ?? "none";
|
|
39
|
+
counts.set(wrapper, (counts.get(wrapper) ?? 0) + 1);
|
|
40
|
+
}
|
|
41
|
+
return [...counts.entries()].map(([wrapper, count]) => ({ wrapper, count })).sort((a, b) => b.count - a.count || a.wrapper.localeCompare(b.wrapper, "en"));
|
|
42
|
+
}
|
|
43
|
+
function renderTemplate(segments, slots) {
|
|
44
|
+
return segments.map((segment) => (segment.kind === "literal" ? segment.text : slotToken(slots[segment.slot]))).join("");
|
|
45
|
+
}
|
|
46
|
+
/** The token value at a slot's position, with the literal affixes the template shows stripped off. */
|
|
47
|
+
function coreAt(item, slot) {
|
|
48
|
+
const raw = item.tokens[slot.index]?.value ?? "";
|
|
49
|
+
const end = slot.suffix ? raw.length - slot.suffix.length : raw.length;
|
|
50
|
+
return raw.slice(slot.prefix?.length ?? 0, end);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Link a name slot to the directory slot it repeats.
|
|
54
|
+
*
|
|
55
|
+
* Containment rather than equality: affix recovery can take a character off the token,
|
|
56
|
+
* and a directory segment may carry decoration its stem twin lacks. Containment on its
|
|
57
|
+
* own is weak evidence, so it counts only where the contained token is long enough to
|
|
58
|
+
* be an identifier, or where the two positions have the same cardinality — a token
|
|
59
|
+
* that appears inside a segment AND varies with it one-for-one is that segment's
|
|
60
|
+
* identity, while one that merely appears inside it is a coincidence of naming.
|
|
61
|
+
*/
|
|
62
|
+
function crossCheckIdentity(group, nameSlots, directorySlots, directoryValues) {
|
|
63
|
+
if (group.kind !== "tokens" && group.kind !== "family")
|
|
64
|
+
return;
|
|
65
|
+
for (const slot of nameSlots) {
|
|
66
|
+
if (slot.distinctValues < 2)
|
|
67
|
+
continue;
|
|
68
|
+
for (let k = 0; k < directoryValues.length; k++) {
|
|
69
|
+
const values = directoryValues[k];
|
|
70
|
+
const paired = directorySlots[k].distinctValues === slot.distinctValues;
|
|
71
|
+
let matches = 0;
|
|
72
|
+
for (let i = 0; i < group.items.length; i++) {
|
|
73
|
+
const core = coreAt(group.items[i], slot);
|
|
74
|
+
const segment = values[i] ?? "";
|
|
75
|
+
const alike = core === segment ||
|
|
76
|
+
(segment.includes(core) && (paired || core.length >= MIN_IDENTITY_TOKEN_LENGTH)) ||
|
|
77
|
+
(core.includes(segment) && (paired || segment.length >= MIN_IDENTITY_TOKEN_LENGTH));
|
|
78
|
+
if (alike)
|
|
79
|
+
matches++;
|
|
80
|
+
}
|
|
81
|
+
if (matches / group.items.length < IDENTITY_CROSS_CHECK_RATIO)
|
|
82
|
+
continue;
|
|
83
|
+
slot.sameAs = k;
|
|
84
|
+
slot.crossCheckMismatches = group.items.length - matches;
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function assembleMinedSet(group, path) {
|
|
90
|
+
const members = group.items.map((item) => item.file);
|
|
91
|
+
const { slots: nameSlots, segments: nameSegments } = describeGroup(group);
|
|
92
|
+
const directorySlots = [];
|
|
93
|
+
const directorySegments = [];
|
|
94
|
+
const directoryValues = [];
|
|
95
|
+
let variablePosition = 0;
|
|
96
|
+
for (const segment of path) {
|
|
97
|
+
if (segment.kind === "literal") {
|
|
98
|
+
directorySegments.push({ kind: "literal", text: `${segment.name}/` });
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const at = variablePosition++;
|
|
102
|
+
const values = members.map((member) => member.varValues[at] ?? "");
|
|
103
|
+
const distinct = new Set(values);
|
|
104
|
+
if (distinct.size === 1) {
|
|
105
|
+
directorySegments.push({ kind: "literal", text: `${[...distinct][0]}/` });
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const slot = describeSlot(values, "directory", segment.index);
|
|
109
|
+
if (slot.prefix)
|
|
110
|
+
directorySegments.push({ kind: "literal", text: slot.prefix });
|
|
111
|
+
directorySegments.push({ kind: "slot", slot: directorySlots.length });
|
|
112
|
+
if (slot.suffix)
|
|
113
|
+
directorySegments.push({ kind: "literal", text: slot.suffix });
|
|
114
|
+
directorySegments.push({ kind: "literal", text: "/" });
|
|
115
|
+
directorySlots.push(slot);
|
|
116
|
+
directoryValues.push(values);
|
|
117
|
+
}
|
|
118
|
+
crossCheckIdentity(group, nameSlots, directorySlots, directoryValues);
|
|
119
|
+
const slots = [...directorySlots, ...nameSlots];
|
|
120
|
+
const segments = [
|
|
121
|
+
...directorySegments,
|
|
122
|
+
...nameSegments.map((segment) => (segment.kind === "slot" ? { kind: "slot", slot: segment.slot + directorySlots.length } : segment)),
|
|
123
|
+
];
|
|
124
|
+
const origin = group.kind === "tokens" ? "mined" : group.kind;
|
|
125
|
+
return { origin, pathTemplate: renderTemplate(segments, slots), segments, slots, members };
|
|
126
|
+
}
|
|
127
|
+
function finalizeSet(draft, index) {
|
|
128
|
+
const id = `set-${index + 1}`;
|
|
129
|
+
const slotIds = draft.slots.map((_, position) => `${id}.slot-${position + 1}`);
|
|
130
|
+
const slots = draft.slots.map((slot, position) => ({
|
|
131
|
+
id: slotIds[position],
|
|
132
|
+
location: slot.location,
|
|
133
|
+
index: slot.index,
|
|
134
|
+
tokenClass: slot.tokenClass,
|
|
135
|
+
...(slot.width !== undefined ? { width: slot.width } : {}),
|
|
136
|
+
...(slot.minLength !== undefined ? { minLength: slot.minLength, maxLength: slot.maxLength } : {}),
|
|
137
|
+
...(slot.skeleton !== undefined ? { skeleton: slot.skeleton } : {}),
|
|
138
|
+
...(slot.prefix !== undefined ? { prefix: slot.prefix } : {}),
|
|
139
|
+
...(slot.suffix !== undefined ? { suffix: slot.suffix } : {}),
|
|
140
|
+
distinctValues: slot.distinctValues,
|
|
141
|
+
sampleValues: slot.sampleValues,
|
|
142
|
+
...(slot.sameAs !== undefined ? { sameAsSlot: slotIds[slot.sameAs], crossCheckMismatches: slot.crossCheckMismatches ?? 0 } : {}),
|
|
143
|
+
}));
|
|
144
|
+
const segments = draft.segments.map((segment) => segment.kind === "literal" ? { kind: "literal", text: segment.text } : { kind: "slot", slotId: slotIds[segment.slot] });
|
|
145
|
+
const { members, completeness } = assessCompleteness(draft.members);
|
|
146
|
+
return {
|
|
147
|
+
set: {
|
|
148
|
+
id,
|
|
149
|
+
origin: draft.origin,
|
|
150
|
+
...(draft.marker ? { marker: draft.marker } : {}),
|
|
151
|
+
pathTemplate: draft.pathTemplate,
|
|
152
|
+
segments,
|
|
153
|
+
memberCount: members.length,
|
|
154
|
+
fileCount: fileCountOf(members),
|
|
155
|
+
totalBytes: bytesOf(members),
|
|
156
|
+
formats: formatCensus(members),
|
|
157
|
+
wrappers: wrapperCensus(members),
|
|
158
|
+
slots,
|
|
159
|
+
completeness,
|
|
160
|
+
examplePaths: members.slice(0, MAX_EXAMPLE_PATHS).map((member) => member.path),
|
|
161
|
+
members,
|
|
162
|
+
},
|
|
163
|
+
values: draft.slots.map((slot, position) => [slotIds[position], slot.values]),
|
|
164
|
+
memberValues: draft.slots.map((slot, position) => [slotIds[position], slot.memberValues]),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Sample the leftovers across formats first, then by size.
|
|
169
|
+
*
|
|
170
|
+
* A metadata sheet, a README, and a paper are each one file of their own format, so a
|
|
171
|
+
* size-ordered sample would bury all three under whichever large files failed to
|
|
172
|
+
* group. Covering formats first is what makes the notable singletons visible.
|
|
173
|
+
*/
|
|
174
|
+
function sampleLeftovers(members) {
|
|
175
|
+
const bySize = [...members].sort((a, b) => b.size - a.size || a.path.localeCompare(b.path, "en"));
|
|
176
|
+
const picked = [];
|
|
177
|
+
const seen = new Set();
|
|
178
|
+
for (const member of bySize) {
|
|
179
|
+
if (picked.length >= MAX_LEFTOVER_SAMPLE)
|
|
180
|
+
break;
|
|
181
|
+
if (seen.has(member.format))
|
|
182
|
+
continue;
|
|
183
|
+
seen.add(member.format);
|
|
184
|
+
picked.push(member);
|
|
185
|
+
}
|
|
186
|
+
for (const member of bySize) {
|
|
187
|
+
if (picked.length >= MAX_LEFTOVER_SAMPLE)
|
|
188
|
+
break;
|
|
189
|
+
if (!picked.includes(member))
|
|
190
|
+
picked.push(member);
|
|
191
|
+
}
|
|
192
|
+
return picked.map((member) => ({ path: member.path, size: member.size, format: member.format }));
|
|
193
|
+
}
|
|
194
|
+
/** Observe the detected sets of a scanned tree. Pure — no I/O, no container, no model. */
|
|
195
|
+
export function detectSets(files, options = {}) {
|
|
196
|
+
const { kept, summary } = quarantine(files);
|
|
197
|
+
const { units, unclaimed } = applyMarkers(attachCompanions(kept));
|
|
198
|
+
const drafts = [];
|
|
199
|
+
for (const unit of units) {
|
|
200
|
+
const template = `${unit.root ? `${unit.root}/` : ""}**`;
|
|
201
|
+
drafts.push({
|
|
202
|
+
origin: "marker",
|
|
203
|
+
marker: unit.label,
|
|
204
|
+
pathTemplate: template,
|
|
205
|
+
segments: [{ kind: "literal", text: template }],
|
|
206
|
+
slots: [],
|
|
207
|
+
members: unit.members,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
const leftovers = [];
|
|
211
|
+
const emit = (context, path) => {
|
|
212
|
+
const hasDirectorySlot = path.some((segment) => segment.kind === "variable");
|
|
213
|
+
const mined = mineContext(context);
|
|
214
|
+
let sets = mined.sets;
|
|
215
|
+
let residue = mined.residue;
|
|
216
|
+
// The long tail of tiny sets inside a repeated directory shape is noise: the
|
|
217
|
+
// repetition is the structure, and the catch-all is where it belongs.
|
|
218
|
+
if (hasDirectorySlot && sets.length > MAX_SETS_PER_CONTEXT) {
|
|
219
|
+
const ordered = [...sets].sort((a, b) => b.items.length - a.items.length);
|
|
220
|
+
sets = ordered.slice(0, MAX_SETS_PER_CONTEXT);
|
|
221
|
+
residue = [...residue, ...ordered.slice(MAX_SETS_PER_CONTEXT).flatMap((group) => group.items)];
|
|
222
|
+
}
|
|
223
|
+
const caught = catchAll(residue, hasDirectorySlot);
|
|
224
|
+
for (const group of [...sets, ...caught.sets])
|
|
225
|
+
drafts.push(assembleMinedSet(group, path));
|
|
226
|
+
leftovers.push(...caught.rest.map((item) => item.file));
|
|
227
|
+
};
|
|
228
|
+
const descend = (entries, path) => {
|
|
229
|
+
const context = [];
|
|
230
|
+
for (const entry of entries)
|
|
231
|
+
for (const file of entry.node.files)
|
|
232
|
+
context.push({ ...file, varValues: entry.varValues });
|
|
233
|
+
if (context.length > 0)
|
|
234
|
+
emit(context, path);
|
|
235
|
+
const children = entries.flatMap((entry) => [...entry.node.children.values()].map((node) => ({ node, varValues: entry.varValues })));
|
|
236
|
+
if (children.length === 0)
|
|
237
|
+
return;
|
|
238
|
+
for (const cluster of clusterChildren(children, options.contentSimilarity)) {
|
|
239
|
+
if (cluster.names.length === 1) {
|
|
240
|
+
descend(cluster.entries, [...path, { kind: "literal", name: cluster.names[0] }]);
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const next = cluster.entries.map((entry) => ({ node: entry.node, varValues: [...entry.varValues, entry.node.name] }));
|
|
244
|
+
descend(next, [...path, { kind: "variable", index: path.length }]);
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
descend([{ node: computeSignatures(buildTree(unclaimed)), varValues: [] }], []);
|
|
248
|
+
const ordered = drafts.sort((a, b) => b.members.length - a.members.length || bytesOf(b.members) - bytesOf(a.members) || a.pathTemplate.localeCompare(b.pathTemplate, "en"));
|
|
249
|
+
const slotValues = new Map();
|
|
250
|
+
const memberSlotValues = new Map();
|
|
251
|
+
const sets = [];
|
|
252
|
+
for (const [index, draft] of ordered.entries()) {
|
|
253
|
+
const finalized = finalizeSet(draft, index);
|
|
254
|
+
sets.push(finalized.set);
|
|
255
|
+
for (const [slotId, values] of finalized.values)
|
|
256
|
+
slotValues.set(slotId, values);
|
|
257
|
+
for (const [slotId, values] of finalized.memberValues)
|
|
258
|
+
memberSlotValues.set(slotId, values);
|
|
259
|
+
}
|
|
260
|
+
const coveredFiles = sets.reduce((total, set) => total + set.fileCount, 0);
|
|
261
|
+
const leftoverFiles = {
|
|
262
|
+
memberCount: leftovers.length,
|
|
263
|
+
fileCount: fileCountOf(leftovers),
|
|
264
|
+
totalBytes: bytesOf(leftovers),
|
|
265
|
+
sample: sampleLeftovers(leftovers),
|
|
266
|
+
};
|
|
267
|
+
return {
|
|
268
|
+
fileCount: files.length,
|
|
269
|
+
keptFileCount: kept.length,
|
|
270
|
+
coverage: kept.length === 0 ? 1 : coveredFiles / kept.length,
|
|
271
|
+
quarantine: summary,
|
|
272
|
+
sets,
|
|
273
|
+
leftovers: leftoverFiles,
|
|
274
|
+
leftoverMembers: leftovers,
|
|
275
|
+
readout: selectReadouts(sets, leftovers),
|
|
276
|
+
slotValues,
|
|
277
|
+
memberSlotValues,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
@@ -1,30 +1,66 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Per-shape header readout
|
|
2
|
+
* Per-shape header readout.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Split by what the readout needs. A header that sits at the front of the file —
|
|
5
|
+
* magic bytes, a `##` preamble, a delimited header row, a compressed prefix — is
|
|
6
|
+
* reachable from a bounded read, so it runs in this process over the workspace read
|
|
7
|
+
* seam (see `readout.ts`) and costs no sandbox round trip.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* A container that indexes its contents from a footer cannot be read that way, and
|
|
10
|
+
* running a container parser over user-supplied bytes in a long-lived multi-tenant
|
|
11
|
+
* process is the exposure the ephemeral sandbox exists to contain. Those readouts run
|
|
12
|
+
* from `decoder/container_readout.py`, a package asset delivered on the command line
|
|
13
|
+
* through the existing `runSandboxExec` path.
|
|
13
14
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* releases on the harness's own path rather than on the image's manual one.
|
|
15
|
+
* Cost is O(shapes), not O(files): shapes are observed from names and sizes alone, so
|
|
16
|
+
* enrichment reads a bounded number of members per shape whatever the tree's size.
|
|
17
17
|
*/
|
|
18
|
+
import type { AgentSession } from "../auth/types.js";
|
|
18
19
|
import type { EmitFn } from "../tools/define-tool.js";
|
|
19
20
|
import type { SandboxClient } from "../sandbox/client.js";
|
|
20
21
|
import type { SandboxRef } from "../sandbox/types.js";
|
|
21
|
-
import type {
|
|
22
|
+
import type { WorkspaceFilesystem } from "../workspace/filesystem.js";
|
|
23
|
+
import type { FileShape, HeaderReadout } from "./types.js";
|
|
22
24
|
/** Members decoded per shape. One is enough to characterise a set whose names already match. */
|
|
23
25
|
export declare const MEMBERS_DECODED_PER_SHAPE = 1;
|
|
26
|
+
/**
|
|
27
|
+
* Prefix reads in flight at once. Each is a bounded read over the workspace seam; running
|
|
28
|
+
* them one at a time makes a menu of sixty leftovers sixty serial round trips, and running
|
|
29
|
+
* them all at once hands the seam an unbounded burst.
|
|
30
|
+
*/
|
|
31
|
+
export declare const READOUT_CONCURRENCY = 8;
|
|
32
|
+
/**
|
|
33
|
+
* The formats whose header lives behind a whole-container parse: Parquet's schema is
|
|
34
|
+
* in its footer, the HDF5 families index objects across the file, and PDF and DOCX
|
|
35
|
+
* both resolve their contents through a trailer. Everything else reads from a prefix.
|
|
36
|
+
*/
|
|
37
|
+
export declare const SANDBOX_CONTAINER_FORMATS: ReadonlySet<string>;
|
|
38
|
+
/** One file to read a header from, and what the scan already knows about its bytes. */
|
|
39
|
+
export interface ReadoutTargetSpec {
|
|
40
|
+
readonly path: string;
|
|
41
|
+
readonly format: string;
|
|
42
|
+
readonly wrapper?: string;
|
|
43
|
+
}
|
|
44
|
+
export interface ReadHeadersArgs {
|
|
45
|
+
readonly targets: readonly ReadoutTargetSpec[];
|
|
46
|
+
readonly session: AgentSession;
|
|
47
|
+
readonly fs: WorkspaceFilesystem;
|
|
48
|
+
/** Omit the sandbox to skip container readouts; every other format still reads. */
|
|
49
|
+
readonly sandboxClient?: SandboxClient;
|
|
50
|
+
readonly sandbox?: SandboxRef;
|
|
51
|
+
/** Absolute in-sandbox path of the analysis root (`/{analysisId}`). */
|
|
52
|
+
readonly mountRoot: string;
|
|
53
|
+
readonly execId: string;
|
|
54
|
+
readonly deadlineMs: number;
|
|
55
|
+
readonly emit: EmitFn;
|
|
56
|
+
}
|
|
24
57
|
export interface EnrichShapesArgs {
|
|
25
58
|
readonly shapes: readonly FileShape[];
|
|
26
|
-
readonly
|
|
27
|
-
readonly
|
|
59
|
+
readonly session: AgentSession;
|
|
60
|
+
readonly fs: WorkspaceFilesystem;
|
|
61
|
+
/** Omit the sandbox to skip container readouts; every other format still reads. */
|
|
62
|
+
readonly sandboxClient?: SandboxClient;
|
|
63
|
+
readonly sandbox?: SandboxRef;
|
|
28
64
|
/** Absolute in-sandbox path of the analysis root (`/{analysisId}`). */
|
|
29
65
|
readonly mountRoot: string;
|
|
30
66
|
readonly execId: string;
|
|
@@ -32,10 +68,17 @@ export interface EnrichShapesArgs {
|
|
|
32
68
|
readonly emit: EmitFn;
|
|
33
69
|
}
|
|
34
70
|
/**
|
|
35
|
-
*
|
|
71
|
+
* Read a header from each named file, keyed by path.
|
|
72
|
+
*
|
|
73
|
+
* A target whose readout failed carries an `unavailable` note rather than vanishing: the
|
|
74
|
+
* readout is enrichment, and a menu without it still carries every structural observation
|
|
75
|
+
* the agent's grouping rests on.
|
|
76
|
+
*/
|
|
77
|
+
export declare function readHeaders(args: ReadHeadersArgs): Promise<Map<string, HeaderReadout>>;
|
|
78
|
+
/**
|
|
79
|
+
* Read one member per shape and attach the readout.
|
|
36
80
|
*
|
|
37
|
-
* A shape whose
|
|
38
|
-
* shape
|
|
39
|
-
* structural observation the agent's grouping rests on.
|
|
81
|
+
* A shape whose readout failed keeps its `unavailable` note rather than losing the
|
|
82
|
+
* shape, for the same reason.
|
|
40
83
|
*/
|
|
41
84
|
export declare function enrichShapes(args: EnrichShapesArgs): Promise<FileShape[]>;
|