@inflexa-ai/harness 0.24.0 → 0.26.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/sandbox/__fixtures__/deps.js +6 -0
- package/dist/agents/sandbox/data-profiler.js +5 -4
- package/dist/agents/sandbox/shared.js +11 -0
- package/dist/agents/sandbox/types.d.ts +1 -1
- package/dist/app/data-profile-orientation.js +30 -1
- package/dist/contracts/data-profile.d.ts +166 -0
- package/dist/contracts/index.d.ts +1 -0
- package/dist/execution/input-signature.d.ts +33 -0
- package/dist/execution/input-signature.js +0 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +10 -3
- package/dist/input-scan/coverage.d.ts +22 -0
- package/dist/input-scan/coverage.js +85 -0
- package/dist/input-scan/enrich.d.ts +41 -0
- package/dist/input-scan/enrich.js +227 -0
- package/dist/input-scan/formats.d.ts +41 -0
- package/dist/input-scan/formats.js +279 -0
- package/dist/input-scan/index.d.ts +12 -0
- package/dist/input-scan/index.js +11 -0
- package/dist/input-scan/scan.d.ts +50 -0
- package/dist/input-scan/scan.js +182 -0
- package/dist/input-scan/shapes.d.ts +46 -0
- package/dist/input-scan/shapes.js +0 -0
- package/dist/input-scan/types.d.ts +172 -0
- package/dist/input-scan/types.js +12 -0
- package/dist/lib/page-capture.d.ts +44 -15
- package/dist/lib/page-capture.js +78 -19
- package/dist/loop/run-agent.js +36 -19
- package/dist/memory/card-builders.d.ts +4 -6
- package/dist/memory/card-builders.js +4 -6
- package/dist/memory/conversation-display-replay.d.ts +7 -6
- package/dist/memory/conversation-display-replay.js +7 -6
- package/dist/memory/conversation-display-storage.d.ts +2 -10
- package/dist/memory/conversation-display-storage.js +79 -32
- package/dist/memory/thread-history.d.ts +31 -20
- package/dist/memory/thread-history.js +45 -45
- package/dist/prompts/report-session.d.ts +1 -1
- package/dist/prompts/report-session.js +22 -13
- package/dist/prompts/sandbox/data-profiler.d.ts +1 -1
- package/dist/prompts/sandbox/data-profiler.js +159 -275
- package/dist/provenance/exec-frame.js +9 -3
- package/dist/runtime/boot.d.ts +7 -8
- package/dist/runtime/boot.js +7 -19
- package/dist/sandbox/client.d.ts +7 -0
- package/dist/sandbox/create-sandbox.js +1 -0
- package/dist/sandbox/docker-client.d.ts +1 -0
- package/dist/sandbox/docker-client.js +20 -15
- package/dist/sandbox/k8s-client.d.ts +6 -5
- package/dist/sandbox/k8s-client.js +55 -39
- package/dist/sandbox/reaper.d.ts +24 -13
- package/dist/sandbox/reaper.js +47 -14
- package/dist/sandbox/types.d.ts +6 -5
- package/dist/schemas/data-profile-schemas.d.ts +62 -0
- package/dist/schemas/data-profile-schemas.js +68 -2
- package/dist/state/data-profile.d.ts +7 -88
- package/dist/state/index.d.ts +1 -1
- package/dist/tasks/data-profile-activity.d.ts +8 -0
- package/dist/tasks/data-profile-activity.js +1 -0
- package/dist/tasks/data-profile-index.d.ts +45 -0
- package/dist/tasks/data-profile-index.js +134 -0
- package/dist/tasks/data-profile.d.ts +8 -23
- package/dist/tasks/data-profile.js +150 -117
- package/dist/tools/define-tool.d.ts +19 -8
- package/dist/tools/define-tool.js +28 -12
- package/dist/tools/report-session/examine-page.d.ts +22 -13
- package/dist/tools/report-session/examine-page.js +15 -10
- package/dist/tools/report-session/index.d.ts +1 -1
- package/dist/tools/research/generate-plan.js +3 -8
- package/dist/tools/research/inspect-data-profile.d.ts +30 -4
- package/dist/tools/research/inspect-data-profile.js +75 -20
- package/dist/tools/workspace/scan-inputs.d.ts +38 -0
- package/dist/tools/workspace/scan-inputs.js +61 -0
- package/dist/tools/workspace/workspace-search.d.ts +1 -1
- package/dist/tools/workspace/workspace-search.js +12 -9
- package/dist/workspace/filesystem.d.ts +27 -0
- package/dist/workspace/filesystem.js +27 -0
- package/package.json +1 -1
- package/dist/app/data-profile-policy.d.ts +0 -36
- package/dist/app/data-profile-policy.js +0 -37
- package/dist/memory/content-to-cortex.d.ts +0 -75
- package/dist/memory/content-to-cortex.js +0 -201
- package/dist/memory/conversation-display-backfill.d.ts +0 -23
- package/dist/memory/conversation-display-backfill.js +0 -91
- package/dist/memory/reconstruct-cards.d.ts +0 -27
- package/dist/memory/reconstruct-cards.js +0 -57
|
@@ -5,98 +5,17 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { ResultAsync } from "neverthrow";
|
|
7
7
|
import { type DbError } from "../lib/db-result.js";
|
|
8
|
-
import type {
|
|
8
|
+
import type { DataProfileLifecycleStatus, DataProfileResult } from "../contracts/data-profile.js";
|
|
9
9
|
import type { Querier } from "./db.js";
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* verbatim and never stats a source file or compares signatures itself.
|
|
11
|
+
* The persisted profile's shape lives in `contracts/` — it is read by consumers outside
|
|
12
|
+
* this package (a host route, a UI), and they must not import this module, which pulls in
|
|
13
|
+
* `pg` and the whole ledger query surface, to obtain a type. Re-exported here so ledger
|
|
14
|
+
* call sites keep one import.
|
|
16
15
|
*/
|
|
17
|
-
export
|
|
18
|
-
fileId: string;
|
|
19
|
-
size: number;
|
|
20
|
-
mtimeMs: number;
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* One profiled file's persisted record.
|
|
24
|
-
*
|
|
25
|
-
* `path` + `description` are the original pair every snapshot carries. Everything
|
|
26
|
-
* below them is **optional on read** for the same reason `DataProfileResult.inputFiles`
|
|
27
|
-
* is: a snapshot written before the record was widened carries only the pair, and a
|
|
28
|
-
* reader must render that row rather than reject it. A writer fills in whatever the
|
|
29
|
-
* profiler reported.
|
|
30
|
-
*/
|
|
31
|
-
export interface DataProfileFile {
|
|
32
|
-
path: string;
|
|
33
|
-
description: string;
|
|
34
|
-
/** Semantic data type (count-matrix, variants, clinical-metadata, document, …). */
|
|
35
|
-
dataType?: string;
|
|
36
|
-
/** File format (CSV, h5ad, VCF, BAM, …). */
|
|
37
|
-
format?: string;
|
|
38
|
-
rows?: number | null;
|
|
39
|
-
cols?: number | null;
|
|
40
|
-
/** Searchable labels the profiler attached. */
|
|
41
|
-
tags?: string[];
|
|
42
|
-
/** Quality issues specific to this file. */
|
|
43
|
-
warnings?: string[];
|
|
44
|
-
/** Flat format-specific metrics (sparsity, medianLibrarySize, tiTvRatio, …). */
|
|
45
|
-
metrics?: Record<string, string | number | boolean>;
|
|
46
|
-
}
|
|
47
|
-
/** Dataset-wide quality findings the profiler recorded. */
|
|
48
|
-
export interface DataProfileQualityAssessment {
|
|
49
|
-
concerns: string[];
|
|
50
|
-
strengths: string[];
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* The profile snapshot `completeDataProfile` persists and `loadDataProfileStatus` reads back.
|
|
54
|
-
*
|
|
55
|
-
* This row is the **only** durable home of the profile: the profiler's `runs/data-profile/`
|
|
56
|
-
* scratch tree is wiped when profiling completes, so there is no file to fall back on. It
|
|
57
|
-
* therefore carries the profiler's full finding, not a summary of it — anything dropped here
|
|
58
|
-
* is destroyed, and a downstream agent can only recover it by re-reading the raw input files.
|
|
59
|
-
*
|
|
60
|
-
* Every field past `summary` / `files` / `inputFileIds` / `profiledAt` is **optional on read**:
|
|
61
|
-
* a snapshot written before the record was widened carries only those four, and a reader must
|
|
62
|
-
* render it rather than throw. There is no parse at the read boundary (the jsonb column is cast
|
|
63
|
-
* straight to this type), so optionality *is* the compatibility mechanism — exactly as it
|
|
64
|
-
* already is for `inputFiles`.
|
|
65
|
-
*/
|
|
66
|
-
export interface DataProfileResult {
|
|
67
|
-
summary: string;
|
|
68
|
-
files: DataProfileFile[];
|
|
69
|
-
/** Which files the profile covered — the audit record. */
|
|
70
|
-
inputFileIds: string[];
|
|
71
|
-
/**
|
|
72
|
-
* Whether the same bytes were covered — the drift comparand. Optional on read:
|
|
73
|
-
* a snapshot written before this field existed carries only `inputFileIds`, and
|
|
74
|
-
* a consumer treats the absence as drift (re-profiling repairs it), exactly as
|
|
75
|
-
* it already treats a wholly absent `result`.
|
|
76
|
-
*/
|
|
77
|
-
inputFiles?: DataProfileInputFile[];
|
|
78
|
-
profiledAt: string;
|
|
79
|
-
/** Scientific domain (transcriptomics, cheminformatics, clinical, …). */
|
|
80
|
-
domain?: string;
|
|
81
|
-
/** Specific subtype within the domain (bulk-rna-seq, single-cell, LC-MS/MS, …). */
|
|
82
|
-
subtype?: string;
|
|
83
|
-
/**
|
|
84
|
-
* Subject organism with its provenance and confidence. Explicit `null` is a real,
|
|
85
|
-
* distinct value — the profiler looked and no input identified an organism — as
|
|
86
|
-
* opposed to absent, which means the snapshot predates this field.
|
|
87
|
-
*/
|
|
88
|
-
organism?: Organism | null;
|
|
89
|
-
tissue?: string | null;
|
|
90
|
-
cellType?: string | null;
|
|
91
|
-
condition?: string | null;
|
|
92
|
-
/** Public dataset accessions found in any input (GSE/SRP/PRJNA/E-MTAB/phs/EGAS). */
|
|
93
|
-
accessions?: string[];
|
|
94
|
-
/** Conditions, groups, comparisons, replicates, pairing. */
|
|
95
|
-
experimentalDesign?: string;
|
|
96
|
-
qualityAssessment?: DataProfileQualityAssessment;
|
|
97
|
-
}
|
|
16
|
+
export type { DataProfileAxis, DataProfileCoverage, DataProfileFile, DataProfileInputFile, DataProfileInputSignature, DataProfileKind, DataProfileOrganism, DataProfileQualityAssessment, DataProfileLifecycleStatus, DataProfileResult, DataProfileSubjectSource, } from "../contracts/data-profile.js";
|
|
98
17
|
export interface DataProfileStatus {
|
|
99
|
-
status:
|
|
18
|
+
status: DataProfileLifecycleStatus;
|
|
100
19
|
error: string | null;
|
|
101
20
|
startedAt: string | null;
|
|
102
21
|
completedAt: string | null;
|
package/dist/state/index.d.ts
CHANGED
|
@@ -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 } from "./data-profile.js";
|
|
24
|
+
export type { DataProfileStatus, DataProfileResult, DataProfileInputFile, DataProfileFile, DataProfileQualityAssessment, 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";
|
|
@@ -50,6 +50,14 @@ export type ActivityWrite = (part: unknown) => Promise<void>;
|
|
|
50
50
|
export interface ProfileActivityEmitter {
|
|
51
51
|
/** Reported before the sandbox is created — the longest wait in a profile, and the one that precedes the agent entirely. */
|
|
52
52
|
sandboxInit(): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Reported before the deterministic input scan, which runs between a ready sandbox
|
|
55
|
+
* and the agent's first turn. On a large tree it is the second-longest operation in
|
|
56
|
+
* a profile, and left unreported it would read as `Running data-profiler` for
|
|
57
|
+
* minutes before the agent had begun — the misreport `sandbox-init` exists to
|
|
58
|
+
* prevent.
|
|
59
|
+
*/
|
|
60
|
+
scanning(): Promise<void>;
|
|
53
61
|
/** Reported once the sandbox is ready, covering the gap before the agent's first tool call. */
|
|
54
62
|
agentStarting(): Promise<void>;
|
|
55
63
|
/**
|
|
@@ -47,6 +47,7 @@ export function createProfileActivityEmitter(write, frame, logger) {
|
|
|
47
47
|
};
|
|
48
48
|
return {
|
|
49
49
|
sandboxInit: () => emit("sandbox-init", "Starting sandbox"),
|
|
50
|
+
scanning: () => emit("executing", "Scanning input files"),
|
|
50
51
|
agentStarting: () => emit("executing", "Running data-profiler"),
|
|
51
52
|
forTool: (name, input, resolveDetail) => emit("executing", activityForTool(name, input, resolveDetail)),
|
|
52
53
|
indexing: () => emit("indexing", "Indexing input descriptions for search"),
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The profile's vector index, as a pure projection.
|
|
3
|
+
*
|
|
4
|
+
* The index is a function of the scan crossed with the submitted kinds: for each kind,
|
|
5
|
+
* one entry; for each entity, one entry templated from the kinds it participates in and
|
|
6
|
+
* its own axis value. It has no persisted representation of its own and nothing derives
|
|
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.
|
|
10
|
+
*
|
|
11
|
+
* Two tiers, because they answer different queries. A query naming a KIND of data
|
|
12
|
+
* ("variant calls") wants one result carrying the set's count and path pattern; a
|
|
13
|
+
* per-entity tier would return five arbitrary subjects out of thousands of
|
|
14
|
+
* indistinguishable ones. A query naming an ENTITY ("PT0421") wants that entity. A
|
|
15
|
+
* per-file tier adds nothing: a file is identified by its kind and its entity, and its
|
|
16
|
+
* path is on the filesystem.
|
|
17
|
+
*
|
|
18
|
+
* `type: "input"` keeps its existing meaning for the entity tier, so searches written
|
|
19
|
+
* against it keep matching; `type: "input-kind"` is additive. Renaming `"input"` would
|
|
20
|
+
* have silently broken existing search on every new analysis.
|
|
21
|
+
*/
|
|
22
|
+
import type { InputScan } from "../input-scan/types.js";
|
|
23
|
+
import type { DataProfileAxis, DataProfileFile, DataProfileKind } from "../state/data-profile.js";
|
|
24
|
+
/** Entity entries built per analysis. Past this the entity tier is a liability, not a search aid. */
|
|
25
|
+
export declare const MAX_ENTITY_ENTRIES = 20000;
|
|
26
|
+
export interface ProfileIndexEntry {
|
|
27
|
+
readonly id: string;
|
|
28
|
+
readonly text: string;
|
|
29
|
+
readonly metadata: Record<string, unknown>;
|
|
30
|
+
}
|
|
31
|
+
export interface BuildProfileIndexArgs {
|
|
32
|
+
readonly analysisId: string;
|
|
33
|
+
readonly kinds: readonly DataProfileKind[];
|
|
34
|
+
readonly axes?: readonly DataProfileAxis[];
|
|
35
|
+
/** The scan the profile was taken against — the source of the entity value set. */
|
|
36
|
+
readonly scan?: InputScan;
|
|
37
|
+
/** Individually described files, indexed only when there are no kinds to index. */
|
|
38
|
+
readonly files?: readonly DataProfileFile[];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Build every index entry for a completed profile. Deterministic and model-free: the
|
|
42
|
+
* text is templated from the kind's own description and the entity's axis value, so
|
|
43
|
+
* indexing costs no LLM tokens whatever the dataset's size.
|
|
44
|
+
*/
|
|
45
|
+
export declare function buildProfileIndexEntries(args: BuildProfileIndexArgs): ProfileIndexEntry[];
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The profile's vector index, as a pure projection.
|
|
3
|
+
*
|
|
4
|
+
* The index is a function of the scan crossed with the submitted kinds: for each kind,
|
|
5
|
+
* one entry; for each entity, one entry templated from the kinds it participates in and
|
|
6
|
+
* its own axis value. It has no persisted representation of its own and nothing derives
|
|
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.
|
|
10
|
+
*
|
|
11
|
+
* Two tiers, because they answer different queries. A query naming a KIND of data
|
|
12
|
+
* ("variant calls") wants one result carrying the set's count and path pattern; a
|
|
13
|
+
* per-entity tier would return five arbitrary subjects out of thousands of
|
|
14
|
+
* indistinguishable ones. A query naming an ENTITY ("PT0421") wants that entity. A
|
|
15
|
+
* per-file tier adds nothing: a file is identified by its kind and its entity, and its
|
|
16
|
+
* path is on the filesystem.
|
|
17
|
+
*
|
|
18
|
+
* `type: "input"` keeps its existing meaning for the entity tier, so searches written
|
|
19
|
+
* against it keep matching; `type: "input-kind"` is additive. Renaming `"input"` would
|
|
20
|
+
* have silently broken existing search on every new analysis.
|
|
21
|
+
*/
|
|
22
|
+
/** Entity entries built per analysis. Past this the entity tier is a liability, not a search aid. */
|
|
23
|
+
export const MAX_ENTITY_ENTRIES = 20_000;
|
|
24
|
+
/** Kind names named in one entity's entry text. */
|
|
25
|
+
const MAX_KINDS_IN_ENTITY_TEXT = 6;
|
|
26
|
+
function slug(value) {
|
|
27
|
+
return value
|
|
28
|
+
.toLowerCase()
|
|
29
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
30
|
+
.replace(/^-|-$/g, "")
|
|
31
|
+
.slice(0, 60);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The entity value set: the largest distinct-value set any single variable position
|
|
35
|
+
* carries.
|
|
36
|
+
*
|
|
37
|
+
* Entities are what a dataset is ABOUT, and the position that varies most widely across
|
|
38
|
+
* one shape is what identifies them — 1171 subjects, against three timepoints and two
|
|
39
|
+
* replicates. Taking the widest position rather than the cross product is deliberate:
|
|
40
|
+
* the cross product is the file set, which is the tier this projection exists to avoid.
|
|
41
|
+
*/
|
|
42
|
+
function entityValues(scan) {
|
|
43
|
+
if (!scan)
|
|
44
|
+
return { values: [], distinct: 0 };
|
|
45
|
+
let best = [];
|
|
46
|
+
for (const shape of scan.manifest.shapes) {
|
|
47
|
+
const positions = scan.positionValues.get(shape.id);
|
|
48
|
+
if (!positions)
|
|
49
|
+
continue;
|
|
50
|
+
for (const position of shape.variablePositions) {
|
|
51
|
+
const values = positions[position.index];
|
|
52
|
+
if (values && values.length > best.length)
|
|
53
|
+
best = values;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { values: [...best].sort((a, b) => a.localeCompare(b, "en")), distinct: best.length };
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The axis label for an entity set. The agent labels axes; the scan counts values, so
|
|
60
|
+
* the axis whose cardinality matches the observed set is the one the agent was naming.
|
|
61
|
+
* With no match the entries still index under a neutral label rather than not at all.
|
|
62
|
+
*/
|
|
63
|
+
function entityAxisLabel(axes, distinct) {
|
|
64
|
+
if (!axes || axes.length === 0)
|
|
65
|
+
return "entity";
|
|
66
|
+
const exact = axes.find((axis) => axis.cardinality === distinct);
|
|
67
|
+
if (exact)
|
|
68
|
+
return exact.label;
|
|
69
|
+
const widest = [...axes].sort((a, b) => b.cardinality - a.cardinality)[0];
|
|
70
|
+
return widest?.label ?? "entity";
|
|
71
|
+
}
|
|
72
|
+
function kindEntry(analysisId, kind) {
|
|
73
|
+
const facts = [`${kind.count} files`, kind.format, kind.pathPattern].filter((f) => Boolean(f)).join(", ");
|
|
74
|
+
return {
|
|
75
|
+
id: `/${analysisId}/kind/${slug(kind.name)}`,
|
|
76
|
+
text: `${kind.name} — ${kind.description} One member is ${kind.memberRepresents}. (${facts})`,
|
|
77
|
+
metadata: {
|
|
78
|
+
type: "input-kind",
|
|
79
|
+
kind: kind.name,
|
|
80
|
+
pathPattern: kind.pathPattern,
|
|
81
|
+
count: kind.count,
|
|
82
|
+
...(kind.format ? { format: kind.format } : {}),
|
|
83
|
+
...(kind.axisLabels ? { axisLabels: kind.axisLabels } : {}),
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function entityEntry(analysisId, label, value, kinds) {
|
|
88
|
+
const named = kinds.slice(0, MAX_KINDS_IN_ENTITY_TEXT);
|
|
89
|
+
const carried = named.map((kind) => `${kind.name} (${kind.memberRepresents})`).join("; ");
|
|
90
|
+
return {
|
|
91
|
+
id: `/${analysisId}/entity/${encodeURIComponent(value)}`,
|
|
92
|
+
text: `${label} ${value} — input data for this ${label}: ${carried}.`,
|
|
93
|
+
metadata: {
|
|
94
|
+
type: "input",
|
|
95
|
+
entity: value,
|
|
96
|
+
axis: label,
|
|
97
|
+
kinds: named.map((kind) => kind.name),
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/** A file entry, for the degenerate case where a profile submitted no kinds at all. */
|
|
102
|
+
function fileEntry(analysisId, file) {
|
|
103
|
+
return {
|
|
104
|
+
id: `/${analysisId}/${file.path}`,
|
|
105
|
+
text: `${file.path} — ${file.description}`,
|
|
106
|
+
metadata: {
|
|
107
|
+
type: "input",
|
|
108
|
+
...(file.dataType ? { dataType: file.dataType } : {}),
|
|
109
|
+
...(file.format ? { format: file.format } : {}),
|
|
110
|
+
...(file.tags ? { tags: file.tags } : {}),
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Build every index entry for a completed profile. Deterministic and model-free: the
|
|
116
|
+
* text is templated from the kind's own description and the entity's axis value, so
|
|
117
|
+
* indexing costs no LLM tokens whatever the dataset's size.
|
|
118
|
+
*/
|
|
119
|
+
export function buildProfileIndexEntries(args) {
|
|
120
|
+
const { analysisId, kinds, axes, scan, files } = args;
|
|
121
|
+
if (kinds.length === 0) {
|
|
122
|
+
// No kinds is a degenerate profile, but the notable files it did describe stay
|
|
123
|
+
// discoverable — the same losslessness the per-file fallback description had.
|
|
124
|
+
return (files ?? []).map((file) => fileEntry(analysisId, file));
|
|
125
|
+
}
|
|
126
|
+
const entries = kinds.map((kind) => kindEntry(analysisId, kind));
|
|
127
|
+
const { values, distinct } = entityValues(scan);
|
|
128
|
+
if (values.length > 1 && values.length <= MAX_ENTITY_ENTRIES) {
|
|
129
|
+
const label = entityAxisLabel(axes, distinct);
|
|
130
|
+
for (const value of values)
|
|
131
|
+
entries.push(entityEntry(analysisId, label, value, kinds));
|
|
132
|
+
}
|
|
133
|
+
return entries;
|
|
134
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Data-profile DBOS workflow —
|
|
3
|
-
*
|
|
4
|
-
* cortex_artifacts, and indexes
|
|
5
|
-
* are staged under `data/inputs/` by the embedder before the
|
|
6
|
-
* the body assumes a populated tree and never
|
|
2
|
+
* Data-profile DBOS workflow — scans the already-staged input tree, runs the
|
|
3
|
+
* data-profiler sandbox agent over the resulting manifest, registers each staged
|
|
4
|
+
* file in cortex_artifacts, and indexes the profile's kinds and entities into the
|
|
5
|
+
* pgvector store. Inputs are staged under `data/inputs/` by the embedder before the
|
|
6
|
+
* run (see the data-profile-init spec); the body assumes a populated tree and never
|
|
7
|
+
* downloads.
|
|
7
8
|
*
|
|
8
9
|
* Recoverable: a crashed Cortex pod resumes the workflow from the DBOS step
|
|
9
10
|
* cache. The run authorization is minted at the async edge (`triggerDataProfile`)
|
|
@@ -29,7 +30,7 @@ import type { SandboxClient } from "../sandbox/client.js";
|
|
|
29
30
|
import type { WorkspaceFilesystem } from "../workspace/filesystem.js";
|
|
30
31
|
import { type ResolveWorkspaceRoot } from "../workspace/paths.js";
|
|
31
32
|
import { type ProfilerOutput } from "../schemas/data-profile-schemas.js";
|
|
32
|
-
import { type
|
|
33
|
+
import { type DataProfileCoverage, type DataProfileResult } from "../state/index.js";
|
|
33
34
|
/** The body's construction-time deps — closed over at registration. */
|
|
34
35
|
export interface DataProfileDeps extends EnvironmentStorePaths {
|
|
35
36
|
/** Operational logging seam; omitted falls back to no-op. */
|
|
@@ -92,22 +93,6 @@ export interface DataProfileWorkflowInput {
|
|
|
92
93
|
*/
|
|
93
94
|
readonly ownsMandate?: boolean;
|
|
94
95
|
}
|
|
95
|
-
/**
|
|
96
|
-
* Build the drift-signature comparand a completed profile persists, or `undefined`
|
|
97
|
-
* to omit it. `inputFiles` records whether the profiled files still hold the same
|
|
98
|
-
* bytes; it sits beside the `inputFileIds` audit record (WHICH files) in the stored
|
|
99
|
-
* `DataProfileResult`.
|
|
100
|
-
*
|
|
101
|
-
* A workflow input persisted before `StagedInput.mtimeMs` existed (recovered across
|
|
102
|
-
* the deploy that added it) carries entries with NO `mtimeMs` despite the compile-time
|
|
103
|
-
* type — the same recovered-legacy-input case `ownsMandate` handles. A per-entry
|
|
104
|
-
* `{ …, mtimeMs: undefined }` would JSON.stringify to `{ fileId, size }` and violate
|
|
105
|
-
* `DataProfileInputFile`, so the WHOLE signature is omitted when ANY entry lacks
|
|
106
|
-
* `mtimeMs`: an absent comparand reads back as drift and buys one clean re-profile,
|
|
107
|
-
* exactly as a wholly-absent `result` already does (see `DataProfileResult.inputFiles`).
|
|
108
|
-
* The cast defeats the required-`number` type precisely to observe that legacy gap.
|
|
109
|
-
*/
|
|
110
|
-
export declare function buildDriftSignature(stagedInputs: readonly StagedInput[]): DataProfileInputFile[] | undefined;
|
|
111
96
|
/**
|
|
112
97
|
* Project the profiler's structured output plus the staged manifest into the record
|
|
113
98
|
* persisted on `cortex_analysis_state`.
|
|
@@ -124,7 +109,7 @@ export declare function buildDriftSignature(stagedInputs: readonly StagedInput[]
|
|
|
124
109
|
* indistinguishable from a legacy snapshot that predates the field, which is exactly
|
|
125
110
|
* the reading a consumer must already tolerate.
|
|
126
111
|
*/
|
|
127
|
-
export declare function buildDataProfileResult(profile: ProfilerOutput, stagedInputs: readonly StagedInput[], profiledAt: string): DataProfileResult;
|
|
112
|
+
export declare function buildDataProfileResult(profile: ProfilerOutput, stagedInputs: readonly StagedInput[], profiledAt: string, coverage?: DataProfileCoverage): DataProfileResult;
|
|
128
113
|
/**
|
|
129
114
|
* Register the data-profile workflow with DBOS. Returns the registered
|
|
130
115
|
* callable so `triggerDataProfile` can dispatch via `DBOS.startWorkflow`.
|