@inflexa-ai/harness 0.28.1 → 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.
Files changed (83) hide show
  1. package/dist/app/data-profile-orientation.d.ts +52 -27
  2. package/dist/app/data-profile-orientation.js +331 -92
  3. package/dist/app/data-profile-view.d.ts +70 -0
  4. package/dist/app/data-profile-view.js +123 -0
  5. package/dist/app/synthesize-run.js +1 -1
  6. package/dist/contracts/data-profile.d.ts +276 -25
  7. package/dist/contracts/index.d.ts +3 -1
  8. package/dist/contracts/index.js +1 -0
  9. package/dist/contracts/profile-vocabulary.d.ts +103 -0
  10. package/dist/contracts/profile-vocabulary.js +449 -0
  11. package/dist/contracts/target-dossier.d.ts +21 -21
  12. package/dist/execution/input-signature.d.ts +16 -3
  13. package/dist/execution/input-signature.js +0 -0
  14. package/dist/execution/post-step-pipeline.js +1 -1
  15. package/dist/execution/staged-input.d.ts +4 -4
  16. package/dist/index.d.ts +5 -1
  17. package/dist/index.js +7 -0
  18. package/dist/input-scan/clustering.d.ts +63 -0
  19. package/dist/input-scan/clustering.js +208 -0
  20. package/dist/input-scan/companions.d.ts +30 -0
  21. package/dist/input-scan/companions.js +138 -0
  22. package/dist/input-scan/decoder/container_readout.py +171 -0
  23. package/dist/input-scan/detect-sets.d.ts +24 -0
  24. package/dist/input-scan/detect-sets.js +279 -0
  25. package/dist/input-scan/enrich.d.ts +62 -19
  26. package/dist/input-scan/enrich.js +132 -187
  27. package/dist/input-scan/formats.d.ts +13 -5
  28. package/dist/input-scan/formats.js +13 -5
  29. package/dist/input-scan/index.d.ts +10 -4
  30. package/dist/input-scan/index.js +7 -4
  31. package/dist/input-scan/markers.d.ts +21 -0
  32. package/dist/input-scan/markers.js +89 -0
  33. package/dist/input-scan/menu.d.ts +127 -0
  34. package/dist/input-scan/menu.js +0 -0
  35. package/dist/input-scan/quarantine.d.ts +17 -0
  36. package/dist/input-scan/quarantine.js +69 -0
  37. package/dist/input-scan/readout-budget.d.ts +32 -0
  38. package/dist/input-scan/readout-budget.js +61 -0
  39. package/dist/input-scan/readout.d.ts +33 -0
  40. package/dist/input-scan/readout.js +336 -0
  41. package/dist/input-scan/scan.d.ts +5 -4
  42. package/dist/input-scan/scan.js +5 -5
  43. package/dist/input-scan/set-types.d.ts +209 -0
  44. package/dist/input-scan/set-types.js +15 -0
  45. package/dist/input-scan/templates.d.ts +93 -0
  46. package/dist/input-scan/templates.js +369 -0
  47. package/dist/input-scan/tokens.d.ts +94 -0
  48. package/dist/input-scan/tokens.js +267 -0
  49. package/dist/input-scan/tuning.d.ts +55 -0
  50. package/dist/input-scan/tuning.js +55 -0
  51. package/dist/input-scan/types.d.ts +2 -2
  52. package/dist/prompts/conversation.d.ts +1 -1
  53. package/dist/prompts/conversation.js +8 -6
  54. package/dist/prompts/report-session.d.ts +1 -1
  55. package/dist/prompts/report-session.js +3 -2
  56. package/dist/prompts/sandbox/data-profiler.d.ts +10 -1
  57. package/dist/prompts/sandbox/data-profiler.js +197 -53
  58. package/dist/prompts/sandbox-standards.d.ts +1 -1
  59. package/dist/prompts/sandbox-standards.js +10 -7
  60. package/dist/schemas/data-profile-schemas.d.ts +943 -80
  61. package/dist/schemas/data-profile-schemas.js +300 -115
  62. package/dist/state/data-profile.d.ts +1 -1
  63. package/dist/state/index.d.ts +1 -1
  64. package/dist/state/vector-store.d.ts +13 -0
  65. package/dist/state/vector-store.js +8 -0
  66. package/dist/tasks/data-profile-absorb.d.ts +78 -0
  67. package/dist/tasks/data-profile-absorb.js +285 -0
  68. package/dist/tasks/data-profile-index.d.ts +34 -25
  69. package/dist/tasks/data-profile-index.js +160 -59
  70. package/dist/tasks/data-profile-resolve.d.ts +72 -0
  71. package/dist/tasks/data-profile-resolve.js +639 -0
  72. package/dist/tasks/data-profile.d.ts +16 -5
  73. package/dist/tasks/data-profile.js +270 -131
  74. package/dist/tools/research/inspect-data-profile.d.ts +48 -22
  75. package/dist/tools/research/inspect-data-profile.js +73 -54
  76. package/dist/tools/workspace/scan-inputs.d.ts +4 -4
  77. package/dist/tools/workspace/scan-inputs.js +13 -14
  78. package/dist/tools/workspace/workspace-search.d.ts +11 -2
  79. package/dist/tools/workspace/workspace-search.js +44 -16
  80. package/dist/workflows/target-assessment/phase4-assemble.d.ts +21 -21
  81. package/package.json +5 -3
  82. package/dist/input-scan/coverage.d.ts +0 -22
  83. package/dist/input-scan/coverage.js +0 -85
