@inflexa-ai/harness 0.28.1 → 0.30.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/agents/conversation-agent.js +7 -4
- package/dist/agents/report-session-agent.js +1 -1
- package/dist/agents/sandbox/data-profiler.js +0 -1
- package/dist/agents/sandbox/immune-profiling-agent.js +3 -1
- package/dist/agents/sandbox/network-agent.js +0 -1
- package/dist/agents/sandbox/types.d.ts +1 -1
- package/dist/agents/sandbox/types.js +1 -1
- 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/config/resource-limits.d.ts +1 -1
- package/dist/config/resource-limits.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/schemas/chat-parts.d.ts +12 -12
- package/dist/contracts/severity.d.ts +2 -2
- package/dist/contracts/target-dossier.d.ts +91 -91
- package/dist/data/safety-panel-schema.d.ts +4 -4
- package/dist/execution/input-signature.d.ts +16 -3
- package/dist/execution/input-signature.js +0 -0
- package/dist/execution/post-step-pipeline.js +1 -1
- package/dist/execution/staged-input.d.ts +4 -4
- package/dist/index.d.ts +7 -2
- package/dist/index.js +14 -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/loop/run-agent.d.ts +13 -1
- package/dist/loop/run-agent.js +2 -1
- package/dist/prompts/SOUL.d.ts +2 -2
- package/dist/prompts/SOUL.js +17 -1
- package/dist/prompts/conversation.d.ts +1 -1
- package/dist/prompts/conversation.js +45 -71
- package/dist/prompts/planner.js +57 -10
- package/dist/prompts/report-session.d.ts +1 -1
- package/dist/prompts/report-session.js +3 -2
- package/dist/prompts/sandbox/bulk-transcriptomics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/bulk-transcriptomics-agent.js +3 -1
- package/dist/prompts/sandbox/data-profiler.d.ts +10 -1
- package/dist/prompts/sandbox/data-profiler.js +197 -53
- package/dist/prompts/sandbox/dna-methylation-agent.d.ts +1 -1
- package/dist/prompts/sandbox/dna-methylation-agent.js +18 -12
- package/dist/prompts/sandbox/immune-profiling-agent.d.ts +1 -1
- package/dist/prompts/sandbox/immune-profiling-agent.js +6 -2
- package/dist/prompts/sandbox/metabolomics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/metabolomics-agent.js +8 -4
- package/dist/prompts/sandbox/microbiome-agent.d.ts +1 -1
- package/dist/prompts/sandbox/microbiome-agent.js +12 -6
- package/dist/prompts/sandbox/network-agent.d.ts +1 -1
- package/dist/prompts/sandbox/network-agent.js +9 -6
- package/dist/prompts/sandbox/spatial-omics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/spatial-omics-agent.js +4 -2
- package/dist/prompts/sandbox-standards.d.ts +1 -1
- package/dist/prompts/sandbox-standards.js +10 -7
- package/dist/providers/ai-sdk.js +34 -1
- package/dist/providers/reasoning.d.ts +65 -0
- package/dist/providers/reasoning.js +81 -0
- package/dist/providers/types.d.ts +14 -0
- package/dist/schemas/data-profile-schemas.d.ts +947 -84
- package/dist/schemas/data-profile-schemas.js +300 -115
- package/dist/schemas/run-synthesis.d.ts +4 -4
- 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/bio/chembl.d.ts +1 -1
- package/dist/tools/bio/chembl.js +15 -10
- package/dist/tools/bio/comptox.js +5 -1
- package/dist/tools/bio/drug-gene-interactions.js +5 -2
- package/dist/tools/bio/gene-disease-evidence.d.ts +19 -9
- package/dist/tools/bio/gene-disease-evidence.js +78 -22
- package/dist/tools/bio/gene-preclinical-profile.d.ts +33 -13
- package/dist/tools/bio/gene-preclinical-profile.js +63 -22
- package/dist/tools/bio/lookup-annotation.d.ts +1 -0
- package/dist/tools/bio/lookup-annotation.js +28 -10
- package/dist/tools/bio/opentargets.d.ts +10 -2
- package/dist/tools/bio/opentargets.js +42 -13
- package/dist/tools/bio/pubchem.js +5 -2
- package/dist/tools/bio/pubmed.js +9 -3
- package/dist/tools/bio/search-clinical-trials.d.ts +25 -25
- package/dist/tools/bio/search-clinical-trials.js +56 -18
- package/dist/tools/bio/search-faers.d.ts +26 -6
- package/dist/tools/bio/search-faers.js +70 -18
- package/dist/tools/bio/search-gene.d.ts +19 -3
- package/dist/tools/bio/search-gene.js +69 -12
- package/dist/tools/bio/search-geo-datasets.js +4 -1
- package/dist/tools/bio/search-interactions.js +6 -2
- package/dist/tools/bio/target-safety.d.ts +2 -2
- package/dist/tools/bio/target-safety.js +3 -2
- package/dist/tools/lib/cbioportal-client.js +25 -6
- package/dist/tools/lib/chembl-client.d.ts +3 -1
- package/dist/tools/lib/chembl-client.js +6 -1
- package/dist/tools/lib/gwas-catalog-client.d.ts +7 -6
- package/dist/tools/lib/gwas-catalog-client.js +18 -6
- package/dist/tools/lib/openfda-client.d.ts +7 -5
- package/dist/tools/lib/openfda-client.js +17 -9
- package/dist/tools/lib/opentargets-client.d.ts +20 -0
- package/dist/tools/lib/opentargets-client.js +45 -0
- package/dist/tools/lib/pathway-client.d.ts +7 -0
- package/dist/tools/lib/pathway-client.js +68 -1
- package/dist/tools/research/context7-docs.js +3 -3
- package/dist/tools/research/generate-analogy-report.js +2 -2
- package/dist/tools/research/generate-plan.d.ts +3 -0
- package/dist/tools/research/generate-plan.js +64 -5
- package/dist/tools/research/inspect-data-profile.d.ts +48 -22
- package/dist/tools/research/inspect-data-profile.js +73 -54
- package/dist/tools/research/resolve-citation.js +8 -3
- package/dist/tools/research/search-arxiv.js +8 -3
- package/dist/tools/research/search-semantic-scholar.js +10 -4
- 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.d.ts +6 -0
- package/dist/workflows/sandbox-step.js +31 -14
- package/dist/workflows/target-assessment/assemblers/safety.d.ts +1 -1
- package/dist/workflows/target-assessment/phase4-assemble.d.ts +38 -38
- package/dist/workflows/target-assessment/schemas.d.ts +12 -12
- package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +6 -6
- package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +6 -6
- package/dist/workflows/target-assessment/synthesis/index.d.ts +12 -12
- package/package.json +5 -3
- package/dist/input-scan/coverage.d.ts +0 -22
- package/dist/input-scan/coverage.js +0 -85
|
@@ -1,43 +1,90 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Data-profile orientation — the bounded projection of a persisted
|
|
3
|
-
* `DataProfileResult` into
|
|
4
|
-
*
|
|
3
|
+
* `DataProfileResult` into what an agent needs to know WHAT DATASET IT IS HOLDING
|
|
4
|
+
* before it does anything else.
|
|
5
5
|
*
|
|
6
|
-
* The persisted profile is deliberately rich
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
6
|
+
* The persisted profile is deliberately rich because the DB row is its only durable
|
|
7
|
+
* home — the profiler's scratch tree is deleted on completion. That richness is right
|
|
8
|
+
* for a pull-on-demand tool (`inspect_data_profile`) and wrong for anything injected
|
|
9
|
+
* unconditionally into a context window, which is what a step seed does. This is the
|
|
10
|
+
* lossy half of that pair: a hard character budget, a fixed section order, and visible
|
|
11
|
+
* truncation counts, so an agent that needs more knows to go pull it.
|
|
12
|
+
*
|
|
13
|
+
* What is lossy is the PROSE, not the structure. The submission schema bounds every array
|
|
14
|
+
* an agent authors — operations, dimensions, annotations — so the resolved record is small
|
|
15
|
+
* by construction, and a rendering that dropped half the groups to make room for a design
|
|
16
|
+
* note would be discarding the addressable facts to keep the summary of them. Groups
|
|
17
|
+
* therefore render one line each, all of them, each followed by the slots that vary across
|
|
18
|
+
* its members; dimensions and notable files follow; and the design note and the caveats
|
|
19
|
+
* take whatever budget is left. Groups are elided only when one line apiece already
|
|
20
|
+
* overruns the whole budget, and then the count that did not fit is stated outright.
|
|
21
|
+
*
|
|
22
|
+
* The census — kept files, groups, unclassified, quarantined, and whether the scan reached
|
|
23
|
+
* the whole tree — rides in the header on a reserved budget, so no clamp can remove the one
|
|
24
|
+
* line that says how much of the dataset the profile speaks for.
|
|
25
|
+
*
|
|
26
|
+
* Every elision is marked. A section fitted short, a list tail dropped, a prose value
|
|
27
|
+
* clamped: each ends the rendering with {@link ELISION_MARKER}, whose room is reserved
|
|
28
|
+
* before anything else is fitted. A rendering that silently lost a section reads as a
|
|
29
|
+
* profile that never had one.
|
|
13
30
|
*
|
|
14
31
|
* A pure function over the record — no I/O, no LLM, no pipeline. It is the whole
|
|
15
32
|
* mechanism.
|
|
16
33
|
*
|
|
17
|
-
* Every path it renders is projected into the frame-independent
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
34
|
+
* Every path it renders is projected into the frame-independent `/{analysisId}/…`
|
|
35
|
+
* form. The record stores a path relative to the analysis root, which is the correct
|
|
36
|
+
* form to store; but a step agent resolves a relative path against its own working
|
|
37
|
+
* directory, thus the stored form names a file that does not exist there. The absolute
|
|
38
|
+
* form resolves to the same file in every frame, including the conversation agent's, so
|
|
39
|
+
* the projection is unconditional.
|
|
23
40
|
*/
|
|
24
41
|
import { toAnalysisRootPath } from "../workspace/paths.js";
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
42
|
+
import { profileCaveats, profileDimensions, profileFileRecords, profileGroups } from "./data-profile-view.js";
|
|
43
|
+
/**
|
|
44
|
+
* The character budget the projection guarantees it will not exceed.
|
|
45
|
+
*
|
|
46
|
+
* Sized from the record's own bounds rather than from a feel for how long a blurb should
|
|
47
|
+
* be: the census header, a group line plus a slots line for each of a submission's worth of
|
|
48
|
+
* groups, the dimension list, the notable files, and a prose tail. A profile past that
|
|
49
|
+
* elides its group tail with a stated count — which is the honest outcome, and a far
|
|
50
|
+
* cheaper one than a briefing that omits the groups an execution agent must address files
|
|
51
|
+
* by.
|
|
52
|
+
*/
|
|
53
|
+
export const DATA_PROFILE_ORIENTATION_MAX_CHARS = 6000;
|
|
54
|
+
/** Notable files listed before the tail is elided (the count still reports the total). */
|
|
28
55
|
const MAX_FILES = 8;
|
|
29
|
-
/**
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
/**
|
|
33
|
-
const
|
|
56
|
+
/** The group name's ceiling — long enough to identify, short enough to keep the line one line. */
|
|
57
|
+
const MAX_GROUP_NAME_CHARS = 48;
|
|
58
|
+
const MAX_GROUP_DESCRIPTION_CHARS = 90;
|
|
59
|
+
/** Formats named in a group's census before the rest are counted rather than named. */
|
|
60
|
+
const MAX_GROUP_FORMATS = 3;
|
|
61
|
+
/** Slots named on a group's slots line before the rest are counted. */
|
|
62
|
+
const MAX_SLOTS_PER_LINE = 4;
|
|
63
|
+
/** A slot's values ride inline only while it is categorical enough for them to mean something. */
|
|
64
|
+
const MAX_INLINE_SLOT_VALUES = 4;
|
|
65
|
+
const MAX_SLOT_VALUE_CHARS = 24;
|
|
66
|
+
const MAX_DIMENSION_EXAMPLES = 3;
|
|
67
|
+
/** Caveats listed — the profiler orders them, so these are the top ones. */
|
|
68
|
+
const MAX_CAVEATS = 3;
|
|
69
|
+
/**
|
|
70
|
+
* The share of the whole rendering agent-authored caveats may occupy.
|
|
71
|
+
*
|
|
72
|
+
* The per-item cap alone does not bound the section: three long caveats still displace
|
|
73
|
+
* the structure a planner reads. Prose is what gets cut when the two compete.
|
|
74
|
+
*/
|
|
75
|
+
const CAVEAT_SHARE = 0.25;
|
|
76
|
+
/** The whole identity line's ceiling, so the census below it always fits under the budget. */
|
|
77
|
+
const MAX_IDENTITY_CHARS = 240;
|
|
34
78
|
const MAX_DESIGN_CHARS = 200;
|
|
35
|
-
const
|
|
79
|
+
const MAX_CAVEAT_CHARS = 120;
|
|
36
80
|
const MAX_FILE_DESCRIPTION_CHARS = 100;
|
|
37
|
-
|
|
81
|
+
/** Stated whenever the budget — not a per-section cap — cost the rendering something. */
|
|
82
|
+
const ELISION_MARKER = "… trimmed to fit";
|
|
83
|
+
/** Prose shortened below this states nothing; its section is dropped instead. */
|
|
84
|
+
const MIN_PROSE_CHARS = 24;
|
|
38
85
|
/**
|
|
39
|
-
* Clamp to `max` chars, marking any elision with an ellipsis. Whitespace is left
|
|
40
|
-
*
|
|
86
|
+
* Clamp to `max` chars, marking any elision with an ellipsis. Whitespace is left alone —
|
|
87
|
+
* folding a multi-line value onto one line is `clip`'s job, not this one's.
|
|
41
88
|
*/
|
|
42
89
|
function clamp(text, max) {
|
|
43
90
|
if (text.length <= max)
|
|
@@ -76,26 +123,8 @@ function formatFileFacts(file) {
|
|
|
76
123
|
const facts = [dims, present(file.format)].filter((f) => f !== undefined);
|
|
77
124
|
return facts.length > 0 ? ` (${facts.join(", ")})` : "";
|
|
78
125
|
}
|
|
79
|
-
/**
|
|
80
|
-
|
|
81
|
-
* characters. The bound is a guarantee, not a target: the assembled text is
|
|
82
|
-
* hard-clamped, so no profile — however verbose — can blow a caller's budget.
|
|
83
|
-
*
|
|
84
|
-
* Field order is fixed and reflects what an agent must know first: what the
|
|
85
|
-
* dataset IS, how it was designed, what is wrong with it, then what files carry
|
|
86
|
-
* it. Both list sections state their true total, so an elided tail is visible
|
|
87
|
-
* rather than silent.
|
|
88
|
-
*
|
|
89
|
-
* A snapshot written before the record was widened carries none of the structured
|
|
90
|
-
* fields. Rather than emit a bare file list, that case falls back to the narrative
|
|
91
|
-
* summary — the one orientation a legacy row does have.
|
|
92
|
-
*
|
|
93
|
-
* `analysisId` roots each rendered file path (see the module header). It is the
|
|
94
|
-
* id of the analysis the profile belongs to, and it is what the sandbox mounts
|
|
95
|
-
* the tree at.
|
|
96
|
-
*/
|
|
97
|
-
export function buildDataProfileOrientation(result, analysisId, maxChars = DATA_PROFILE_ORIENTATION_MAX_CHARS) {
|
|
98
|
-
const lines = [];
|
|
126
|
+
/** What the dataset IS, or — on a snapshot carrying no structured identity — its summary. */
|
|
127
|
+
function identityLine(result) {
|
|
99
128
|
const identity = [
|
|
100
129
|
[present(result.domain), present(result.subtype)].filter((f) => f !== undefined).join(" / ") || undefined,
|
|
101
130
|
formatOrganism(result.organism),
|
|
@@ -103,56 +132,266 @@ export function buildDataProfileOrientation(result, analysisId, maxChars = DATA_
|
|
|
103
132
|
present(result.cellType) ? `cells: ${present(result.cellType)}` : undefined,
|
|
104
133
|
present(result.condition) ? `condition: ${present(result.condition)}` : undefined,
|
|
105
134
|
].filter((f) => f !== undefined);
|
|
106
|
-
|
|
107
|
-
|
|
135
|
+
return `Dataset: ${identity.length > 0 ? identity.join(" — ") : clip(result.summary, MAX_IDENTITY_CHARS)}`;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The census: how much of the tree the profile speaks for, as a count rather than a
|
|
139
|
+
* warning.
|
|
140
|
+
*
|
|
141
|
+
* A partition-era row states all four figures outright, so all four are rendered whether
|
|
142
|
+
* or not any of them is zero — a census with a term omitted is a census a reader has to
|
|
143
|
+
* guess at. An older row states what its era recorded and no more; one that states
|
|
144
|
+
* nothing gets no census line, because inventing one would put a number behind a fact the
|
|
145
|
+
* snapshot cannot support.
|
|
146
|
+
*/
|
|
147
|
+
function censusLine(result, groups) {
|
|
148
|
+
if (result.partition) {
|
|
149
|
+
const { keptFiles, unclassifiedFiles, quarantine, scanTruncated } = result.partition;
|
|
150
|
+
// A truncated walk makes every figure a figure over PART of the tree, so it rides the
|
|
151
|
+
// census for the same reason the census itself does: it is the qualification a reader
|
|
152
|
+
// must have before trusting any other number here, and no clamp may remove it.
|
|
153
|
+
const partial = scanTruncated ? " · SCAN INCOMPLETE, counts cover part of the tree" : "";
|
|
154
|
+
return `Census: ${keptFiles} files in ${groups.length} groups · ${unclassifiedFiles} unclassified · ${quarantine.count} quarantined${partial}`;
|
|
108
155
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
lines.push(`Dataset: ${clip(result.summary, MAX_SUMMARY_FALLBACK_CHARS)}`);
|
|
156
|
+
if (result.coverage) {
|
|
157
|
+
const unmatched = result.coverage.unmatched > 0 ? ` · ${result.coverage.unmatched} matching no kind` : "";
|
|
158
|
+
return `Census: ${result.coverage.total} files in ${groups.length} groups${unmatched}`;
|
|
113
159
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
160
|
+
if (groups.length === 0)
|
|
161
|
+
return undefined;
|
|
162
|
+
return `Census: ${groups.reduce((sum, group) => sum + group.count, 0)} files in ${groups.length} groups`;
|
|
163
|
+
}
|
|
164
|
+
/** What a block of lines costs once joined — its characters plus its separators. */
|
|
165
|
+
function costOf(block) {
|
|
166
|
+
return block.reduce((sum, line) => sum + line.length, 0) + Math.max(0, block.length - 1);
|
|
167
|
+
}
|
|
168
|
+
/** The resolved records behind the era-independent views, for the facts only they carry. */
|
|
169
|
+
function byId(records) {
|
|
170
|
+
return new Map((records ?? []).map((record) => [record.id, record]));
|
|
171
|
+
}
|
|
172
|
+
/** `VCF` for a single-format group, `VCF 40, TBI 40` for a mixed one. */
|
|
173
|
+
function formatCensus(view, group) {
|
|
174
|
+
const formats = (group?.formats ?? []).filter((entry) => present(entry.format) !== undefined);
|
|
175
|
+
if (formats.length === 0)
|
|
176
|
+
return present(view.format);
|
|
177
|
+
if (formats.length === 1)
|
|
178
|
+
return present(formats[0].format);
|
|
179
|
+
const shown = formats.slice(0, MAX_GROUP_FORMATS).map((entry) => `${entry.format} ${entry.count}`);
|
|
180
|
+
return `${shown.join(", ")}${formats.length > shown.length ? `, +${formats.length - shown.length}` : ""}`;
|
|
181
|
+
}
|
|
182
|
+
/** `name1 digits-fixed ×40` — where a slot sits, what it looks like, how far it runs. */
|
|
183
|
+
function renderSlot(slot) {
|
|
184
|
+
const where = slot.location === "directory" ? "dir" : "name";
|
|
185
|
+
const inline = slot.distinctValues <= MAX_INLINE_SLOT_VALUES && slot.sampleValues.length > 0
|
|
186
|
+
? ` (${slot.sampleValues
|
|
187
|
+
.slice(0, MAX_INLINE_SLOT_VALUES)
|
|
188
|
+
.map((value) => clip(value, MAX_SLOT_VALUE_CHARS))
|
|
189
|
+
.join(", ")})`
|
|
190
|
+
: "";
|
|
191
|
+
return `${where}${slot.index} ${slot.tokenClass} ×${slot.distinctValues}${inline}`;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* The varying positions in a group's members, which is how an execution agent addresses
|
|
195
|
+
* one of them.
|
|
196
|
+
*
|
|
197
|
+
* A single-member group's slot varies over nothing, so it is noise; every other slotted
|
|
198
|
+
* group gets the line. A legacy row carries no slots and gets none.
|
|
199
|
+
*/
|
|
200
|
+
function slotsLine(group, members) {
|
|
201
|
+
const slots = group?.slots ?? [];
|
|
202
|
+
if (slots.length === 0 || (slots.length === 1 && members <= 1))
|
|
203
|
+
return undefined;
|
|
204
|
+
const shown = slots.slice(0, MAX_SLOTS_PER_LINE).map(renderSlot);
|
|
205
|
+
const more = slots.length > shown.length ? ` · +${slots.length - shown.length} more` : "";
|
|
206
|
+
return ` slots: ${shown.join(" · ")}${more}`;
|
|
207
|
+
}
|
|
208
|
+
/** One group: what it is and how many, then the positions its members vary at. */
|
|
209
|
+
function groupEntry(view, group) {
|
|
210
|
+
const facts = [`${view.count}x`, formatCensus(view, group), present(group?.categoryLabel ?? group?.category)]
|
|
211
|
+
.filter((fact) => fact !== undefined)
|
|
212
|
+
.join(", ");
|
|
213
|
+
const line = `- ${clip(view.name, MAX_GROUP_NAME_CHARS)} (${facts}) — ${clip(view.memberRepresents, MAX_GROUP_DESCRIPTION_CHARS)}`;
|
|
214
|
+
const slots = slotsLine(group, view.count);
|
|
215
|
+
return slots ? [line, slots] : [line];
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Every group, one line each plus its slots.
|
|
219
|
+
*
|
|
220
|
+
* Groups are the addressable structure, so they are reserved ahead of everything the
|
|
221
|
+
* budget might otherwise spend on prose. They elide only when one line apiece already
|
|
222
|
+
* overruns the whole budget, and then the tail that did not fit is counted rather than
|
|
223
|
+
* quietly absent.
|
|
224
|
+
*/
|
|
225
|
+
function groupsSection(entries, budget) {
|
|
226
|
+
const head = `Groups (${entries.length}):`;
|
|
227
|
+
for (let shown = entries.length; shown >= 0; shown--) {
|
|
228
|
+
const dropped = entries.length - shown;
|
|
229
|
+
const tail = dropped > 0 ? [`…and ${dropped} more group${dropped === 1 ? "" : "s"}`] : [];
|
|
230
|
+
const block = [head, ...entries.slice(0, shown).flat(), ...tail];
|
|
231
|
+
if (costOf(block) <= budget)
|
|
232
|
+
return { block, dropped };
|
|
122
233
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
234
|
+
return { block: [], dropped: entries.length };
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* A headed list rendered with as many entries as the budget holds. An entry is whole or
|
|
238
|
+
* absent — a structural line cut mid-word states a fact that is not true — and the head
|
|
239
|
+
* counts what survived against the true total. `dropped` reports the tail the budget cost,
|
|
240
|
+
* over and above whatever the section's own cap already removed.
|
|
241
|
+
*/
|
|
242
|
+
function listSection(label, total, entries, budget) {
|
|
243
|
+
for (let shown = entries.length; shown > 0; shown--) {
|
|
244
|
+
const head = shown < total ? `${label} (${shown} of ${total}):` : `${label} (${total}):`;
|
|
245
|
+
const block = [head, ...entries.slice(0, shown)];
|
|
246
|
+
if (costOf(block) <= budget)
|
|
247
|
+
return { block, dropped: entries.length - shown };
|
|
248
|
+
}
|
|
249
|
+
return { block: [], dropped: entries.length };
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* One dimension: every number its observations reported, side by side.
|
|
253
|
+
*
|
|
254
|
+
* Observations that disagree both stand, and picking one would be a judgement no consumer
|
|
255
|
+
* could see being made. A nesting relation rides along because it is what tells a planner
|
|
256
|
+
* the two dimensions are not independent.
|
|
257
|
+
*/
|
|
258
|
+
function dimensionEntry(view, dimension) {
|
|
259
|
+
const nests = dimension?.nestsUnder ? ` · nests under ${dimension.nestsUnder.dimension}` : "";
|
|
260
|
+
const examples = view.exampleValues.length > 0
|
|
261
|
+
? ` · e.g. ${view.exampleValues
|
|
262
|
+
.slice(0, MAX_DIMENSION_EXAMPLES)
|
|
263
|
+
.map((value) => clip(value, MAX_SLOT_VALUE_CHARS))
|
|
264
|
+
.join(", ")}`
|
|
265
|
+
: "";
|
|
266
|
+
return `- ${view.label}: ${view.cardinalities.join(" / ") || "unmeasured"}${nests}${examples}`;
|
|
267
|
+
}
|
|
268
|
+
/** The caveats line: whole items while they fit, then one shortened item, then nothing. */
|
|
269
|
+
function caveatsSection(caveats, budget) {
|
|
270
|
+
const capped = caveats.slice(0, MAX_CAVEATS).map((caveat) => clip(caveat, MAX_CAVEAT_CHARS));
|
|
271
|
+
const render = (texts) => {
|
|
272
|
+
const more = caveats.length > texts.length ? ` (+${caveats.length - texts.length} more)` : "";
|
|
273
|
+
return `Caveats: ${texts.join("; ")}${more}`;
|
|
274
|
+
};
|
|
275
|
+
for (let shown = capped.length; shown > 0; shown--) {
|
|
276
|
+
const line = render(capped.slice(0, shown));
|
|
277
|
+
if (line.length <= budget)
|
|
278
|
+
return line;
|
|
279
|
+
}
|
|
280
|
+
const room = budget - render([""]).length;
|
|
281
|
+
return room >= MIN_PROSE_CHARS ? render([clamp(capped[0], room)]) : undefined;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Fit the body sections into `budget`, in order, each one whole or absent.
|
|
285
|
+
*
|
|
286
|
+
* The order is a priority: the resolved structure is reserved before the agent's prose, so
|
|
287
|
+
* a verbose design note or a run of long caveats can only ever cost the sections below it,
|
|
288
|
+
* and the prose is the only thing a clamp reaches in any profile whose structure fits.
|
|
289
|
+
*/
|
|
290
|
+
function composeBody(result, analysisId, groups, budget, maxChars) {
|
|
291
|
+
const lines = [];
|
|
292
|
+
let elided = false;
|
|
293
|
+
let used = 0;
|
|
294
|
+
const room = () => budget - used - (lines.length > 0 ? 1 : 0);
|
|
295
|
+
const push = (block) => {
|
|
296
|
+
for (const line of block) {
|
|
297
|
+
used += (lines.length > 0 ? 1 : 0) + line.length;
|
|
298
|
+
lines.push(line);
|
|
144
299
|
}
|
|
300
|
+
};
|
|
301
|
+
if (groups.length > 0) {
|
|
302
|
+
const resolved = byId(result.groups);
|
|
303
|
+
const section = groupsSection(groups.map((view) => groupEntry(view, resolved.get(view.id))), room());
|
|
304
|
+
push(section.block);
|
|
305
|
+
if (section.dropped > 0)
|
|
306
|
+
elided = true;
|
|
307
|
+
}
|
|
308
|
+
const dimensions = profileDimensions(result);
|
|
309
|
+
if (dimensions.length > 0) {
|
|
310
|
+
const resolved = new Map((result.dimensions ?? []).map((dimension) => [dimension.label, dimension]));
|
|
311
|
+
const entries = dimensions.map((view) => dimensionEntry(view, resolved.get(view.label)));
|
|
312
|
+
const section = listSection("Dimensions", dimensions.length, entries, room());
|
|
313
|
+
push(section.block);
|
|
314
|
+
if (section.dropped > 0)
|
|
315
|
+
elided = true;
|
|
316
|
+
}
|
|
317
|
+
// The members the agent wrote about individually — the metadata sheet, the README, the
|
|
318
|
+
// outlier — never a sample of the dataset, which the groups above already account for.
|
|
319
|
+
const files = profileFileRecords(result);
|
|
320
|
+
if (files.length > 0) {
|
|
321
|
+
const entries = files
|
|
322
|
+
.slice(0, MAX_FILES)
|
|
323
|
+
.map((file) => `- ${toAnalysisRootPath(analysisId, file.path)} — ${clip(file.description, MAX_FILE_DESCRIPTION_CHARS)}${formatFileFacts(file)}`);
|
|
324
|
+
const section = listSection(groups.length > 0 ? "Notable files" : "Files", files.length, entries, room());
|
|
325
|
+
push(section.block);
|
|
326
|
+
if (section.dropped > 0)
|
|
327
|
+
elided = true;
|
|
145
328
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
329
|
+
const design = present(result.experimentalDesign);
|
|
330
|
+
if (design) {
|
|
331
|
+
const text = clip(design, MAX_DESIGN_CHARS);
|
|
332
|
+
const available = room() - "Design: ".length;
|
|
333
|
+
if (available >= text.length)
|
|
334
|
+
push([`Design: ${text}`]);
|
|
335
|
+
else if (available >= MIN_PROSE_CHARS) {
|
|
336
|
+
push([`Design: ${clamp(text, available)}`]);
|
|
337
|
+
elided = true;
|
|
155
338
|
}
|
|
339
|
+
else
|
|
340
|
+
elided = true;
|
|
341
|
+
}
|
|
342
|
+
const caveats = profileCaveats(result)
|
|
343
|
+
.map((c) => present(c))
|
|
344
|
+
.filter((c) => c !== undefined);
|
|
345
|
+
if (caveats.length > 0) {
|
|
346
|
+
// Three caps, and only one of them is elision: the per-item and share caps are the
|
|
347
|
+
// section's own and already report themselves through `(+N more)`, so the marker is
|
|
348
|
+
// owed only for what the remaining room cost on top of them.
|
|
349
|
+
const share = Math.floor(maxChars * CAVEAT_SHARE);
|
|
350
|
+
const available = Math.min(share, room());
|
|
351
|
+
const line = caveatsSection(caveats, available);
|
|
352
|
+
if (line)
|
|
353
|
+
push([line]);
|
|
354
|
+
if (available < share && line !== caveatsSection(caveats, share))
|
|
355
|
+
elided = true;
|
|
156
356
|
}
|
|
157
|
-
return
|
|
357
|
+
return { lines, elided };
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Project a persisted profile into an orientation blurb of at most `maxChars`
|
|
361
|
+
* characters. The bound is a guarantee, not a target: every section is fitted against the
|
|
362
|
+
* remaining budget before it is rendered, so no profile — however verbose — can blow a
|
|
363
|
+
* caller's budget, and nothing the budget removed leaves a half-line behind.
|
|
364
|
+
*
|
|
365
|
+
* Section order is fixed: identity, census, groups with their slots, dimensions, the
|
|
366
|
+
* members the agent wrote about individually, the experimental design, then the caveats.
|
|
367
|
+
* The header holds its own reserved budget, so the census survives whatever the body cannot
|
|
368
|
+
* hold, and the prose sits last so that it — and not the structure — is what a clamp
|
|
369
|
+
* reaches first. Each list section states its true total, and the trailing marker states
|
|
370
|
+
* that the budget, rather than a per-section cap, is why something is missing.
|
|
371
|
+
*
|
|
372
|
+
* `analysisId` roots each rendered file path (see the module header). It is the
|
|
373
|
+
* id of the analysis the profile belongs to, and it is what the sandbox mounts
|
|
374
|
+
* the tree at.
|
|
375
|
+
*/
|
|
376
|
+
export function buildDataProfileOrientation(result, analysisId, maxChars = DATA_PROFILE_ORIENTATION_MAX_CHARS) {
|
|
377
|
+
const groups = profileGroups(result);
|
|
378
|
+
const census = censusLine(result, groups);
|
|
379
|
+
// The census is reserved out of the budget before the identity line is clipped into
|
|
380
|
+
// what remains, so the one line saying how much of the dataset is accounted for
|
|
381
|
+
// cannot be the line the body's budget drops.
|
|
382
|
+
const identityBudget = Math.max(0, Math.min(MAX_IDENTITY_CHARS, maxChars - (census ? census.length + 1 : 0)));
|
|
383
|
+
const header = [clamp(identityLine(result), identityBudget), census].filter((line) => Boolean(line)).join("\n");
|
|
384
|
+
const headerText = clamp(header, maxChars);
|
|
385
|
+
const bodyBudget = maxChars - headerText.length - 1;
|
|
386
|
+
if (bodyBudget <= 0)
|
|
387
|
+
return headerText;
|
|
388
|
+
const draft = composeBody(result, analysisId, groups, bodyBudget, maxChars);
|
|
389
|
+
if (!draft.elided)
|
|
390
|
+
return draft.lines.length > 0 ? `${headerText}\n${draft.lines.join("\n")}` : headerText;
|
|
391
|
+
// The marker's room is taken out of the budget first and the body re-fitted into what
|
|
392
|
+
// is left, so declaring the elision can never be what overruns the bound.
|
|
393
|
+
if (bodyBudget < ELISION_MARKER.length)
|
|
394
|
+
return headerText;
|
|
395
|
+
const trimmed = composeBody(result, analysisId, groups, bodyBudget - ELISION_MARKER.length - 1, maxChars);
|
|
396
|
+
return `${headerText}\n${[...trimmed.lines, ELISION_MARKER].join("\n")}`;
|
|
158
397
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One reading of a persisted profile, whichever era wrote it.
|
|
3
|
+
*
|
|
4
|
+
* A snapshot carries the fields of its era: rows written under the groups-and-dimensions
|
|
5
|
+
* model carry `groups`/`dimensions`, and rows written before it carry `kinds`/`axes` and
|
|
6
|
+
* a `files` list. There is no version field to consult — optionality is the compatibility
|
|
7
|
+
* mechanism — so a consumer must read both, and reading both in five places is how they
|
|
8
|
+
* drift apart. They read it here instead.
|
|
9
|
+
*
|
|
10
|
+
* The projection is lossy in one direction only: it narrows the newer record to what the
|
|
11
|
+
* older consumers already handled. Nothing it drops is unavailable — the full record is
|
|
12
|
+
* on the row, and consumers that want the resolved structure read it directly.
|
|
13
|
+
*/
|
|
14
|
+
import type { DataProfileDimension, DataProfileFile, DataProfileResult } from "../contracts/data-profile.js";
|
|
15
|
+
/** A group as consumers read it, whether it was authored as a group or as a kind. */
|
|
16
|
+
export interface ProfileGroupView {
|
|
17
|
+
readonly id: string;
|
|
18
|
+
readonly name: string;
|
|
19
|
+
readonly memberRepresents: string;
|
|
20
|
+
readonly description: string;
|
|
21
|
+
/** Members — a data file and its companions count once. */
|
|
22
|
+
readonly count: number;
|
|
23
|
+
/** Display only. Nothing computes membership from it. */
|
|
24
|
+
readonly pattern: string;
|
|
25
|
+
readonly format?: string;
|
|
26
|
+
/** The scanner slots this group carries, on a groups-era row. */
|
|
27
|
+
readonly slotIds?: readonly string[];
|
|
28
|
+
readonly unclassified?: boolean;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* A dimension as consumers read it. `cardinalities` carries every number the
|
|
32
|
+
* observations reported, side by side: there is no canonical count, and a single field
|
|
33
|
+
* would be a silent judgement between disagreeing sources.
|
|
34
|
+
*/
|
|
35
|
+
export interface ProfileDimensionView {
|
|
36
|
+
readonly label: string;
|
|
37
|
+
readonly cardinalities: readonly number[];
|
|
38
|
+
readonly exampleValues: readonly string[];
|
|
39
|
+
readonly description?: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Every number the observations reported, side by side and de-duplicated.
|
|
43
|
+
*
|
|
44
|
+
* There is no canonical cardinality: a metadata sheet describing one count and files
|
|
45
|
+
* existing for another both stand, so a single number would be a silent judgement between
|
|
46
|
+
* disagreeing sources.
|
|
47
|
+
*/
|
|
48
|
+
export declare function dimensionCardinalities(dimension: DataProfileDimension): number[];
|
|
49
|
+
/** The dataset's structure, from whichever field the snapshot carries it in. */
|
|
50
|
+
export declare function profileGroups(result: DataProfileResult): ProfileGroupView[];
|
|
51
|
+
/** What varies across the dataset, from whichever field the snapshot carries it in. */
|
|
52
|
+
export declare function profileDimensions(result: DataProfileResult): ProfileDimensionView[];
|
|
53
|
+
/**
|
|
54
|
+
* The files the profile describes individually.
|
|
55
|
+
*
|
|
56
|
+
* On a groups-era row those are the members the agent annotated, which ride on the group
|
|
57
|
+
* they belong to; on an older row they are the `files` list. Either way this is a handful
|
|
58
|
+
* of notable inputs, never the dataset.
|
|
59
|
+
*/
|
|
60
|
+
export declare function profileFileRecords(result: DataProfileResult): DataProfileFile[];
|
|
61
|
+
/**
|
|
62
|
+
* How many files the dataset holds, as far as the snapshot can establish it.
|
|
63
|
+
*
|
|
64
|
+
* The partition is the authority where present — it is a census, not an estimate. Older
|
|
65
|
+
* rows fall back to coverage, then to summed kind counts, then to `null`: reporting the
|
|
66
|
+
* described-file count would be a dataset size the row cannot support.
|
|
67
|
+
*/
|
|
68
|
+
export declare function profileDatasetFileCount(result: DataProfileResult): number | null;
|
|
69
|
+
/** Dataset-wide findings the agent wrote, from whichever field the snapshot carries them in. */
|
|
70
|
+
export declare function profileCaveats(result: DataProfileResult): string[];
|