@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
|
@@ -1,205 +1,133 @@
|
|
|
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 { readFile } from "node:fs/promises";
|
|
18
19
|
import { runSandboxExec } from "../tools/workspace/run-exec.js";
|
|
20
|
+
import { READOUT_PREFIX_BYTES, readPrefix } from "./readout.js";
|
|
19
21
|
/** Members decoded per shape. One is enough to characterise a set whose names already match. */
|
|
20
22
|
export const MEMBERS_DECODED_PER_SHAPE = 1;
|
|
21
|
-
/** Wall-clock bound on the
|
|
23
|
+
/** Wall-clock bound on the container-decode exec. It opens footers, nothing more. */
|
|
22
24
|
const ENRICH_TIMEOUT_SECONDS = 120;
|
|
23
25
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
|
|
26
|
+
* Prefix reads in flight at once. Each is a bounded read over the workspace seam; running
|
|
27
|
+
* them one at a time makes a menu of sixty leftovers sixty serial round trips, and running
|
|
28
|
+
* them all at once hands the seam an unbounded burst.
|
|
29
|
+
*/
|
|
30
|
+
export const READOUT_CONCURRENCY = 8;
|
|
31
|
+
/** Characters of decoder stderr carried into a per-file reason. */
|
|
32
|
+
const MAX_STDERR_CHARS = 160;
|
|
33
|
+
/**
|
|
34
|
+
* The formats whose header lives behind a whole-container parse: Parquet's schema is
|
|
35
|
+
* in its footer, the HDF5 families index objects across the file, and PDF and DOCX
|
|
36
|
+
* both resolve their contents through a trailer. Everything else reads from a prefix.
|
|
37
|
+
*/
|
|
38
|
+
export const SANDBOX_CONTAINER_FORMATS = new Set(["parquet", "hdf5", "h5ad", "loom", "pdf", "docx"]);
|
|
39
|
+
const CONTAINER_DECODER = new URL("./decoder/container_readout.py", import.meta.url);
|
|
40
|
+
/**
|
|
41
|
+
* Read a header from each named file, keyed by path.
|
|
42
|
+
*
|
|
43
|
+
* A target whose readout failed carries an `unavailable` note rather than vanishing: the
|
|
44
|
+
* readout is enrichment, and a menu without it still carries every structural observation
|
|
45
|
+
* the agent's grouping rests on.
|
|
28
46
|
*/
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
except zlib.error:
|
|
45
|
-
return b""
|
|
46
|
-
|
|
47
|
-
def sniff_delimiter(line):
|
|
48
|
-
counts = {d: line.count(d) for d in [",", "\t", ";", "|"]}
|
|
49
|
-
best = max(counts, key=lambda d: counts[d])
|
|
50
|
-
return best if counts[best] > 0 else None
|
|
51
|
-
|
|
52
|
-
def text_fields(text):
|
|
53
|
-
lines = [l for l in text.splitlines() if l.strip() != ""]
|
|
54
|
-
if not lines:
|
|
55
|
-
return {}
|
|
56
|
-
head = lines[0]
|
|
57
|
-
if head.startswith("##fileformat=VCF") or head.startswith("##"):
|
|
58
|
-
fields = {}
|
|
59
|
-
for line in lines[:200]:
|
|
60
|
-
if line.startswith("##fileformat="):
|
|
61
|
-
fields["fileformat"] = line.split("=", 1)[1][:40]
|
|
62
|
-
elif line.startswith("##reference="):
|
|
63
|
-
fields["reference"] = line.split("=", 1)[1][:80]
|
|
64
|
-
elif line.startswith("#CHROM"):
|
|
65
|
-
cols = line.split("\t")
|
|
66
|
-
samples = cols[9:]
|
|
67
|
-
fields["sampleCount"] = len(samples)
|
|
68
|
-
fields["samples"] = ", ".join(samples[:5])[:120]
|
|
69
|
-
break
|
|
70
|
-
fields["metaLines"] = sum(1 for l in lines if l.startswith("##"))
|
|
71
|
-
return fields
|
|
72
|
-
if head.startswith(">"):
|
|
73
|
-
return {"firstRecord": head[1:80], "recordsInPrefix": sum(1 for l in lines if l.startswith(">"))}
|
|
74
|
-
if head.startswith("@") and len(lines) > 1 and not head.startswith("@HD"):
|
|
75
|
-
return {"firstRecord": head[1:80], "readLength": len(lines[1])}
|
|
76
|
-
if head.startswith("@HD") or head.startswith("@SQ"):
|
|
77
|
-
return {"referenceSequences": sum(1 for l in lines if l.startswith("@SQ")), "readGroups": sum(1 for l in lines if l.startswith("@RG"))}
|
|
78
|
-
delim = sniff_delimiter(head)
|
|
79
|
-
if delim:
|
|
80
|
-
columns = next(csv.reader([head], delimiter=delim))
|
|
81
|
-
return {
|
|
82
|
-
"delimiter": {",": "comma", "\t": "tab", ";": "semicolon", "|": "pipe"}[delim],
|
|
83
|
-
"columnCount": len(columns),
|
|
84
|
-
"columns": ", ".join(c.strip()[:24] for c in columns[:12])[:200],
|
|
85
|
-
"linesInPrefix": len(lines),
|
|
47
|
+
export async function readHeaders(args) {
|
|
48
|
+
const readouts = new Map();
|
|
49
|
+
const containers = [];
|
|
50
|
+
const prefixed = [];
|
|
51
|
+
for (const target of args.targets) {
|
|
52
|
+
if (SANDBOX_CONTAINER_FORMATS.has(target.format))
|
|
53
|
+
containers.push(target);
|
|
54
|
+
else
|
|
55
|
+
prefixed.push(target);
|
|
56
|
+
}
|
|
57
|
+
let next = 0;
|
|
58
|
+
await Promise.all(Array.from({ length: Math.min(READOUT_CONCURRENCY, prefixed.length) }, async () => {
|
|
59
|
+
for (let i = next++; i < prefixed.length; i = next++) {
|
|
60
|
+
const target = prefixed[i];
|
|
61
|
+
readouts.set(target.path, await readFromPrefix(args, target));
|
|
86
62
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
with h5py.File(path, "r") as f:
|
|
96
|
-
keys = list(f.keys())[:20]
|
|
97
|
-
out = {"rootKeys": ", ".join(keys)[:200], "rootKeyCount": len(f.keys())}
|
|
98
|
-
for probe in ("X", "obs", "var", "matrix"):
|
|
99
|
-
if probe in f:
|
|
100
|
-
node = f[probe]
|
|
101
|
-
shape = getattr(node, "shape", None)
|
|
102
|
-
if shape:
|
|
103
|
-
out[probe + "Shape"] = "x".join(str(d) for d in shape)
|
|
104
|
-
return out
|
|
105
|
-
except Exception as exc:
|
|
106
|
-
return {"unavailable": "hdf5 open failed: " + str(exc)[:80]}
|
|
107
|
-
|
|
108
|
-
def pdf_fields(path):
|
|
109
|
-
if os.path.getsize(path) > DOC_MAX:
|
|
110
|
-
return {"unavailable": "document too large to page-count within the scan's bound"}
|
|
111
|
-
try:
|
|
112
|
-
from pypdf import PdfReader
|
|
113
|
-
reader = PdfReader(path)
|
|
114
|
-
text = (reader.pages[0].extract_text() or "")[:200] if reader.pages else ""
|
|
115
|
-
return {"pageCount": len(reader.pages), "firstPageText": " ".join(text.split())[:200]}
|
|
116
|
-
except Exception as exc:
|
|
117
|
-
return {"unavailable": "pdf read failed: " + str(exc)[:80]}
|
|
118
|
-
|
|
119
|
-
def docx_fields(path):
|
|
120
|
-
if os.path.getsize(path) > DOC_MAX:
|
|
121
|
-
return {"unavailable": "document too large to inspect within the scan's bound"}
|
|
122
|
-
try:
|
|
123
|
-
import zipfile, re
|
|
124
|
-
with zipfile.ZipFile(path) as z:
|
|
125
|
-
names = z.namelist()
|
|
126
|
-
out = {"parts": len(names)}
|
|
127
|
-
if "docProps/app.xml" in names:
|
|
128
|
-
app = z.read("docProps/app.xml").decode("utf8", "replace")
|
|
129
|
-
for tag in ("Pages", "Words"):
|
|
130
|
-
m = re.search("<" + tag + ">(\\d+)</" + tag + ">", app)
|
|
131
|
-
if m:
|
|
132
|
-
out[tag.lower()] = int(m.group(1))
|
|
133
|
-
return out
|
|
134
|
-
except Exception as exc:
|
|
135
|
-
return {"unavailable": "docx read failed: " + str(exc)[:80]}
|
|
136
|
-
|
|
137
|
-
def describe(path):
|
|
138
|
-
ext = path.lower()
|
|
139
|
-
if ext.endswith(".pdf"):
|
|
140
|
-
return pdf_fields(path)
|
|
141
|
-
if ext.endswith(".docx"):
|
|
142
|
-
return docx_fields(path)
|
|
143
|
-
if ext.endswith((".h5", ".hdf5", ".h5ad", ".loom")):
|
|
144
|
-
return hdf5_fields(path)
|
|
145
|
-
raw = prefix_bytes(path)
|
|
146
|
-
if raw[:2] == b"\x1f\x8b":
|
|
147
|
-
wrapper = "bgzip" if len(raw) > 13 and raw[3] & 4 and raw[12:14] == b"BC" else "gzip"
|
|
148
|
-
inner = gunzip_prefix(raw)
|
|
149
|
-
if inner[:4] == b"BAM\x01":
|
|
150
|
-
l_text = int.from_bytes(inner[4:8], "little")
|
|
151
|
-
header = inner[8:8 + min(l_text, 65536)].decode("utf8", "replace")
|
|
152
|
-
fields = text_fields(header)
|
|
153
|
-
fields["wrapper"] = wrapper
|
|
154
|
-
fields["container"] = "bam"
|
|
155
|
-
return fields
|
|
156
|
-
if inner[:4] == b"BCF\x02" or inner[:3] == b"BCF":
|
|
157
|
-
return {"wrapper": wrapper, "container": "bcf"}
|
|
158
|
-
fields = text_fields(inner.decode("utf8", "replace"))
|
|
159
|
-
fields["wrapper"] = wrapper
|
|
160
|
-
return fields
|
|
161
|
-
if raw[:8] == b"\x89HDF\r\n\x1a\n":
|
|
162
|
-
return hdf5_fields(path)
|
|
163
|
-
return text_fields(raw.decode("utf8", "replace"))
|
|
164
|
-
|
|
165
|
-
for path in sys.argv[1:]:
|
|
166
|
-
try:
|
|
167
|
-
fields = describe(path)
|
|
168
|
-
except Exception as exc:
|
|
169
|
-
fields = {"unavailable": type(exc).__name__ + ": " + str(exc)[:80]}
|
|
170
|
-
note = fields.pop("unavailable", None)
|
|
171
|
-
record = {"path": path, "fields": fields}
|
|
172
|
-
if note:
|
|
173
|
-
record["unavailable"] = note
|
|
174
|
-
sys.stdout.write(json.dumps(record) + "\n")
|
|
175
|
-
`;
|
|
63
|
+
}));
|
|
64
|
+
const { sandboxClient, sandbox } = args;
|
|
65
|
+
if (containers.length > 0 && sandboxClient && sandbox) {
|
|
66
|
+
for (const [path, readout] of await decodeContainers(args, { sandboxClient, sandbox }, containers))
|
|
67
|
+
readouts.set(path, readout);
|
|
68
|
+
}
|
|
69
|
+
return readouts;
|
|
70
|
+
}
|
|
176
71
|
/**
|
|
177
|
-
*
|
|
72
|
+
* Read one member per shape and attach the readout.
|
|
178
73
|
*
|
|
179
|
-
* A shape whose
|
|
180
|
-
* shape
|
|
181
|
-
* structural observation the agent's grouping rests on.
|
|
74
|
+
* A shape whose readout failed keeps its `unavailable` note rather than losing the
|
|
75
|
+
* shape, for the same reason.
|
|
182
76
|
*/
|
|
183
77
|
export async function enrichShapes(args) {
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
78
|
+
const targets = new Map();
|
|
79
|
+
for (const shape of args.shapes) {
|
|
80
|
+
const path = shape.examplePaths.slice(0, MEMBERS_DECODED_PER_SHAPE)[0];
|
|
81
|
+
if (path !== undefined && !targets.has(path))
|
|
82
|
+
targets.set(path, shape);
|
|
83
|
+
}
|
|
84
|
+
if (targets.size === 0)
|
|
85
|
+
return [...args.shapes];
|
|
86
|
+
const readouts = await readHeaders({
|
|
87
|
+
...args,
|
|
88
|
+
targets: [...targets.entries()].map(([path, shape]) => ({ path, format: shape.format, ...(shape.wrapper ? { wrapper: shape.wrapper } : {}) })),
|
|
89
|
+
});
|
|
90
|
+
const byShape = new Map();
|
|
91
|
+
for (const [path, shape] of targets) {
|
|
92
|
+
const readout = readouts.get(path);
|
|
93
|
+
if (readout)
|
|
94
|
+
byShape.set(shape.id, readout);
|
|
192
95
|
}
|
|
96
|
+
return args.shapes.map((shape) => {
|
|
97
|
+
const header = byShape.get(shape.id);
|
|
98
|
+
return header ? { ...shape, header } : shape;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
async function readFromPrefix(args, target) {
|
|
102
|
+
const { path } = target;
|
|
103
|
+
const read = await args.fs
|
|
104
|
+
.readBytes({ session: args.session, path, length: READOUT_PREFIX_BYTES })
|
|
105
|
+
.unwrapOr({ kind: "read_failed" });
|
|
106
|
+
if (read.kind !== "ok")
|
|
107
|
+
return { path, fields: {}, unavailable: `prefix unreadable (${read.kind})` };
|
|
108
|
+
const readout = await readPrefix({ prefix: read.bytes, format: target.format, ...(target.wrapper ? { wrapper: target.wrapper } : {}) });
|
|
109
|
+
return { path, fields: readout.fields, ...(readout.unavailable ? { unavailable: readout.unavailable } : {}) };
|
|
110
|
+
}
|
|
111
|
+
async function decodeContainers(args, machine, containers) {
|
|
112
|
+
const readouts = new Map();
|
|
113
|
+
const source = await readFile(CONTAINER_DECODER, "utf8").catch(() => undefined);
|
|
114
|
+
if (source === undefined) {
|
|
115
|
+
for (const { path } of containers)
|
|
116
|
+
readouts.set(path, { path, fields: {}, unavailable: "container decoder is missing from this build" });
|
|
117
|
+
return readouts;
|
|
118
|
+
}
|
|
119
|
+
const byAbsolute = new Map();
|
|
120
|
+
for (const target of containers)
|
|
121
|
+
byAbsolute.set(`${args.mountRoot}/${target.path}`, target);
|
|
193
122
|
const result = await runSandboxExec({
|
|
194
|
-
sandboxClient:
|
|
195
|
-
sandbox:
|
|
123
|
+
sandboxClient: machine.sandboxClient,
|
|
124
|
+
sandbox: machine.sandbox,
|
|
196
125
|
execId: args.execId,
|
|
197
|
-
command: ["python3", "-c",
|
|
126
|
+
command: ["python3", "-c", source, ...byAbsolute.keys()],
|
|
198
127
|
timeoutSeconds: ENRICH_TIMEOUT_SECONDS,
|
|
199
128
|
deadlineMs: args.deadlineMs,
|
|
200
129
|
emit: args.emit,
|
|
201
130
|
});
|
|
202
|
-
const readouts = new Map();
|
|
203
131
|
for (const line of result.stdout.split("\n")) {
|
|
204
132
|
const trimmed = line.trim();
|
|
205
133
|
if (!trimmed.startsWith("{"))
|
|
@@ -211,17 +139,34 @@ export async function enrichShapes(args) {
|
|
|
211
139
|
catch {
|
|
212
140
|
continue;
|
|
213
141
|
}
|
|
214
|
-
const
|
|
215
|
-
if (!
|
|
142
|
+
const target = parsed.path ? byAbsolute.get(parsed.path) : undefined;
|
|
143
|
+
if (!target)
|
|
216
144
|
continue;
|
|
217
|
-
readouts.set(
|
|
218
|
-
path:
|
|
145
|
+
readouts.set(target.path, {
|
|
146
|
+
path: target.path,
|
|
219
147
|
fields: parsed.fields ?? {},
|
|
220
148
|
...(parsed.unavailable ? { unavailable: parsed.unavailable } : {}),
|
|
221
149
|
});
|
|
222
150
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
151
|
+
// A target the decoder said nothing about is told WHY, from the exec's own outcome: the
|
|
152
|
+
// decoder reports per path and exits 0 even when every path failed, so a silent gap is
|
|
153
|
+
// the exec itself having timed out, died, or never started — and "reported nothing" on
|
|
154
|
+
// its own is the one thing a reader cannot diagnose.
|
|
155
|
+
const failure = describeExecFailure(result);
|
|
156
|
+
for (const { path } of containers) {
|
|
157
|
+
if (!readouts.has(path))
|
|
158
|
+
readouts.set(path, { path, fields: {}, unavailable: failure });
|
|
159
|
+
}
|
|
160
|
+
return readouts;
|
|
161
|
+
}
|
|
162
|
+
function describeExecFailure(result) {
|
|
163
|
+
if (result.timedOut)
|
|
164
|
+
return `container decoder timed out after ${ENRICH_TIMEOUT_SECONDS}s`;
|
|
165
|
+
if (result.syntheticFailure)
|
|
166
|
+
return `container decoder did not complete: ${result.syntheticFailure.reason}`;
|
|
167
|
+
if (result.exitCode !== 0) {
|
|
168
|
+
const stderr = result.stderr.replace(/\s+/g, " ").trim().slice(0, MAX_STDERR_CHARS);
|
|
169
|
+
return `container decoder exited ${result.exitCode ?? "with no code"}${stderr ? `: ${stderr}` : ""}`;
|
|
170
|
+
}
|
|
171
|
+
return "container decoder reported nothing for this member";
|
|
227
172
|
}
|
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
* Format detection over a bounded byte prefix.
|
|
3
3
|
*
|
|
4
4
|
* A prefix read compared against a magic-byte table is NOT a decode: no parser
|
|
5
|
-
* runs over the bytes, so this belongs in the harness process.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* runs over the bytes, so this belongs in the harness process. So does every
|
|
6
|
+
* readout a bounded prefix suffices for (see `readout.ts`); a parser that must
|
|
7
|
+
* walk a whole container stays in the sandbox (see `enrich.ts`).
|
|
8
8
|
*
|
|
9
9
|
* Compression wrappers are reported ALONGSIDE the inner format, never in place of
|
|
10
10
|
* it: `.vcf.gz` is a VCF, and a shape observed on the wrapper would present
|
|
11
11
|
* unrelated data as mechanically alike. The wrapper comes from the bytes; the inner
|
|
12
|
-
* format of a wrapped file comes from its extension chain, which the
|
|
13
|
-
*
|
|
12
|
+
* format of a wrapped file comes from its extension chain, which the header
|
|
13
|
+
* readout then confirms against the decompressed prefix.
|
|
14
14
|
*
|
|
15
15
|
* The recognised list is a floor, not a closed set. An unrecognised format reports
|
|
16
16
|
* `unknown` with its extension chain preserved, and still joins a shape — shape
|
|
@@ -18,6 +18,14 @@
|
|
|
18
18
|
*/
|
|
19
19
|
/** Bytes read per file for detection. Enough for every magic below plus a text sniff line. */
|
|
20
20
|
export declare const MAGIC_PREFIX_BYTES = 512;
|
|
21
|
+
/**
|
|
22
|
+
* Every suffix the table above recognises, plus the compression wrappers.
|
|
23
|
+
*
|
|
24
|
+
* A dotted token outside this set belongs to the stem: `calls.somatic.vcf.gz` has a
|
|
25
|
+
* two-token extension chain and a two-token stem, and reading `somatic` as part of
|
|
26
|
+
* the extension would hide a categorical position inside what looks like a format.
|
|
27
|
+
*/
|
|
28
|
+
export declare const KNOWN_EXTENSIONS: ReadonlySet<string>;
|
|
21
29
|
/** The extension chain of a basename, outermost last. `sample.vcf.gz` → `["vcf", "gz"]`. */
|
|
22
30
|
export declare function extensionChain(fileName: string): string[];
|
|
23
31
|
/** The extension chain with any compression wrappers stripped from the tail. */
|
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
* Format detection over a bounded byte prefix.
|
|
3
3
|
*
|
|
4
4
|
* A prefix read compared against a magic-byte table is NOT a decode: no parser
|
|
5
|
-
* runs over the bytes, so this belongs in the harness process.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* runs over the bytes, so this belongs in the harness process. So does every
|
|
6
|
+
* readout a bounded prefix suffices for (see `readout.ts`); a parser that must
|
|
7
|
+
* walk a whole container stays in the sandbox (see `enrich.ts`).
|
|
8
8
|
*
|
|
9
9
|
* Compression wrappers are reported ALONGSIDE the inner format, never in place of
|
|
10
10
|
* it: `.vcf.gz` is a VCF, and a shape observed on the wrapper would present
|
|
11
11
|
* unrelated data as mechanically alike. The wrapper comes from the bytes; the inner
|
|
12
|
-
* format of a wrapped file comes from its extension chain, which the
|
|
13
|
-
*
|
|
12
|
+
* format of a wrapped file comes from its extension chain, which the header
|
|
13
|
+
* readout then confirms against the decompressed prefix.
|
|
14
14
|
*
|
|
15
15
|
* The recognised list is a floor, not a closed set. An unrecognised format reports
|
|
16
16
|
* `unknown` with its extension chain preserved, and still joins a shape — shape
|
|
@@ -110,6 +110,14 @@ const EXTENSION_FORMATS = {
|
|
|
110
110
|
yml: "yaml",
|
|
111
111
|
log: "text",
|
|
112
112
|
};
|
|
113
|
+
/**
|
|
114
|
+
* Every suffix the table above recognises, plus the compression wrappers.
|
|
115
|
+
*
|
|
116
|
+
* A dotted token outside this set belongs to the stem: `calls.somatic.vcf.gz` has a
|
|
117
|
+
* two-token extension chain and a two-token stem, and reading `somatic` as part of
|
|
118
|
+
* the extension would hide a categorical position inside what looks like a format.
|
|
119
|
+
*/
|
|
120
|
+
export const KNOWN_EXTENSIONS = new Set([...Object.keys(EXTENSION_FORMATS), ...WRAPPER_EXTENSIONS]);
|
|
113
121
|
/**
|
|
114
122
|
* `.bed` is PLINK's binary genotype table AND the interval format, and only the
|
|
115
123
|
* bytes tell them apart (PLINK writes a three-byte magic).
|
|
@@ -4,9 +4,15 @@
|
|
|
4
4
|
* The scan observes; the profiler agent groups. Nothing exported here names a kind
|
|
5
5
|
* or an axis (see the input-scan-manifest spec).
|
|
6
6
|
*/
|
|
7
|
-
export { MAGIC_PREFIX_BYTES, detectFormat, extensionChain, innerExtensions, type DetectedFormat } from "./formats.js";
|
|
7
|
+
export { KNOWN_EXTENSIONS, MAGIC_PREFIX_BYTES, detectFormat, extensionChain, innerExtensions, type DetectedFormat } from "./formats.js";
|
|
8
|
+
export { detectSets, type DetectSetsOptions } from "./detect-sets.js";
|
|
9
|
+
export { type ClusterEvidence, type ContentSimilarity } from "./clustering.js";
|
|
8
10
|
export { MAX_SAMPLE_VALUES, MAX_SHAPES, nameStem, observeShapes, tokenizeStem, type ObservedShapes } from "./shapes.js";
|
|
9
|
-
export { MAX_SCANNED_FILES, buildManifest, renderInputScanManifest, scanInputTree, type ScanInputTreeArgs } from "./scan.js";
|
|
10
|
-
export {
|
|
11
|
-
export {
|
|
11
|
+
export { MAX_SCANNED_FILES, buildManifest, formatBytes, renderInputScanManifest, scanInputTree, type ScanInputTreeArgs } from "./scan.js";
|
|
12
|
+
export { MAX_CORRESPONDENCES, MAX_CORRESPONDENCE_SAMPLE, MAX_MENU_SETS, buildCorrespondences, buildSetMenu, renderSetMenu, type SetMenu, type SlotCorrespondence, type UnlistedSets, } from "./menu.js";
|
|
13
|
+
export { readoutTargets, selectReadouts, type ReadoutTarget } from "./readout-budget.js";
|
|
14
|
+
export { MEMBERS_DECODED_PER_SHAPE, SANDBOX_CONTAINER_FORMATS, enrichShapes, readHeaders, type EnrichShapesArgs, type ReadHeadersArgs, type ReadoutTargetSpec, } from "./enrich.js";
|
|
15
|
+
export { READOUT_PREFIX_BYTES, READOUT_TEXT_BYTES, readPrefix, type PrefixReadout, type ReadoutFields } from "./readout.js";
|
|
12
16
|
export type { FileShape, FormatCount, HeaderReadout, InputScan, InputScanManifest, PositionCooccurrence, ScannedFile, ShapeValueOverlap, UnstructuredEntry, UnstructuredFiles, VariablePosition, } from "./types.js";
|
|
17
|
+
export type { CompanionFile, DetectedSet, DetectedSets, IncompleteMember, LeftoverFiles, MemberFile, QuarantineReason, QuarantineReasonCount, QuarantineSummary, ReadoutSelection, SetCompleteness, SetMember, SetOrigin, SetRepresentative, SetSlot, SlotLocation, TemplateSegment, WrapperCount, } from "./set-types.js";
|
|
18
|
+
export type { SlotTokenClass } from "./tokens.js";
|
package/dist/input-scan/index.js
CHANGED
|
@@ -4,8 +4,11 @@
|
|
|
4
4
|
* The scan observes; the profiler agent groups. Nothing exported here names a kind
|
|
5
5
|
* or an axis (see the input-scan-manifest spec).
|
|
6
6
|
*/
|
|
7
|
-
export { MAGIC_PREFIX_BYTES, detectFormat, extensionChain, innerExtensions } from "./formats.js";
|
|
7
|
+
export { KNOWN_EXTENSIONS, MAGIC_PREFIX_BYTES, detectFormat, extensionChain, innerExtensions } from "./formats.js";
|
|
8
|
+
export { detectSets } from "./detect-sets.js";
|
|
8
9
|
export { MAX_SAMPLE_VALUES, MAX_SHAPES, nameStem, observeShapes, tokenizeStem } from "./shapes.js";
|
|
9
|
-
export { MAX_SCANNED_FILES, buildManifest, renderInputScanManifest, scanInputTree } from "./scan.js";
|
|
10
|
-
export {
|
|
11
|
-
export {
|
|
10
|
+
export { MAX_SCANNED_FILES, buildManifest, formatBytes, renderInputScanManifest, scanInputTree } from "./scan.js";
|
|
11
|
+
export { MAX_CORRESPONDENCES, MAX_CORRESPONDENCE_SAMPLE, MAX_MENU_SETS, buildCorrespondences, buildSetMenu, renderSetMenu, } from "./menu.js";
|
|
12
|
+
export { readoutTargets, selectReadouts } from "./readout-budget.js";
|
|
13
|
+
export { MEMBERS_DECODED_PER_SHAPE, SANDBOX_CONTAINER_FORMATS, enrichShapes, readHeaders, } from "./enrich.js";
|
|
14
|
+
export { READOUT_PREFIX_BYTES, READOUT_TEXT_BYTES, readPrefix } from "./readout.js";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stage 1 — a recognised marker claims its subtree.
|
|
3
|
+
*
|
|
4
|
+
* Some collections announce themselves. Where a marker file names the collection, it
|
|
5
|
+
* identifies it more precisely than its filenames ever could, so the claimed files
|
|
6
|
+
* skip inference entirely: mining a matrix triplet into name templates would report
|
|
7
|
+
* three unrelated singletons where the directory is one thing.
|
|
8
|
+
*
|
|
9
|
+
* The catalogue is a floor, not a closed set.
|
|
10
|
+
*/
|
|
11
|
+
import type { MemberFile } from "./set-types.js";
|
|
12
|
+
export interface MarkerUnit {
|
|
13
|
+
readonly label: string;
|
|
14
|
+
readonly root: string;
|
|
15
|
+
readonly members: readonly MemberFile[];
|
|
16
|
+
}
|
|
17
|
+
/** The marker-claimed units of a tree, and the members no marker claimed. */
|
|
18
|
+
export declare function applyMarkers(files: readonly MemberFile[]): {
|
|
19
|
+
units: MarkerUnit[];
|
|
20
|
+
unclaimed: MemberFile[];
|
|
21
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stage 1 — a recognised marker claims its subtree.
|
|
3
|
+
*
|
|
4
|
+
* Some collections announce themselves. Where a marker file names the collection, it
|
|
5
|
+
* identifies it more precisely than its filenames ever could, so the claimed files
|
|
6
|
+
* skip inference entirely: mining a matrix triplet into name templates would report
|
|
7
|
+
* three unrelated singletons where the directory is one thing.
|
|
8
|
+
*
|
|
9
|
+
* The catalogue is a floor, not a closed set.
|
|
10
|
+
*/
|
|
11
|
+
import { basenameOf, dirnameOf } from "./tokens.js";
|
|
12
|
+
/** A marker whose presence claims the directory it sits in, and nothing below it. */
|
|
13
|
+
const DIRECTORY_MARKERS = [
|
|
14
|
+
{
|
|
15
|
+
label: "feature-barcode-matrix",
|
|
16
|
+
requires: [/^matrix\.mtx(\.gz)?$/, /^barcodes\.tsv(\.gz)?$/, /^(features|genes)\.tsv(\.gz)?$/],
|
|
17
|
+
},
|
|
18
|
+
];
|
|
19
|
+
/**
|
|
20
|
+
* A marker whose presence claims its whole subtree.
|
|
21
|
+
*
|
|
22
|
+
* A descriptor may need corroboration: a bare descriptor file can sit at the root of a
|
|
23
|
+
* tree it does not describe, and letting it claim everything below would swallow the
|
|
24
|
+
* dataset it happens to share a directory with.
|
|
25
|
+
*/
|
|
26
|
+
const SUBTREE_MARKERS = [
|
|
27
|
+
{ label: "study-manifest", name: "meta_study.txt" },
|
|
28
|
+
{ label: "dataset-descriptor", name: "dataset_description.json", corroborate: /^sub-/ },
|
|
29
|
+
];
|
|
30
|
+
function hasCorroboratingChild(files, root, pattern) {
|
|
31
|
+
const prefix = root === "" ? "" : `${root}/`;
|
|
32
|
+
return files.some((file) => file.path.startsWith(prefix) && pattern.test(file.path.slice(prefix.length).split("/")[0]));
|
|
33
|
+
}
|
|
34
|
+
/** The marker-claimed units of a tree, and the members no marker claimed. */
|
|
35
|
+
export function applyMarkers(files) {
|
|
36
|
+
const byDirectory = new Map();
|
|
37
|
+
for (const file of files) {
|
|
38
|
+
const directory = dirnameOf(file.path);
|
|
39
|
+
const bucket = byDirectory.get(directory);
|
|
40
|
+
if (bucket)
|
|
41
|
+
bucket.push(file);
|
|
42
|
+
else
|
|
43
|
+
byDirectory.set(directory, [file]);
|
|
44
|
+
}
|
|
45
|
+
const subtrees = [];
|
|
46
|
+
const directories = [];
|
|
47
|
+
for (const [directory, members] of byDirectory) {
|
|
48
|
+
const names = members.map((member) => basenameOf(member.path));
|
|
49
|
+
for (const marker of DIRECTORY_MARKERS) {
|
|
50
|
+
if (marker.requires.every((pattern) => names.some((name) => pattern.test(name))))
|
|
51
|
+
directories.push({ label: marker.label, root: directory });
|
|
52
|
+
}
|
|
53
|
+
for (const marker of SUBTREE_MARKERS) {
|
|
54
|
+
if (!names.includes(marker.name))
|
|
55
|
+
continue;
|
|
56
|
+
if (marker.corroborate && !hasCorroboratingChild(files, directory, marker.corroborate))
|
|
57
|
+
continue;
|
|
58
|
+
subtrees.push({ label: marker.label, root: directory });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// An outer claim wins: a study that contains a descriptor is one study, not two units.
|
|
62
|
+
subtrees.sort((a, b) => a.root.length - b.root.length);
|
|
63
|
+
const outermost = [];
|
|
64
|
+
for (const candidate of subtrees) {
|
|
65
|
+
const covered = outermost.some((claim) => candidate.root === claim.root || candidate.root.startsWith(claim.root === "" ? "" : `${claim.root}/`));
|
|
66
|
+
if (!covered)
|
|
67
|
+
outermost.push(candidate);
|
|
68
|
+
}
|
|
69
|
+
const claimed = new Set();
|
|
70
|
+
const units = [];
|
|
71
|
+
for (const claim of outermost) {
|
|
72
|
+
const prefix = claim.root === "" ? "" : `${claim.root}/`;
|
|
73
|
+
const members = files.filter((file) => !claimed.has(file.path) && file.path.startsWith(prefix));
|
|
74
|
+
if (members.length === 0)
|
|
75
|
+
continue;
|
|
76
|
+
for (const member of members)
|
|
77
|
+
claimed.add(member.path);
|
|
78
|
+
units.push({ label: claim.label, root: claim.root, members });
|
|
79
|
+
}
|
|
80
|
+
for (const claim of directories) {
|
|
81
|
+
const members = (byDirectory.get(claim.root) ?? []).filter((file) => !claimed.has(file.path));
|
|
82
|
+
if (members.length === 0)
|
|
83
|
+
continue;
|
|
84
|
+
for (const member of members)
|
|
85
|
+
claimed.add(member.path);
|
|
86
|
+
units.push({ label: claim.label, root: claim.root, members });
|
|
87
|
+
}
|
|
88
|
+
return { units, unclaimed: files.filter((file) => !claimed.has(file.path)) };
|
|
89
|
+
}
|