@@ -1,31 +1,43 @@
1
1
  /**
2
- * Schemas for data profiling.
2
+ * The profiler's authoring contract.
3
3
  *
4
- * Defines the profiler structured output schema used by the data-profile
5
- * task and sandbox agent.
4
+ * What the agent submits is JUDGEMENT, never enumeration: operations on the scan's menu,
5
+ * what each resulting group means, and the dimensions it saw with the evidence it saw
6
+ * them in. Membership, counts, and display patterns are computed at resolution from the
7
+ * scanner's own templates, so no field here carries a count or a path pattern — those are
8
+ * unrepresentable rather than discouraged, because a discouraged field is a field that
9
+ * gets filled.
10
+ *
11
+ * Every array is bounded, and the bound is the enforcement: an over-long submission is a
12
+ * schema error the agent corrects, never a silent truncation. The governing invariant is
13
+ * that nothing the agent authors grows with the number of input files.
14
+ *
15
+ * The category enums are derived from the shipped vocabulary (`contracts/profile-vocabulary`),
16
+ * so the catalogue this validates against and the catalogue the prompt renders are one list.
6
17
  */
7
18
  import { z } from "zod";
8
- // ── Profiler structured output (sandbox agent) ─────────────────────
9
- /** Per-file metadata — includes path so the agent can discover files itself. */
10
- export const ProfilerFileSchema = z.object({
11
- path: z.string().describe("File path relative to analysis root (e.g. data/inputs/{fileId}/counts.txt)"),
12
- description: z.string().describe("What this file contains and its role in the analysis"),
13
- dataType: z
14
- .string()
15
- .describe("Semantic data type: count-matrix, normalized-expression, variants, alignments, " +
16
- "clinical-metadata, clinical-sdtm, clinical-adam, clinical-response, " +
17
- "pharmacokinetic-data, adverse-events, safety-labs, " +
18
- "molecular-structures, annotations, document (paper PDF, README, methods doc)"),
19
- format: z.string().describe("File format (e.g. CSV, TSV, h5ad, MTX, VCF, BAM, SDF, FASTQ)"),
20
- rows: z.number().nullish(),
21
- cols: z.number().nullish(),
22
- tags: z.array(z.string()).optional().describe("Searchable labels for downstream discovery"),
23
- warnings: z.array(z.string()).optional().describe("Quality issues or concerns specific to this file"),
24
- metrics: z
25
- .record(z.string(), z.union([z.string(), z.number(), z.boolean()]))
26
- .optional()
27
- .describe("Identity-establishing facts as flat key-value pairs (e.g. sparsity, delimiter, normalizationState, missingRate)"),
28
- });
19
+ import { DIMENSION_CATEGORY_IDS, DIMENSION_PROBE_IDS, GROUP_CATEGORY_IDS, GROUP_ROLE_IDS } from "../contracts/profile-vocabulary.js";
20
+ // ── Bounds ─────────────────────────────────────────────────────────────────
21
+ /** Operations one submission may carry — the menu's own bound. */
22
+ export const MAX_OPERATIONS = 40;
23
+ /** Groups one split may produce. A slot needing more is an identifier, and identifiers are never split. */
24
+ export const MAX_SPLIT_GROUPS = 12;
25
+ /** Slot values one split group may name. */
26
+ export const MAX_SPLIT_VALUES = 50;
27
+ export const MAX_MERGE_SETS = 12;
28
+ /** Paths one explicit `group` may gather — the leftover aggregate, not the dataset. */
29
+ export const MAX_EXPLICIT_PATHS = 50;
30
+ /** Members of one group the agent may annotate individually. Never required to be exhaustive. */
31
+ export const MAX_MEMBER_ANNOTATIONS = 25;
32
+ /** Dataset-level dimensions. An experimental design with more varying dimensions is vanishingly rare. */
33
+ export const MAX_DIMENSIONS = 12;
34
+ export const MAX_OBSERVATIONS = 6;
35
+ export const MAX_RECONCILIATIONS = 4;
36
+ export const MAX_EXAMPLE_VALUES = 5;
37
+ /** Files a `not-found` probe may name as searched — the probe-searchable set is bounded by design. */
38
+ export const MAX_SEARCHED_FILES = 10;
39
+ export const MAX_CAVEATS = 10;
40
+ // ── Dataset identity ───────────────────────────────────────────────────────
29
41
  /**
30
42
  * Where an extracted subject fact came from. Used so downstream agents can
31
43
  * judge how much to trust a field and so this profiler can flag conflicts.
@@ -33,7 +45,7 @@ export const ProfilerFileSchema = z.object({
33
45
  export const SubjectSourceEnum = z.enum([
34
46
  "metadata", // sample-sheet/header column, internal manifest
35
47
  "document", // paper PDF, README, methods doc
36
- "filename", // file or folder naming convention (GSExxx, "human_", etc.)
48
+ "filename", // file or folder naming convention
37
49
  "user-context", // user-supplied analysis context
38
50
  "inferred", // inferred from data content (e.g. gene symbol patterns)
39
51
  ]);
@@ -45,113 +57,286 @@ export const OrganismSchema = z.object({
45
57
  .enum(["high", "medium", "low"])
46
58
  .describe("high: explicit user-context statement, an organism column, or a paper/README statement. " +
47
59
  "medium: filename convention or accession lookup. " +
48
- "low: inferred from gene IDs or other indirect signals, OR sources disagree (in which case also record the conflict in qualityAssessment.concerns)."),
60
+ "low: inferred from gene IDs or other indirect signals, OR sources disagree (in which case also record the conflict in caveats)."),
49
61
  notes: z.string().optional().describe("Brief note when source is 'inferred' or confidence is 'low' (one sentence)."),
50
62
  });
51
- /** Kinds an output may carry. A tree needing more than this is not being grouped. */
52
- export const MAX_KINDS = 30;
53
- /** Axes an output may carry — an experimental design with more varying dimensions is vanishingly rare. */
54
- export const MAX_AXES = 8;
63
+ // ── Group annotations ──────────────────────────────────────────────────────
64
+ export const MemberAnnotationSchema = z
65
+ .object({
66
+ path: z.string().describe("Path of the member, relative to the analysis root."),
67
+ note: z.string().describe("What is individually notable about THIS member — what you read in it, or why it stands out from its group."),
68
+ })
69
+ .strict();
55
70
  /**
56
- * Individually described files an output may carry. `files` holds NOTABLE SINGLETONS
57
- * — the metadata sheet, the README, the paper, an outlier that fits no kind — not the
58
- * dataset's contents.
59
- */
60
- export const MAX_NOTABLE_FILES = 50;
61
- /**
62
- * A repeating set of files that are the same sort of thing.
63
- *
64
- * A kind is a claim about MEANING, so only the agent can author one: the input scan
65
- * reports shapes (these filenames differ only here) and cannot establish that the
66
- * files are the same sort of thing. Kinds need not correspond one-to-one with observed
67
- * shapes — one kind may span several shapes, and one shape may split into several
68
- * kinds.
71
+ * What a group IS, stated by the agent.
69
72
  *
70
- * A singleton is a kind of `count` 1. There is one concept with a count, not two
71
- * concepts with a threshold between them, so degenerate datasets need no special case.
73
+ * `.strict()` because the fields NOT here are the point: a count, a path pattern, and a
74
+ * member list are all computed at resolution from the operation's own membership, and a
75
+ * submission that carries one is rejected rather than quietly overridden.
72
76
  */
