@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
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Incremental absorption — replaying a completed profile's recipe against a fresh scan.
|
|
3
|
+
*
|
|
4
|
+
* The recipe is keyed to scanner TEMPLATES, so it survives the scan that produced it: a
|
|
5
|
+
* tree that gained files for ten more subjects instantiates the same templates, and the
|
|
6
|
+
* operations that grouped it still say what its files are. Replaying them re-derives
|
|
7
|
+
* membership, counts, and the partition deterministically — no sandbox, no model — and the
|
|
8
|
+
* profile completes on arithmetic alone.
|
|
9
|
+
*
|
|
10
|
+
* Three outcomes past "there was nothing to replay". A replay claiming every kept file is a
|
|
11
|
+
* FULL absorb and completes the profile. A replay leaving files unclaimed is a PARTIAL
|
|
12
|
+
* absorb: those files are the delta, and the agent sees them alongside the resolution that
|
|
13
|
+
* already stands rather than an empty page. A recipe whose templates no longer resolve is
|
|
14
|
+
* STRANDED and falls back to a full re-profile — loudly, because silently absorbing the
|
|
15
|
+
* half that still resolves would persist a profile describing part of a tree while
|
|
16
|
+
* claiming to describe all of it.
|
|
17
|
+
*
|
|
18
|
+
* Group ids survive a replay because the reconstructed annotation carries the PERSISTED
|
|
19
|
+
* group name, which is what the original resolution derived the id from — including the
|
|
20
|
+
* `name — value` a slot split minted. That is checked rather than trusted: every group id
|
|
21
|
+
* the recipe named must be present in the replayed resolution, so a step that resolved to
|
|
22
|
+
* nothing strands the recipe instead of quietly deleting a group and renumbering its
|
|
23
|
+
* siblings out from under a dimension's `groupIds`.
|
|
24
|
+
*
|
|
25
|
+
* Dimensions are re-resolved, not copied. Their slot observations are persisted as
|
|
26
|
+
* template + slot position, so a replay binds each to whatever the FRESH scan has there
|
|
27
|
+
* and recomputes its cardinality and values — a scan-scoped slot id would name a
|
|
28
|
+
* different slot after the set order changed, and the numbers riding on it would describe
|
|
29
|
+
* some other slot entirely.
|
|
30
|
+
*/
|
|
31
|
+
import { resolveProfileSubmission } from "./data-profile-resolve.js";
|
|
32
|
+
/** Delta paths named in the repair briefing before the tail folds into a count. */
|
|
33
|
+
export const MAX_DELTA_PATHS = 30;
|
|
34
|
+
/** Every kept path of a scan — members and leftovers alike. Companions ride on their member. */
|
|
35
|
+
function keptPaths(detected) {
|
|
36
|
+
const paths = new Set();
|
|
37
|
+
for (const set of detected.sets)
|
|
38
|
+
for (const member of set.members)
|
|
39
|
+
paths.add(member.path);
|
|
40
|
+
for (const member of detected.leftoverMembers)
|
|
41
|
+
paths.add(member.path);
|
|
42
|
+
return paths;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* A persisted group read back as the annotation that produced it.
|
|
46
|
+
*
|
|
47
|
+
* `role` and `category` are widened to `string` on the contract, because a consumer
|
|
48
|
+
* rendering an old row must not fail on a retired id. They are carried through as they
|
|
49
|
+
* stand: a replay records what the profile already said, and re-adjudicating a category
|
|
50
|
+
* against today's catalogue would be authoring, which is the one thing a replay must not do.
|
|
51
|
+
*/
|
|
52
|
+
function annotationOf(group, kept) {
|
|
53
|
+
const annotations = (group.memberAnnotations ?? []).filter((entry) => kept.has(entry.path));
|
|
54
|
+
return {
|
|
55
|
+
name: group.name,
|
|
56
|
+
memberRepresents: group.memberRepresents,
|
|
57
|
+
description: group.description,
|
|
58
|
+
role: group.role,
|
|
59
|
+
category: group.category,
|
|
60
|
+
...(group.categoryLabel ? { categoryLabel: group.categoryLabel } : {}),
|
|
61
|
+
...(group.subtype ? { subtype: group.subtype } : {}),
|
|
62
|
+
...(group.categoryReason ? { categoryReason: group.categoryReason } : {}),
|
|
63
|
+
...(annotations.length > 0 ? { memberAnnotations: annotations.map((entry) => ({ path: entry.path, note: entry.note })) } : {}),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
class StrandedRecipe extends Error {
|
|
67
|
+
}
|
|
68
|
+
function requireSet(ctx, template, op) {
|
|
69
|
+
const set = ctx.setsByTemplate.get(template);
|
|
70
|
+
if (!set)
|
|
71
|
+
throw new StrandedRecipe(`${op}: the template "${template}" is not on the fresh menu`);
|
|
72
|
+
return set;
|
|
73
|
+
}
|
|
74
|
+
function requireGroup(ctx, groupId, op) {
|
|
75
|
+
const group = groupId ? ctx.groupsById.get(groupId) : undefined;
|
|
76
|
+
if (!group)
|
|
77
|
+
throw new StrandedRecipe(`${op}: the recipe names group "${groupId ?? "(none)"}", which the profile does not carry`);
|
|
78
|
+
return group;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Rebuild one recipe step as the operation that produced it.
|
|
82
|
+
*
|
|
83
|
+
* A split replays as a value mapping whatever it was authored as, and that is deliberate:
|
|
84
|
+
* a slot value the mapping does not name is a value nobody has judged, so its members fall
|
|
85
|
+
* out unclaimed and become the delta rather than landing in a group by default.
|
|
86
|
+
* `undefined` means the step addressed only files that are gone.
|
|
87
|
+
*/
|
|
88
|
+
function replayStep(ctx, step) {
|
|
89
|
+
if (step.op === "use") {
|
|
90
|
+
const set = requireSet(ctx, step.templates[0] ?? "", "use");
|
|
91
|
+
return { op: "use", setId: set.id, group: annotationOf(requireGroup(ctx, step.groupIds[0], "use"), ctx.kept) };
|
|
92
|
+
}
|
|
93
|
+
if (step.op === "merge") {
|
|
94
|
+
const sets = step.templates.map((template) => requireSet(ctx, template, "merge"));
|
|
95
|
+
const group = requireGroup(ctx, step.groupIds[0], "merge");
|
|
96
|
+
return { op: "merge", setIds: sets.map((set) => set.id), group: annotationOf(group, ctx.kept), reason: group.reason ?? "" };
|
|
97
|
+
}
|
|
98
|
+
if (step.op === "group") {
|
|
99
|
+
const paths = (step.paths ?? []).filter((path) => ctx.kept.has(path));
|
|
100
|
+
// Every path gone means the group is gone. Absorbing the rest would report a FULL
|
|
101
|
+
// absorb over a profile that silently lost a group, renumbered its siblings, and
|
|
102
|
+
// left every dimension bound to it pointing at nothing.
|
|
103
|
+
if (paths.length === 0)
|
|
104
|
+
throw new StrandedRecipe(`group: every path the recipe grouped as "${step.groupIds[0] ?? "(none)"}" is gone from the tree`);
|
|
105
|
+
return { op: "group", paths, group: annotationOf(requireGroup(ctx, step.groupIds[0], "group"), ctx.kept) };
|
|
106
|
+
}
|
|
107
|
+
const set = requireSet(ctx, step.templates[0] ?? "", "split");
|
|
108
|
+
const slot = step.slotIndex !== undefined ? set.slots[step.slotIndex] : undefined;
|
|
109
|
+
if (!slot)
|
|
110
|
+
throw new StrandedRecipe(`split: the template "${set.pathTemplate}" no longer carries a slot at position ${step.slotIndex ?? "(none)"}`);
|
|
111
|
+
const mapped = (step.valueMapping ?? [])
|
|
112
|
+
.filter((entry) => entry.values.length > 0)
|
|
113
|
+
.map((entry) => ({ values: [...entry.values], group: annotationOf(requireGroup(ctx, entry.groupId, "split"), ctx.kept) }));
|
|
114
|
+
if (mapped.length === 0)
|
|
115
|
+
throw new StrandedRecipe(`split: the recipe for "${set.pathTemplate}" maps no slot value to a group`);
|
|
116
|
+
const reason = ctx.groupsById.get(step.groupIds[0] ?? "")?.reason ?? "";
|
|
117
|
+
return { op: "split", setId: set.id, by: { kind: "values", slotId: slot.id, groups: mapped }, reason };
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Rebind one persisted dimension to the fresh scan.
|
|
121
|
+
*
|
|
122
|
+
* A slot observation is re-addressed through its template binding and nothing else: its
|
|
123
|
+
* cardinality and values are computed, not asserted, so carrying the persisted numbers
|
|
124
|
+
* across a scan would restate an old measurement under a new one's name. A binding that
|
|
125
|
+
* no longer resolves strands the recipe — rebinding to whatever now occupies that
|
|
126
|
+
* position would silently reattach the dimension to a different slot.
|
|
127
|
+
*/
|
|
128
|
+
function replayDimension(ctx, dimension) {
|
|
129
|
+
const observations = dimension.observations.map((observation) => {
|
|
130
|
+
if (observation.kind !== "slot")
|
|
131
|
+
return { ...observation };
|
|
132
|
+
const binding = observation.binding;
|
|
133
|
+
if (!binding) {
|
|
134
|
+
throw new StrandedRecipe(`dimension "${dimension.label}": a slot observation carries no template binding, so it cannot be re-resolved`);
|
|
135
|
+
}
|
|
136
|
+
const set = ctx.setsByTemplate.get(binding.template);
|
|
137
|
+
if (!set)
|
|
138
|
+
throw new StrandedRecipe(`dimension "${dimension.label}": the template "${binding.template}" is not on the fresh menu`);
|
|
139
|
+
const slot = set.slots[binding.slotIndex];
|
|
140
|
+
if (!slot) {
|
|
141
|
+
throw new StrandedRecipe(`dimension "${dimension.label}": the template "${binding.template}" no longer carries a slot at position ${binding.slotIndex}`);
|
|
142
|
+
}
|
|
143
|
+
return { kind: "slot", setId: set.id, slotId: slot.id, ...(observation.note ? { note: observation.note } : {}) };
|
|
144
|
+
});
|
|
145
|
+
return {
|
|
146
|
+
label: dimension.label,
|
|
147
|
+
category: dimension.category,
|
|
148
|
+
...(dimension.categoryLabel ? { categoryLabel: dimension.categoryLabel } : {}),
|
|
149
|
+
...(dimension.scope ? { scope: dimension.scope } : {}),
|
|
150
|
+
...(dimension.description ? { description: dimension.description } : {}),
|
|
151
|
+
observations,
|
|
152
|
+
...(dimension.reconciliations && dimension.reconciliations.length > 0
|
|
153
|
+
? { reconciliations: dimension.reconciliations.map((entry) => ({ ...entry })) }
|
|
154
|
+
: {}),
|
|
155
|
+
...(dimension.nestsUnder ? { nestsUnder: { ...dimension.nestsUnder } } : {}),
|
|
156
|
+
...(dimension.treatmentReason ? { treatmentReason: dimension.treatmentReason } : {}),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
/** Every group id the authored steps named, including a value-mapped split's per-value groups. */
|
|
160
|
+
function namedGroupIds(steps) {
|
|
161
|
+
const ids = new Set();
|
|
162
|
+
for (const step of steps) {
|
|
163
|
+
for (const id of step.groupIds)
|
|
164
|
+
ids.add(id);
|
|
165
|
+
for (const entry of step.valueMapping ?? [])
|
|
166
|
+
ids.add(entry.groupId);
|
|
167
|
+
}
|
|
168
|
+
return [...ids];
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Replay a completed profile's recipe against a fresh scan.
|
|
172
|
+
*
|
|
173
|
+
* Pure. Strandedness is decided both while rebuilding the operations — a template, slot,
|
|
174
|
+
* group id, or dimension binding that no longer resolves — and after resolving them, by
|
|
175
|
+
* checking that every group the recipe named came back. What remains is a fact about the
|
|
176
|
+
* tree having moved: files the recipe does not account for.
|
|
177
|
+
*
|
|
178
|
+
* The residue is replayed too. The recipe carries the paths the last resolution swept, so
|
|
179
|
+
* a file that profile already declined to classify re-sweeps deterministically and only a
|
|
180
|
+
* file NEW to the tree is delta — without that an unchanged tree would re-absorb as
|
|
181
|
+
* partial and wake the agent to re-judge what it already judged.
|
|
182
|
+
*/
|
|
183
|
+
export function absorbRecipe(prior, detected, menu) {
|
|
184
|
+
if (!prior?.recipe?.length || !prior.groups?.length)
|
|
185
|
+
return { kind: "none" };
|
|
186
|
+
const ctx = {
|
|
187
|
+
setsByTemplate: new Map(menu.sets.map((set) => [set.pathTemplate, set])),
|
|
188
|
+
groupsById: new Map(prior.groups.map((group) => [group.id, group])),
|
|
189
|
+
kept: keptPaths(detected),
|
|
190
|
+
};
|
|
191
|
+
const authored = prior.recipe.filter((step) => step.op !== "unclassified");
|
|
192
|
+
const sweep = prior.recipe.find((step) => step.op === "unclassified");
|
|
193
|
+
if (authored.length === 0)
|
|
194
|
+
return { kind: "none" };
|
|
195
|
+
let operations;
|
|
196
|
+
let dimensions;
|
|
197
|
+
try {
|
|
198
|
+
operations = authored.map((step) => replayStep(ctx, step)).filter((operation) => operation !== undefined);
|
|
199
|
+
dimensions = (prior.dimensions ?? []).map((dimension) => replayDimension(ctx, dimension));
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
if (err instanceof StrandedRecipe)
|
|
203
|
+
return { kind: "stranded", reason: err.message };
|
|
204
|
+
throw err;
|
|
205
|
+
}
|
|
206
|
+
if (operations.length !== authored.length) {
|
|
207
|
+
return { kind: "stranded", reason: "a step of the recipe addressed files or templates that are gone" };
|
|
208
|
+
}
|
|
209
|
+
const submission = {
|
|
210
|
+
operations,
|
|
211
|
+
...(dimensions.length > 0 ? { dimensions } : {}),
|
|
212
|
+
analysisSummary: prior.summary,
|
|
213
|
+
domain: prior.domain ?? "",
|
|
214
|
+
organism: prior.organism ?? null,
|
|
215
|
+
};
|
|
216
|
+
// Final round by construction: no agent is awake to repair anything this raises, so a
|
|
217
|
+
// contested file sweeps rather than being awarded to whichever step ran first.
|
|
218
|
+
const resolution = resolveProfileSubmission(submission, detected, menu, { finalRound: true });
|
|
219
|
+
const missing = namedGroupIds(authored).filter((id) => !resolution.groups.some((group) => group.id === id));
|
|
220
|
+
if (missing.length > 0) {
|
|
221
|
+
return { kind: "stranded", reason: `the replay produced no group for ${missing.join(", ")}, so the recipe no longer describes the tree` };
|
|
222
|
+
}
|
|
223
|
+
if (resolution.dimensions.length !== (prior.dimensions?.length ?? 0)) {
|
|
224
|
+
return { kind: "stranded", reason: "a dimension of the profile did not re-resolve against the fresh scan" };
|
|
225
|
+
}
|
|
226
|
+
const swept = new Set(sweep?.paths ?? []);
|
|
227
|
+
const delta = resolution.unclaimed.filter((path) => !swept.has(path));
|
|
228
|
+
if (delta.length === 0)
|
|
229
|
+
return { kind: "full", operations, resolution };
|
|
230
|
+
return { kind: "partial", operations, resolution, delta };
|
|
231
|
+
}
|
|
232
|
+
/** A carried-forward group in full, so resubmitting it verbatim needs no recall. */
|
|
233
|
+
function renderAnnotation(group, indent = "") {
|
|
234
|
+
const lines = [
|
|
235
|
+
`${indent} - name: ${group.name}`,
|
|
236
|
+
`${indent} - role/category: ${group.role} / ${group.category}${group.subtype ? ` (${group.subtype})` : ""}`,
|
|
237
|
+
`${indent} - memberRepresents: ${group.memberRepresents}`,
|
|
238
|
+
`${indent} - description: ${group.description}`,
|
|
239
|
+
];
|
|
240
|
+
for (const annotation of group.memberAnnotations ?? [])
|
|
241
|
+
lines.push(`${indent} - member ${annotation.path}: ${annotation.note}`);
|
|
242
|
+
return lines;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* The repair briefing for a partial absorb.
|
|
246
|
+
*
|
|
247
|
+
* It hands the agent the resolution that already stands, in fresh menu terms, plus the
|
|
248
|
+
* files it does not cover. A drift event that added one directory should cost one
|
|
249
|
+
* operation, not a re-authoring of a grouping nobody disputes.
|
|
250
|
+
*/
|
|
251
|
+
export function renderAbsorbDelta(outcome) {
|
|
252
|
+
const lines = [];
|
|
253
|
+
lines.push("This analysis was profiled before, and the previous profile's operations still resolve");
|
|
254
|
+
lines.push("against the menu above. They are carried forward here, ready to resubmit as they stand:");
|
|
255
|
+
lines.push("");
|
|
256
|
+
for (const operation of outcome.operations) {
|
|
257
|
+
if (operation.op === "use")
|
|
258
|
+
lines.push(`- \`use\` ${operation.setId}`, ...renderAnnotation(operation.group));
|
|
259
|
+
else if (operation.op === "merge")
|
|
260
|
+
lines.push(`- \`merge\` ${operation.setIds.join(" + ")} — ${operation.reason}`, ...renderAnnotation(operation.group));
|
|
261
|
+
else if (operation.op === "group")
|
|
262
|
+
lines.push(`- \`group\` ${operation.paths.join(", ")}`, ...renderAnnotation(operation.group));
|
|
263
|
+
else {
|
|
264
|
+
lines.push(`- \`split\` ${operation.setId} by ${operation.by.slotId} — ${operation.reason}`);
|
|
265
|
+
if (operation.by.kind === "values") {
|
|
266
|
+
for (const mapped of operation.by.groups)
|
|
267
|
+
lines.push(` - values ${mapped.values.join(", ")}`, ...renderAnnotation(mapped.group, " "));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
lines.push("");
|
|
272
|
+
lines.push(`${outcome.delta.length} kept files are NEW to the tree and accounted for by nothing above:`);
|
|
273
|
+
for (const path of outcome.delta.slice(0, MAX_DELTA_PATHS))
|
|
274
|
+
lines.push(`- ${path}`);
|
|
275
|
+
if (outcome.delta.length > MAX_DELTA_PATHS)
|
|
276
|
+
lines.push(`- … ${outcome.delta.length - MAX_DELTA_PATHS} more`);
|
|
277
|
+
lines.push("");
|
|
278
|
+
lines.push("This is a REPAIR ROUND OVER THE DELTA, not a re-profile. Work out what those files are");
|
|
279
|
+
lines.push("and add the operations that claim them. Then resubmit the WHOLE operation list — the");
|
|
280
|
+
lines.push("carried-forward ones unchanged unless the new files change what a group MEANS, plus");
|
|
281
|
+
lines.push("yours. Do not re-author what already resolves, and do not re-read files the carried");
|
|
282
|
+
lines.push("operations already describe. Anything you still leave unclaimed sweeps into `unclassified`,");
|
|
283
|
+
lines.push("as the previous profile's own residue already has.");
|
|
284
|
+
return lines.join("\n");
|
|
285
|
+
}
|
|
@@ -1,28 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The profile's vector index, as a pure projection.
|
|
3
3
|
*
|
|
4
|
-
* The index is a function of the
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* from it, so it can be rebuilt at any time by re-running the scan and reading `kinds`
|
|
8
|
-
* off the existing profile — no model call, no re-profile. Improving the template,
|
|
9
|
-
* adding a tier, or folding in entity attributes later are all re-projections.
|
|
4
|
+
* The index is a function of the persisted profile crossed with the scan it was taken
|
|
5
|
+
* against. It has no durable representation of its own and nothing derives from it, so it
|
|
6
|
+
* can be rebuilt at any time from those two things — no model call, no re-profile.
|
|
10
7
|
*
|
|
11
|
-
*
|
|
12
|
-
* ("variant calls") wants
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
8
|
+
* Three tiers, because they answer three different queries. A query naming a KIND of data
|
|
9
|
+
* ("variant calls") wants the group: its meaning, its count, its template. A query naming
|
|
10
|
+
* what VARIES ("timepoint") wants the dimension and the evidence behind it. A query naming
|
|
11
|
+
* a particular FILE wants that file — but only the members an agent wrote about
|
|
12
|
+
* individually are worth an entry, because a member entry templated from its group's text
|
|
13
|
+
* is a near-duplicate of the group entry, and thousands of them are recall noise rather
|
|
14
|
+
* than recall.
|
|
17
15
|
*
|
|
18
|
-
* `type: "input"` keeps its existing meaning
|
|
19
|
-
*
|
|
20
|
-
*
|
|
16
|
+
* `type: "input"` keeps its existing meaning — one entry addressing one workspace path —
|
|
17
|
+
* so a search filtered to it matches whichever era wrote the profile it reads.
|
|
18
|
+
*
|
|
19
|
+
* Every file-addressed entry stamps its workspace path into `metadata.path`; pattern
|
|
20
|
+
* entries (group, dimension, kind, entity) never do. That presence is the discriminator
|
|
21
|
+
* `workspace_search` documents, so a consumer needs no id parsing to tell them apart.
|
|
21
22
|
*/
|
|
23
|
+
import type { DataProfileResult } from "../contracts/data-profile.js";
|
|
22
24
|
import type { InputScan } from "../input-scan/types.js";
|
|
23
|
-
|
|
24
|
-
/** Entity entries built per analysis. Past this the entity tier is a liability, not a search aid. */
|
|
25
|
+
/** Entity entries built for a legacy profile. Past this the entity tier is a liability, not a search aid. */
|
|
25
26
|
export declare const MAX_ENTITY_ENTRIES = 20000;
|
|
27
|
+
/**
|
|
28
|
+
* Every metadata `type` a profile writes, in either era.
|
|
29
|
+
*
|
|
30
|
+
* The index is a pure PROJECTION, so re-indexing replaces rather than merges: an upsert
|
|
31
|
+
* keyed by entry id leaves the entries of a renamed group, a dropped dimension, or a
|
|
32
|
+
* de-annotated member behind forever, searchable and wrong. Clearing exactly these types
|
|
33
|
+
* clears exactly what a profile wrote — step outputs, summaries, and syntheses carry their
|
|
34
|
+
* own types and are untouched.
|
|
35
|
+
*/
|
|
36
|
+
export declare const PROFILE_INDEX_TYPES: readonly ["input-group", "input-dimension", "input", "input-kind"];
|
|
26
37
|
export interface ProfileIndexEntry {
|
|
27
38
|
readonly id: string;
|
|
28
39
|
readonly text: string;
|
|
@@ -30,16 +41,14 @@ export interface ProfileIndexEntry {
|
|
|
30
41
|
}
|
|
31
42
|
export interface BuildProfileIndexArgs {
|
|
32
43
|
readonly analysisId: string;
|
|
33
|
-
|
|
34
|
-
readonly
|
|
35
|
-
/** The scan the profile was taken against — the source of
|
|
44
|
+
/** The profile as persisted. Whichever era wrote it, the entries are derived from it alone. */
|
|
45
|
+
readonly result: DataProfileResult;
|
|
46
|
+
/** The scan the profile was taken against — the source of a legacy profile's entity value set. */
|
|
36
47
|
readonly scan?: InputScan;
|
|
37
|
-
/** Individually described files, indexed only when there are no kinds to index. */
|
|
38
|
-
readonly files?: readonly DataProfileFile[];
|
|
39
48
|
}
|
|
40
49
|
/**
|
|
41
|
-
* Build every index entry for a completed profile. Deterministic and model-free:
|
|
42
|
-
* text is templated from the
|
|
43
|
-
* indexing costs no LLM tokens whatever the dataset's size.
|
|
50
|
+
* Build every index entry for a completed profile. Deterministic and model-free: each
|
|
51
|
+
* entry's text is templated from what the agent already wrote and what resolution already
|
|
52
|
+
* computed, so indexing costs no LLM tokens whatever the dataset's size.
|
|
44
53
|
*/
|
|
45
54
|
export declare function buildProfileIndexEntries(args: BuildProfileIndexArgs): ProfileIndexEntry[];
|