73
- export const KindSchema = z.object({
74
- name: z.string().describe("Short label for this set, e.g. 'per-patient variant calls', 'reference transcriptome'."),
77
+ export const GroupAnnotationSchema = z
78
+ .object({
79
+ name: z.string().describe("Short label for this group, e.g. 'per-subject somatic calls', 'the sample sheet'."),
75
80
  memberRepresents: z
76
81
  .string()
77
- .describe("What ONE member of this set represents — 'one patient's somatic variant calls', 'one sequencing lane of one sample'. " +
78
- "This is your grouping decision stated outright, and it is NOT the description: it cannot be answered by " +
79
- "restating the shape the scan observed."),
80
- description: z.string().describe("What this set contains and the role it plays in the analysis."),
81
- count: z.number().int().describe("How many files are in this set."),
82
- pathPattern: z
82
+ .describe("What ONE member of this group represents — 'one subject's somatic variant calls', 'one sequencing lane of one sample'. " +
83
+ "This is your grouping decision stated outright, and it is NOT the description: it cannot be answered by restating " +
84
+ "an observation the scan already made."),
85
+ description: z.string().describe("What this group contains and the role it plays in the analysis."),
86
+ role: z.enum(GROUP_ROLE_IDS).describe("The group's role in the dataset, from the shipped vocabulary."),
87
+ category: z
88
+ .enum(GROUP_CATEGORY_IDS)
89
+ .describe("The group's category, from the shipped vocabulary. Pick the most specific category that covers ALL members — " +
90
+ "members straddling two categories are usually a split you have not made yet. 'other' is allowed and monitored: " +
91
+ "a wrong specific category is worse than an honest 'other' with a good label."),
92
+ categoryLabel: z.string().optional().describe("Required when category is 'other': what this group actually is."),
93
+ subtype: z.string().optional().describe("Free refinement below the category (e.g. 'spatial', 'imaging MS', 'targeted panel')."),
94
+ categoryReason: z.string().optional().describe("Why you overrode a pre-suggested category, or chose one the anti-overlap notes make arguable."),
95
+ memberAnnotations: z
96
+ .array(MemberAnnotationSchema)
97
+ .max(MAX_MEMBER_ANNOTATIONS)
98
+ .optional()
99
+ .describe("Individually notable members. Never a member list — annotate the ones you read or that stand out, and no others."),
100
+ })
101
+ .strict();
102
+ // ── Menu operations ────────────────────────────────────────────────────────
103
+ const UseOperationSchema = z
104
+ .object({
105
+ op: z.literal("use"),
106
+ setId: z.string().describe("A set id from the menu."),
107
+ group: GroupAnnotationSchema,
108
+ })
109
+ .strict();
110
+ const SplitBySlotSchema = z
111
+ .object({
112
+ kind: z.literal("slot"),
113
+ slotId: z.string().describe("A slot id of the named set. One group is resolved per distinct value of this slot."),
114
+ group: GroupAnnotationSchema.describe("The annotation every resulting group carries; the slot value distinguishes them."),
115
+ })
116
+ .strict();
117
+ const SplitByValuesSchema = z
118
+ .object({
119
+ kind: z.literal("values"),
120
+ slotId: z.string().describe("A slot id of the named set, whose values the mapping below assigns to groups."),
121
+ groups: z
122
+ .array(z
123
+ .object({
124
+ values: z.array(z.string()).min(1).max(MAX_SPLIT_VALUES).describe("Slot values, verbatim as the menu reported them."),
125
+ group: GroupAnnotationSchema,
126
+ })
127
+ .strict())
128
+ .min(2)
129
+ .max(MAX_SPLIT_GROUPS),
130
+ rest: GroupAnnotationSchema.optional().describe("Members whose slot value no mapping named. Omit only when the mapping is exhaustive."),
131
+ })
132
+ .strict();
133
+ const SplitOperationSchema = z
134
+ .object({
135
+ op: z.literal("split"),
136
+ setId: z.string(),
137
+ by: z.discriminatedUnion("kind", [SplitBySlotSchema, SplitByValuesSchema]),
138
+ reason: z
83
139
  .string()
84
- .describe("Glob matching this set's members, relative to the analysis root (e.g. 'data/inputs/vcf/*.vcf.gz'). " +
85
- "Coverage is computed by matching this against the scanned tree, so a pattern that matches nothing reads as an uncovered kind."),
86
- format: z.string().optional().describe("File format shared by the members (VCF, h5ad, CSV, …)."),
87
- axisLabels: z
140
+ .describe("Why these values are different substrates: would a downstream step TYPICALLY consume one value's files differently " +
141
+ "than another's? Identity slots (high-cardinality ids) are never split."),
142
+ })
143
+ .strict();
144
+ const MergeOperationSchema = z
145
+ .object({
146
+ op: z.literal("merge"),
147
+ setIds: z
88
148
  .array(z.string())
89
- .max(MAX_AXES)
90
- .optional()
91
- .describe("Labels of the axes (see `axes`) that vary across this set's members — the design this kind participates in."),
92
- });
149
+ .min(2)
150
+ .max(MAX_MERGE_SETS)
151
+ .refine((ids) => new Set(ids).size === ids.length, { message: "Each set may be named once — a repeated id would claim its members twice." })
152
+ .describe("Distinct set ids from the menu, merged into one group."),
153
+ group: GroupAnnotationSchema,
154
+ reason: z.string().describe("Why these sets are one group despite instantiating different templates."),
155
+ })
156
+ .strict();
157
+ const GroupOperationSchema = z
158
+ .object({
159
+ op: z.literal("group"),
160
+ paths: z.array(z.string()).min(1).max(MAX_EXPLICIT_PATHS).describe("Paths relative to the analysis root, for files no set speaks for."),
161
+ group: GroupAnnotationSchema,
162
+ })
163
+ .strict();
93
164
  /**
94
- * What varies across a kind's members.
165
+ * The four ways an agent expresses a grouping.
95
166
  *
96
- * The scan reports that a filename position varies and which values it takes; what the
97
- * variation IS — a subject, a timepoint, a treatment arm, a chromosome shard — is not
98
- * derivable from the values, so the label is required and agent-supplied. An axis
99
- * evident from a metadata sheet but not from filenames belongs here too.
167
+ * Operations, not freehand patterns: a mistyped value is a rejectable error at submit
168
+ * rather than a silent zero-match, and every run starts from the same menu, so two runs
169
+ * over one tree can only differ where the judgement differs.
100
170
  */
101
- export const AxisSchema = z.object({
102
- label: z.string().describe("What this dimension is: 'patient', 'timepoint', 'treatment arm', 'chromosome shard', 'replicate'."),
103
- cardinality: z.number().int().describe("How many distinct values this dimension takes."),
104
- exampleValues: z.array(z.string()).max(20).optional().describe("A few observed values, for orientation."),
105
- description: z.string().optional().describe("Anything a downstream planner needs about this dimension (ordering, pairing, imbalance)."),
106
- });
107
- export const ProfilerOutputSchema = z.object({
108
- /**
109
- * Notable singletons only. The workspace filesystem is the authoritative file list
110
- * — `list_files`, `grep`, and the vector index all read the live tree — so a record
111
- * per input file would store a stale duplicate of something that cannot go wrong.
112
- */
113
- files: z.array(ProfilerFileSchema).max(MAX_NOTABLE_FILES),
114
- kinds: z
115
- .array(KindSchema)
116
- .max(MAX_KINDS)
117
- .describe("The repeating sets this dataset is made of. Group; do not enumerate members — the cap is a hard bound, not a target."),
118
- axes: z.array(AxisSchema).max(MAX_AXES).optional().describe("What varies across the kinds' members — the experimental design, as far as it is observable."),
119
- analysisSummary: z.string().describe("Narrative overview of the dataset — structure, content, analytical potential, and limitations"),
171
+ export const MenuOperationSchema = z.discriminatedUnion("op", [UseOperationSchema, SplitOperationSchema, MergeOperationSchema, GroupOperationSchema]);
172
+ // ── Dimensions and their observations ──────────────────────────────────────
173
+ const CheckedSchema = z
174
+ .object({
175
+ matched: z.number().int().min(0).describe("Values of this observation found in the other source."),
176
+ of: z.number().int().min(1).describe("Values compared."),
177
+ })
178
+ .strict()
179
+ .describe("A measurement you actually performed. Omit the whole field when you did not perform it — its absence is 'unchecked', " +
180
+ "and there is no way to claim a check that did not happen.");
181
+ const SlotObservationSchema = z
182
+ .object({
183
+ kind: z.literal("slot"),
184
+ setId: z.string(),
185
+ slotId: z.string().describe("The slot this dimension is seen in. Cardinality and values are computed from the scan, not asserted."),
186
+ note: z.string().optional(),
187
+ })
188
+ .strict();
189
+ const ColumnObservationSchema = z
190
+ .object({
191
+ kind: z.literal("column"),
192
+ path: z.string().describe("The file you read, relative to the analysis root."),
193
+ column: z.string().describe("The column name, verbatim."),
194
+ exampleValues: z.array(z.string()).min(1).max(MAX_EXAMPLE_VALUES).describe("Verbatim example values, so the citation is checkable against the file."),
195
+ distinctValues: z.number().int().min(1).optional().describe("Distinct values in the column, when you counted them."),
196
+ checked: CheckedSchema.optional(),
197
+ checkedAgainst: z.string().optional().describe("What you compared against, when `checked` is present."),
198
+ note: z.string().optional(),
199
+ })
200
+ .strict();
201
+ const DocumentObservationSchema = z
202
+ .object({
203
+ kind: z.literal("document"),
204
+ path: z.string().describe("The document or mapping file, relative to the analysis root."),
205
+ citation: z.string().describe("What the document says, close to verbatim."),
206
+ statesCardinality: z.number().int().min(1).optional().describe("The count the document states, when it states one."),
207
+ checked: CheckedSchema.optional(),
208
+ checkedAgainst: z.string().optional().describe("What you compared against, when `checked` is present."),
209
+ note: z.string().optional(),
210
+ })
211
+ .strict();
212
+ export const ObservationSchema = z.discriminatedUnion("kind", [SlotObservationSchema, ColumnObservationSchema, DocumentObservationSchema]);
213
+ /**
214
+ * A dataset-level thing that varies, with where it was seen.
215
+ *
216
+ * At least one observation, always: a dimension without one is a claim, and a claim is
217
+ * what this schema exists to make unwritable. A slot binding is the ONLY way a group
218
+ * links to a dimension — there is no freehand "this group varies by X" field, so a
219
+ * per-subject dimension attached to a group whose template has no such slot cannot be
220
+ * written down.
221
+ *
222
+ * Naming a slot is not the same as promoting a dimension. Technical, single-set slots
223
+ * (shards, callers, lanes, read pairs) stay on the set; this list is reserved for
224
+ * biological or cross-set variation and must read as the design at a glance. A value
225
+ * constant across the dataset is not a dimension — it is an identity field.
226
+ */
227
+ export const DimensionSchema = z
228
+ .object({
229
+ label: z.string().describe("What this dimension is, in the dataset's own words."),
230
+ category: z.enum(DIMENSION_CATEGORY_IDS).describe("The dimension's category, from the shipped vocabulary."),
231
+ categoryLabel: z.string().optional().describe("Required when category is 'other': what actually varies."),
232
+ scope: z
233
+ .enum(["biological", "technical"])
234
+ .optional()
235
+ .describe("Only read when category is 'other', whose scope the catalogue cannot derive. Defaults to technical. Ignored for every other category."),
236
+ description: z.string().optional().describe("What a downstream planner needs about this dimension — ordering, pairing, imbalance."),
237
+ observations: z
238
+ .array(ObservationSchema)
239
+ .min(1)
240
+ .max(MAX_OBSERVATIONS)
241
+ .describe("Where you saw it. At least one; a dimension without evidence is refused."),
242
+ reconciliations: z
243
+ .array(z
244
+ .object({
245
+ note: z.string().describe("What disagrees, and between which observations."),
246
+ delta: z.number().int().optional().describe("The numeric difference, when the disagreement is a count."),
247
+ })
248
+ .strict())
249
+ .max(MAX_RECONCILIATIONS)
250
+ .optional()
251
+ .describe("Disagreeing observations both stand. Record the delta here; do not pick a winner and do not report one canonical cardinality."),
252
+ nestsUnder: z
253
+ .object({
254
+ dimension: z.string().describe("The label of the dimension this one nests under."),
255
+ evidence: z.string().describe("Path structure or a mapping file's columns — what makes the nesting observable."),
256
+ })
257
+ .strict()
258
+ .optional(),
259
+ treatmentReason: z.string().optional().describe("Required when you deviate from the category's default treatment: why this dataset is the exception."),
260
+ })
261
+ .strict();
262
+ // ── Probe outcomes ─────────────────────────────────────────────────────────
263
+ const ProbeFoundSchema = z
264
+ .object({
265
+ probe: z.enum(DIMENSION_PROBE_IDS),
266
+ outcome: z.literal("found"),
267
+ dimension: z.string().describe("The label of the dimension you recorded for it."),
268
+ })
269
+ .strict();
270
+ const ProbeNotFoundSchema = z
271
+ .object({
272
+ probe: z.enum(DIMENSION_PROBE_IDS),
273
+ outcome: z.literal("not-found"),
274
+ searched: z
275
+ .array(z.string())
276
+ .min(1)
277
+ .max(MAX_SEARCHED_FILES)
278
+ .describe("The files you looked in — metadata and documentation members, clinical and sample-annotation tables."),
279
+ reason: z.string().describe("Why it is not there. A pointer to where a near-miss landed instead is the most useful form."),
280
+ })
281
+ .strict();
282
+ const ProbeConstantSchema = z
283
+ .object({
284
+ probe: z.enum(DIMENSION_PROBE_IDS),
285
+ outcome: z.literal("found-but-constant"),
286
+ value: z.string().describe("The single value the attribute takes across the dataset."),
287
+ evidence: z.string().describe("Where you saw it."),
288
+ })
289
+ .strict();
290
+ const ProbeAttestedSchema = z
291
+ .object({
292
+ probe: z.enum(DIMENSION_PROBE_IDS),
293
+ outcome: z.literal("attested"),
294
+ citation: z.string().describe("What the prose says."),
295
+ path: z.string().describe("The document that says it."),
296
+ })
297
+ .strict();
298
+ /**
299
+ * One outcome per probe. "Not found after looking" is a correct, complete answer — that
300
+ * is what removes the completeness pressure that otherwise invents dimensions. An
301
+ * attested find is prose-only evidence and can never justify a split.
302
+ */
303
+ export const ProbeReportSchema = z.discriminatedUnion("outcome", [ProbeFoundSchema, ProbeNotFoundSchema, ProbeConstantSchema, ProbeAttestedSchema]);
304
+ // ── The submission ─────────────────────────────────────────────────────────
305
+ export const ProfileSubmissionSchema = z
306
+ .object({
307
+ operations: z
308
+ .array(MenuOperationSchema)
309
+ .min(1)
310
+ .max(MAX_OPERATIONS)
311
+ .describe("Your grouping, as operations on the menu. Every kept file lands in exactly one group; whatever you leave unclaimed is swept visibly."),
312
+ dimensions: z
313
+ .array(DimensionSchema)
314
+ .max(MAX_DIMENSIONS)
315
+ .optional()
316
+ .describe("What varies across the dataset, each with the evidence you saw it in. An empty list is a correct answer for a simple tree."),
317
+ probes: z.array(ProbeReportSchema).max(DIMENSION_PROBE_IDS.length).optional().describe("One outcome per probe you were asked to look for."),
318
+ analysisSummary: z.string().describe("Narrative overview of the dataset — structure, content, analytical potential, and limitations."),
120
319
  domain: z
121
320
  .string()
122
- .describe("Scientific domain (e.g. transcriptomics, proteomics, genomics, cheminformatics, clinical, imaging, metabolomics, multi-omics)"),
123
- subtype: z.string().optional().describe("Specific subtype within domain (e.g. bulk-rna-seq, single-cell, LC-MS/MS, whole-genome-sequencing)"),
321
+ .describe("Scientific domain (e.g. transcriptomics, proteomics, genomics, cheminformatics, clinical, imaging, metabolomics, multi-omics)."),
322
+ subtype: z.string().optional().describe("Specific subtype within domain (e.g. bulk-rna-seq, single-cell, LC-MS/MS, whole-genome-sequencing)."),
124
323
  organism: OrganismSchema.nullable().describe("Subject organism, identified from any input (metadata files, paper PDFs, READMEs, filenames, accession patterns). " +
125
324
  "Set to null ONLY when no input identifies the organism — never guess. " +
126
- "A null organism is acceptable; a wrong taxon ID is not. " +
127
- "If inputs disagree on organism, pick the most-trusted source, set confidence accordingly, and add the conflict to qualityAssessment.concerns."),
128
- tissue: z
129
- .string()
130
- .nullable()
131
- .optional()
132
- .describe("Subject tissue or anatomical site when applicable (e.g. 'rectal mucosal biopsy', 'liver', 'PBMC', 'whole blood'). null if not applicable or unknown."),
133
- cellType: z
134
- .string()
135
- .nullable()
136
- .optional()
137
- .describe("Subject cell type when applicable (e.g. 'CD4+ T cells', 'iPSC-derived hepatocytes', 'primary hepatocytes'). null if not applicable or unknown."),
138
- condition: z
139
- .string()
140
- .nullable()
141
- .optional()
142
- .describe("Disease, treatment, or perturbation under study (e.g. 'Ulcerative Colitis vs healthy controls', 'cisplatin dose-response', 'CRISPR knockout of TP53'). null if not applicable or unknown."),
325
+ "A null organism is acceptable; a wrong taxon ID is not."),
326
+ tissue: z.string().nullish().describe("Subject tissue or anatomical site when it is CONSTANT across the dataset. A varying one is a dimension."),
327
+ cellType: z.string().nullish().describe("Subject cell type when it is CONSTANT across the dataset. A varying one is a dimension."),
328
+ condition: z.string().nullish().describe("Disease, treatment, or perturbation under study, when constant. A varying one is a dimension."),
143
329
  accessions: z
144
330
  .array(z.string())
145
331
  .optional()
146
- .describe("Public dataset accessions found in any input (filenames, metadata, paper). " +
332
+ .describe("Public dataset accessions found in any input. " +
147
333
  "GEO (GSE/GSM), SRA (SRP/SRR/SRX), BioProject (PRJNA/PRJEB/PRJDB), ArrayExpress (E-MTAB-xxxx), dbGaP (phs), EGA (EGAS/EGAD)."),
148
- experimentalDesign: z.string().optional().describe("Description of experimental design — conditions, groups, comparisons, replicates, pairing"),
149
- qualityAssessment: z
150
- .object({
151
- concerns: z
152
- .array(z.string())
153
- .describe("Dataset-wide quality issues (e.g. batch effects, sample imbalance, high missing rate, conflicting organism declarations across inputs)"),
154
- strengths: z.array(z.string()).describe("Dataset strengths (e.g. deep coverage, balanced groups, low duplication)"),
155
- })
156
- .optional(),
157
- });
334
+ experimentalDesign: z.string().optional().describe("Conditions, groups, comparisons, replicates, pairing — the design as far as it is observable."),
335
+ caveats: z
336
+ .array(z.string())
337
+ .max(MAX_CAVEATS)
338
+ .optional()
339
+ .describe("What a planner must know before designing an analysis, in YOUR words. Companion gaps, incomplete crossings, and " +
340
+ "reconciliation deltas are computed and recorded separately — do not restate them here."),
341
+ })
342
+ .strict();
@@ -13,7 +13,7 @@ import type { Querier } from "./db.js";
13
13
  * `pg` and the whole ledger query surface, to obtain a type. Re-exported here so ledger
14
14
  * call sites keep one import.
15
15
  */
16
- export type { DataProfileAxis, DataProfileCoverage, DataProfileFile, DataProfileInputFile, DataProfileInputSignature, DataProfileKind, DataProfileOrganism, DataProfileQualityAssessment, DataProfileLifecycleStatus, DataProfileResult, DataProfileSubjectSource, } from "../contracts/data-profile.js";
16
+ export type { DataProfileAxis, DataProfileChecked, DataProfileCompanionCompleteness, DataProfileCoverage, DataProfileDimension, DataProfileFile, DataProfileGroup, DataProfileGroupSlot, DataProfileInputSignature, DataProfileKind, DataProfileMemberAnnotation, DataProfileObservation, DataProfileOrganism, DataProfilePartition, DataProfileProbeReport, DataProfileQualityAssessment, DataProfileQuarantine, DataProfileRecipeStep, DataProfileLifecycleStatus, DataProfileResult, DataProfileSubjectSource, } from "../contracts/data-profile.js";
17
17
  export interface DataProfileStatus {
18
18
  status: DataProfileLifecycleStatus;
19
19
  error: string | null;
@@ -21,7 +21,7 @@ export type { ActiveSandboxRow } from "./active-sandboxes.js";
21
21
  export { insertPlan, upsertPlan, loadPlan } from "./plans.js";
22
22
  export type { InsertPlanInput, UpsertPlanInput } from "./plans.js";
23
23
  export { tryStartDataProfile, tryRetryDataProfile, tryRerunDataProfile, completeDataProfile, failDataProfile, expireStaleDataProfile, reconcileOrphanedDataProfile, clearDataProfile, loadDataProfileStatus, loadSeedInputFileIds, recordDataProfileWorkflowId, } from "./data-profile.js";
24
- export type { DataProfileStatus, DataProfileResult, DataProfileInputFile, DataProfileFile, DataProfileQualityAssessment, DataProfileKind, DataProfileAxis, DataProfileInputSignature, DataProfileCoverage, } from "./data-profile.js";
24
+ export type { DataProfileStatus, DataProfileResult, DataProfileChecked, DataProfileCompanionCompleteness, DataProfileDimension, DataProfileGroup, DataProfileGroupSlot, DataProfileFile, DataProfileMemberAnnotation, DataProfileObservation, DataProfilePartition, DataProfileProbeReport, DataProfileQualityAssessment, DataProfileQuarantine, DataProfileRecipeStep, DataProfileKind, DataProfileAxis, DataProfileInputSignature, DataProfileCoverage, } from "./data-profile.js";
25
25
  export { queryRunCountsByAnalyses, queryThreadCountsByAnalyses, queryDataProfileStatusByAnalyses } from "./analyses-metrics.js";
26
26
  export { createAnalysisPurge } from "./purge-analysis.js";
27
27
  export type { AnalysisPurge, AnalysisPurgeDeps, AnalysisPurgeOutcome } from "./purge-analysis.js";
@@ -39,6 +39,18 @@ export interface UpsertInput {
39
39
  readonly metadata?: readonly Record<string, unknown>[];
40
40
  readonly ids?: readonly string[];
41
41
  }
42
+ /**
43
+ * A narrow delete surface: every entry of one index whose metadata `type` is named.
44
+ *
45
+ * `type` is the only dimension a caller may delete along, deliberately. The index holds
46
+ * several independently-owned tiers in one table, and a writer rebuilding its own tier
47
+ * has to be unable to reach another's — a general predicate would make that a review
48
+ * question rather than a type error.
49
+ */
50
+ export interface DeleteByTypeInput {
51
+ readonly indexName: string;
52
+ readonly types: readonly string[];
53
+ }
42
54
  export interface QueryInput {
43
55
  readonly indexName: string;
44
56
  readonly queryVector: readonly number[];
@@ -54,6 +66,7 @@ export interface VectorStore {
54
66
  createIndex(input: CreateIndexInput): ResultAsync<void, DbError>;
55
67
  buildIndex(input: BuildIndexInput): ResultAsync<void, DbError>;
56
68
  upsert(input: UpsertInput): ResultAsync<void, DbError>;
69
+ deleteByType(input: DeleteByTypeInput): ResultAsync<void, DbError>;
57
70
  query(input: QueryInput): ResultAsync<QueryResult[], DbError>;
58
71
  }
59
72
  export declare function createVectorStore(pool: Pool): VectorStore;
@@ -85,6 +85,14 @@ export function createVectorStore(pool) {
85
85
  }
86
86
  });
87
87
  },
88
+ deleteByType({ indexName, types }) {
89
+ assertIdent(indexName);
90
+ if (types.length === 0)
91
+ return okAsync(undefined);
92
+ return tryMutation("vectorStore.deleteByType", async () => {
93
+ await pool.query(`DELETE FROM "${indexName}" WHERE metadata->>'type' = ANY($1::text[])`, [[...types]]);
94
+ });
95
+ },
88
96
  query({ indexName, queryVector, topK, filter }) {
89
97
  assertIdent(indexName);
90
98
  const literal = toVectorLiteral(queryVector);
@@ -0,0 +1,78 @@
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 type { DataProfileResult } from "../contracts/data-profile.js";
32
+ import type { SetMenu } from "../input-scan/menu.js";
33
+ import type { DetectedSets } from "../input-scan/set-types.js";
34
+ import type { MenuOperation } from "../schemas/data-profile-schemas.js";
35
+ import { type ProfileResolution } from "./data-profile-resolve.js";
36
+ /** Delta paths named in the repair briefing before the tail folds into a count. */
37
+ export declare const MAX_DELTA_PATHS = 30;
38
+ export type AbsorbKind = "none" | "full" | "partial" | "stranded";
39
+ export type AbsorbOutcome = {
40
+ readonly kind: "none";
41
+ } | {
42
+ readonly kind: "stranded";
43
+ readonly reason: string;
44
+ } | {
45
+ readonly kind: "full";
46
+ readonly operations: readonly MenuOperation[];
47
+ readonly resolution: ProfileResolution;
48
+ } | {
49
+ readonly kind: "partial";
50
+ readonly operations: readonly MenuOperation[];
51
+ readonly resolution: ProfileResolution;
52
+ /** Kept files the replayed recipe does not account for — what the agent is woken for. */
53
+ readonly delta: readonly string[];
54
+ };
55
+ /**
56
+ * Replay a completed profile's recipe against a fresh scan.
57
+ *
58
+ * Pure. Strandedness is decided both while rebuilding the operations — a template, slot,
59
+ * group id, or dimension binding that no longer resolves — and after resolving them, by
60
+ * checking that every group the recipe named came back. What remains is a fact about the
61
+ * tree having moved: files the recipe does not account for.
62
+ *
63
+ * The residue is replayed too. The recipe carries the paths the last resolution swept, so
64
+ * a file that profile already declined to classify re-sweeps deterministically and only a
65
+ * file NEW to the tree is delta — without that an unchanged tree would re-absorb as
66
+ * partial and wake the agent to re-judge what it already judged.
67
+ */
68
+ export declare function absorbRecipe(prior: DataProfileResult | null | undefined, detected: DetectedSets, menu: SetMenu): AbsorbOutcome;
69
+ /**
70
+ * The repair briefing for a partial absorb.
71
+ *
72
+ * It hands the agent the resolution that already stands, in fresh menu terms, plus the
73
+ * files it does not cover. A drift event that added one directory should cost one
74
+ * operation, not a re-authoring of a grouping nobody disputes.
75
+ */
76
+ export declare function renderAbsorbDelta(outcome: Extract<AbsorbOutcome, {
77
+ kind: "partial";
78
+ }>): string;