@inflexa-ai/harness 0.35.3 → 0.35.5
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/citations/types.d.ts +2 -2
- package/dist/config/environment-stores.d.ts +10 -0
- package/dist/contracts/schemas/chat-parts.d.ts +11 -11
- package/dist/execution/run-canceler.js +20 -4
- package/dist/index.d.ts +3 -1
- package/dist/index.js +5 -0
- package/dist/lib/metrics.d.ts +63 -5
- package/dist/lib/metrics.js +104 -5
- package/dist/lib/otel.d.ts +6 -2
- package/dist/lib/otel.js +6 -2
- package/dist/prompts/planner.js +6 -0
- package/dist/prompts/sandbox-standards.d.ts +1 -1
- package/dist/prompts/sandbox-standards.js +8 -3
- package/dist/sandbox/package-identity.d.ts +208 -0
- package/dist/sandbox/package-identity.js +244 -0
- package/dist/sandbox/types.d.ts +20 -25
- package/dist/schemas/plan-schemas.d.ts +3 -3
- package/dist/schemas/validate-plan.js +32 -15
- package/dist/schemas/workflow-state.js +3 -1
- package/dist/state/data-profile.d.ts +20 -6
- package/dist/state/data-profile.js +16 -8
- package/dist/state/index.d.ts +1 -1
- package/dist/state/schema.d.ts +2 -2
- package/dist/tasks/data-profile.js +26 -12
- package/dist/tools/execute-analysis.js +37 -20
- package/dist/tools/research/inspect-run.d.ts +1 -1
- package/dist/tools/sandbox/link-packages.d.ts +6 -6
- package/dist/tools/sandbox/link-packages.js +45 -14
- package/dist/tools/sandbox/list-available-packages.d.ts +23 -1
- package/dist/tools/sandbox/list-available-packages.js +185 -43
- package/dist/workflows/execute-analysis.d.ts +4 -1
- package/dist/workflows/execute-analysis.js +34 -3
- package/dist/workflows/execute-target-assessment.js +59 -28
- package/dist/workflows/sandbox-step.js +7 -1
- package/package.json +1 -1
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The identity of a package, the query that asks for one, and the resolution
|
|
3
|
+
* between the two.
|
|
4
|
+
*
|
|
5
|
+
* A package name crosses the harness, the embedder, and the provisioner. As a
|
|
6
|
+
* bare `string` each site applies the rule that it believes, and two sites then
|
|
7
|
+
* disagree on whether `decoupleR` and `decoupler` are one package. This module
|
|
8
|
+
* is the one place that knows the rule.
|
|
9
|
+
*
|
|
10
|
+
* Two types carry the two ideas. A {@link PackageQuery} is what a person or an
|
|
11
|
+
* agent asked: a spelling, an optional track, an optional version. A
|
|
12
|
+
* {@link PackageIdentity} is the name that an ecosystem recognizes. The
|
|
13
|
+
* identity is opaque, thus a caller cannot invent one, and every identity comes
|
|
14
|
+
* from {@link pythonIdentity}, {@link rIdentity}, or {@link identityOf}.
|
|
15
|
+
*
|
|
16
|
+
* The PEP 503 fold lives in exactly one function of this file. The provisioner
|
|
17
|
+
* holds the Python twin, `images/sandbox-provisioner/package_identity.py`, and
|
|
18
|
+
* one fixture binds the two.
|
|
19
|
+
*/
|
|
20
|
+
import { type Result } from "neverthrow";
|
|
21
|
+
/** The two ecosystems that the pool holds. */
|
|
22
|
+
export type Track = "python" | "r";
|
|
23
|
+
/**
|
|
24
|
+
* What a person or an agent asked for. `spelling` is the name verbatim, as the
|
|
25
|
+
* caller wrote it — a remedy quotes it, and an R name is case-sensitive at
|
|
26
|
+
* `library()`. `track` names one ecosystem, and its absence searches both.
|
|
27
|
+
* `version` pins one exact version.
|
|
28
|
+
*
|
|
29
|
+
* A query is never a key. Two queries of one fold are two asks, because a
|
|
30
|
+
* caller that wrote `decoupleR` did not ask for `decoupler`.
|
|
31
|
+
*/
|
|
32
|
+
export type PackageQuery = {
|
|
33
|
+
readonly spelling: string;
|
|
34
|
+
readonly track?: Track;
|
|
35
|
+
readonly version?: string;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* The brand of an identity name. It has no runtime value: it exists so that a
|
|
39
|
+
* literal `{ track, name }` fails the typecheck outside this module, and thus
|
|
40
|
+
* every identity comes from a constructor that applied the rule of its track.
|
|
41
|
+
*/
|
|
42
|
+
declare const identityBrand: unique symbol;
|
|
43
|
+
/**
|
|
44
|
+
* The name that one ecosystem recognizes, under the track that holds it.
|
|
45
|
+
*
|
|
46
|
+
* The name of a Python identity is the PEP 503 fold of the spelling, because
|
|
47
|
+
* PEP 503 defines the equivalence of a distribution name. The name of an R
|
|
48
|
+
* identity is the DESCRIPTION spelling, verbatim, because `library()` is
|
|
49
|
+
* case-sensitive. Two identities are equal when their keys are equal.
|
|
50
|
+
*/
|
|
51
|
+
export type PackageIdentity = {
|
|
52
|
+
readonly track: Track;
|
|
53
|
+
readonly name: string & {
|
|
54
|
+
readonly [identityBrand]: true;
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* The Python identity of a spelling. The name folds, because PEP 503 makes
|
|
59
|
+
* `PyYAML`, `pyyaml`, and `py_yaml` one distribution.
|
|
60
|
+
*
|
|
61
|
+
* @param spelling The distribution name, as the caller spells it.
|
|
62
|
+
*/
|
|
63
|
+
export declare function pythonIdentity(spelling: string): PackageIdentity;
|
|
64
|
+
/**
|
|
65
|
+
* The R identity of a name. The name keeps its spelling, because `library()`
|
|
66
|
+
* is case-sensitive and `GO.db` loads under that exact name.
|
|
67
|
+
*
|
|
68
|
+
* @param name The DESCRIPTION `Package` value.
|
|
69
|
+
*/
|
|
70
|
+
export declare function rIdentity(name: string): PackageIdentity;
|
|
71
|
+
/**
|
|
72
|
+
* The identity of a name that an emitter already minted — a graph node, a lock
|
|
73
|
+
* row. The dispatch is safe over such a name, because the fold is idempotent.
|
|
74
|
+
*
|
|
75
|
+
* @param track The ecosystem that holds the name.
|
|
76
|
+
* @param name The name as the emitter wrote it.
|
|
77
|
+
*/
|
|
78
|
+
export declare function identityOf(track: Track, name: string): PackageIdentity;
|
|
79
|
+
/**
|
|
80
|
+
* The key of an identity: `<track>:<name>`. Two identities are equal when
|
|
81
|
+
* their keys are equal, thus a map of identities keys on this string.
|
|
82
|
+
*/
|
|
83
|
+
export declare function identityKey(identity: PackageIdentity): string;
|
|
84
|
+
/**
|
|
85
|
+
* The identity that a key names, or `undefined` when the string is not a key.
|
|
86
|
+
*
|
|
87
|
+
* The FIRST colon splits the two halves. A track name holds no colon, and
|
|
88
|
+
* neither a PEP 503 name nor an R name can hold one, thus `r:GO.db` gives the
|
|
89
|
+
* R identity `GO.db`, dot and all. The name rides through {@link identityOf},
|
|
90
|
+
* and the fold is idempotent, thus a key that {@link identityKey} wrote comes
|
|
91
|
+
* back as the identity that wrote it.
|
|
92
|
+
*
|
|
93
|
+
* The key format is minted here, thus it is read here: a reader that splits
|
|
94
|
+
* the string itself owns a second copy of the format.
|
|
95
|
+
*
|
|
96
|
+
* @param key A `<track>:<name>` string, as {@link identityKey} writes it.
|
|
97
|
+
*/
|
|
98
|
+
export declare function parseIdentityKey(key: string): PackageIdentity | undefined;
|
|
99
|
+
/**
|
|
100
|
+
* The store address of an identity: the PEP 503 fold of its name, for both
|
|
101
|
+
* tracks. A store directory is an address and not an identity, thus two
|
|
102
|
+
* identities can share one address — `r:decoupleR` and `python:decoupler`
|
|
103
|
+
* both address as `decoupler`. The pin marker inside the directory carries
|
|
104
|
+
* the identity.
|
|
105
|
+
*/
|
|
106
|
+
export declare function identityAddress(identity: PackageIdentity): string;
|
|
107
|
+
/**
|
|
108
|
+
* Why an entry is not a query.
|
|
109
|
+
*
|
|
110
|
+
* - `empty` — the entry names no package.
|
|
111
|
+
* - `location` — a path, a URL, or a store directory. A location is an
|
|
112
|
+
* installer detail, and the pool resolves names.
|
|
113
|
+
* - `unknown_prefix` — a `<word>:` prefix that is neither `python:` nor `r:`.
|
|
114
|
+
* Unrefused, such a prefix rides into the pool as part of the name.
|
|
115
|
+
* - `unsupported_specifier` — a specifier that is not `==`. Unrefused, a range
|
|
116
|
+
* such as `numpy>=1.26` becomes a package name.
|
|
117
|
+
*/
|
|
118
|
+
export type ParseQueryError = {
|
|
119
|
+
readonly type: "empty";
|
|
120
|
+
} | {
|
|
121
|
+
readonly type: "location";
|
|
122
|
+
readonly entry: string;
|
|
123
|
+
} | {
|
|
124
|
+
readonly type: "unknown_prefix";
|
|
125
|
+
readonly prefix: string;
|
|
126
|
+
readonly entry: string;
|
|
127
|
+
} | {
|
|
128
|
+
readonly type: "unsupported_specifier";
|
|
129
|
+
readonly specifier: string;
|
|
130
|
+
readonly entry: string;
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* Read one entry of the grammar `[python:|r:]<spelling>[==<version>]`.
|
|
134
|
+
*
|
|
135
|
+
* The entry trims once, and the parts keep what the trim left. Every reader of
|
|
136
|
+
* the grammar in the harness calls this function: a second parser would
|
|
137
|
+
* disagree with this one, and the plan validation that passed would then meet
|
|
138
|
+
* a link pass that refuses.
|
|
139
|
+
*
|
|
140
|
+
* @param entry One package entry, as a plan, a tool call, or a host wrote it.
|
|
141
|
+
*/
|
|
142
|
+
export declare function parseQuery(entry: string): Result<PackageQuery, ParseQueryError>;
|
|
143
|
+
/**
|
|
144
|
+
* Write a query in the grammar. The prefix rides only beside a track, and
|
|
145
|
+
* `==<version>` only beside a version. For every query,
|
|
146
|
+
* `parseQuery(formatQuery(query))` gives that query again.
|
|
147
|
+
*/
|
|
148
|
+
export declare function formatQuery(query: PackageQuery): string;
|
|
149
|
+
/**
|
|
150
|
+
* What a pool answers about the identities that it holds. Two reads are
|
|
151
|
+
* enough for the ladder, thus a caller builds this over a graph, a lock, or a
|
|
152
|
+
* census without exposing its own shape.
|
|
153
|
+
*/
|
|
154
|
+
export type PoolIndex = {
|
|
155
|
+
/** Whether the pool holds this exact identity. */
|
|
156
|
+
has: (identity: PackageIdentity) => boolean;
|
|
157
|
+
/**
|
|
158
|
+
* Each R identity of the pool whose address is this fold. The suggestion
|
|
159
|
+
* of an unknown query comes from here: a person who wrote `seurat` meant
|
|
160
|
+
* the R package `Seurat`.
|
|
161
|
+
*/
|
|
162
|
+
rIdentitiesFoldingTo: (fold: string) => readonly PackageIdentity[];
|
|
163
|
+
};
|
|
164
|
+
/**
|
|
165
|
+
* What a query resolves to.
|
|
166
|
+
*
|
|
167
|
+
* - `resolved` — one identity, thus the caller reads the pool under it.
|
|
168
|
+
* - `ambiguous` — one spelling that both tracks hold. The caller refuses, and
|
|
169
|
+
* the remedy is a prefixed query.
|
|
170
|
+
* - `unknown` — the pool holds no identity of this query. `suggestion` names
|
|
171
|
+
* the R identity that the caller probably meant.
|
|
172
|
+
*/
|
|
173
|
+
export type QueryResolution = {
|
|
174
|
+
readonly kind: "resolved";
|
|
175
|
+
readonly identity: PackageIdentity;
|
|
176
|
+
} | {
|
|
177
|
+
readonly kind: "ambiguous";
|
|
178
|
+
readonly python: PackageIdentity;
|
|
179
|
+
readonly r: PackageIdentity;
|
|
180
|
+
} | {
|
|
181
|
+
readonly kind: "unknown";
|
|
182
|
+
readonly suggestion?: PackageIdentity;
|
|
183
|
+
};
|
|
184
|
+
/**
|
|
185
|
+
* Resolve one query against a pool.
|
|
186
|
+
*
|
|
187
|
+
* The version takes no part, because the caller holds the versions: the graph
|
|
188
|
+
* orders them, and this function answers which package the query names.
|
|
189
|
+
*
|
|
190
|
+
* The ladder, in order:
|
|
191
|
+
*
|
|
192
|
+
* 1. The query names a track: the identity of that track, when the pool holds
|
|
193
|
+
* it. Otherwise unknown, with the R suggestion when the track is `r`.
|
|
194
|
+
* 2. The pool holds the R identity of the spelling and the Python identity of
|
|
195
|
+
* its fold: the R identity when the spelling differs from its fold, because
|
|
196
|
+
* an uppercase letter or a dot is evidence of an R spelling. Otherwise
|
|
197
|
+
* ambiguous.
|
|
198
|
+
* 3. The pool holds the R identity: the R identity.
|
|
199
|
+
* 4. The pool holds the Python identity: the Python identity.
|
|
200
|
+
* 5. Exactly one R identity of the pool folds to the fold of the spelling:
|
|
201
|
+
* unknown, with that identity as the suggestion.
|
|
202
|
+
* 6. Otherwise: unknown.
|
|
203
|
+
*
|
|
204
|
+
* A silent Python-first pick is a fault: it sends `decoupleR` to the Python
|
|
205
|
+
* `decoupler`, and the R package never links.
|
|
206
|
+
*/
|
|
207
|
+
export declare function resolveQuery(query: PackageQuery, pool: PoolIndex): QueryResolution;
|
|
208
|
+
export {};
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The identity of a package, the query that asks for one, and the resolution
|
|
3
|
+
* between the two.
|
|
4
|
+
*
|
|
5
|
+
* A package name crosses the harness, the embedder, and the provisioner. As a
|
|
6
|
+
* bare `string` each site applies the rule that it believes, and two sites then
|
|
7
|
+
* disagree on whether `decoupleR` and `decoupler` are one package. This module
|
|
8
|
+
* is the one place that knows the rule.
|
|
9
|
+
*
|
|
10
|
+
* Two types carry the two ideas. A {@link PackageQuery} is what a person or an
|
|
11
|
+
* agent asked: a spelling, an optional track, an optional version. A
|
|
12
|
+
* {@link PackageIdentity} is the name that an ecosystem recognizes. The
|
|
13
|
+
* identity is opaque, thus a caller cannot invent one, and every identity comes
|
|
14
|
+
* from {@link pythonIdentity}, {@link rIdentity}, or {@link identityOf}.
|
|
15
|
+
*
|
|
16
|
+
* The PEP 503 fold lives in exactly one function of this file. The provisioner
|
|
17
|
+
* holds the Python twin, `images/sandbox-provisioner/package_identity.py`, and
|
|
18
|
+
* one fixture binds the two.
|
|
19
|
+
*/
|
|
20
|
+
import { err, ok } from "neverthrow";
|
|
21
|
+
/**
|
|
22
|
+
* The PEP 503 fold: each run of `-`, `_`, and `.` becomes one `-`, and the
|
|
23
|
+
* case lowers. This is the ONE TypeScript home of the rule. The fold is
|
|
24
|
+
* idempotent, thus a fold of a folded name gives that name again.
|
|
25
|
+
*/
|
|
26
|
+
function fold(name) {
|
|
27
|
+
return name.replace(/[-_.]+/g, "-").toLowerCase();
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Brand a name that a constructor of this module already ruled on.
|
|
31
|
+
*
|
|
32
|
+
* The cast is the whole point of the brand: the brand has no runtime value,
|
|
33
|
+
* thus only a cast can produce one, and this function is the one place that
|
|
34
|
+
* casts. A caller outside the module reaches a name through a constructor.
|
|
35
|
+
*/
|
|
36
|
+
function brand(name) {
|
|
37
|
+
// The brand is a compile-time marker with no runtime value, thus a cast is
|
|
38
|
+
// the only way to mint one, and this function is the only site that casts.
|
|
39
|
+
return name;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The Python identity of a spelling. The name folds, because PEP 503 makes
|
|
43
|
+
* `PyYAML`, `pyyaml`, and `py_yaml` one distribution.
|
|
44
|
+
*
|
|
45
|
+
* @param spelling The distribution name, as the caller spells it.
|
|
46
|
+
*/
|
|
47
|
+
export function pythonIdentity(spelling) {
|
|
48
|
+
return { track: "python", name: brand(fold(spelling)) };
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The R identity of a name. The name keeps its spelling, because `library()`
|
|
52
|
+
* is case-sensitive and `GO.db` loads under that exact name.
|
|
53
|
+
*
|
|
54
|
+
* @param name The DESCRIPTION `Package` value.
|
|
55
|
+
*/
|
|
56
|
+
export function rIdentity(name) {
|
|
57
|
+
return { track: "r", name: brand(name) };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The identity of a name that an emitter already minted — a graph node, a lock
|
|
61
|
+
* row. The dispatch is safe over such a name, because the fold is idempotent.
|
|
62
|
+
*
|
|
63
|
+
* @param track The ecosystem that holds the name.
|
|
64
|
+
* @param name The name as the emitter wrote it.
|
|
65
|
+
*/
|
|
66
|
+
export function identityOf(track, name) {
|
|
67
|
+
return track === "python" ? pythonIdentity(name) : rIdentity(name);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The key of an identity: `<track>:<name>`. Two identities are equal when
|
|
71
|
+
* their keys are equal, thus a map of identities keys on this string.
|
|
72
|
+
*/
|
|
73
|
+
export function identityKey(identity) {
|
|
74
|
+
return `${identity.track}:${identity.name}`;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The identity that a key names, or `undefined` when the string is not a key.
|
|
78
|
+
*
|
|
79
|
+
* The FIRST colon splits the two halves. A track name holds no colon, and
|
|
80
|
+
* neither a PEP 503 name nor an R name can hold one, thus `r:GO.db` gives the
|
|
81
|
+
* R identity `GO.db`, dot and all. The name rides through {@link identityOf},
|
|
82
|
+
* and the fold is idempotent, thus a key that {@link identityKey} wrote comes
|
|
83
|
+
* back as the identity that wrote it.
|
|
84
|
+
*
|
|
85
|
+
* The key format is minted here, thus it is read here: a reader that splits
|
|
86
|
+
* the string itself owns a second copy of the format.
|
|
87
|
+
*
|
|
88
|
+
* @param key A `<track>:<name>` string, as {@link identityKey} writes it.
|
|
89
|
+
*/
|
|
90
|
+
export function parseIdentityKey(key) {
|
|
91
|
+
const at = key.indexOf(":");
|
|
92
|
+
if (at < 0)
|
|
93
|
+
return undefined;
|
|
94
|
+
const track = key.slice(0, at);
|
|
95
|
+
const name = key.slice(at + 1);
|
|
96
|
+
if (name === "")
|
|
97
|
+
return undefined;
|
|
98
|
+
if (track !== "python" && track !== "r")
|
|
99
|
+
return undefined;
|
|
100
|
+
return identityOf(track, name);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The store address of an identity: the PEP 503 fold of its name, for both
|
|
104
|
+
* tracks. A store directory is an address and not an identity, thus two
|
|
105
|
+
* identities can share one address — `r:decoupleR` and `python:decoupler`
|
|
106
|
+
* both address as `decoupler`. The pin marker inside the directory carries
|
|
107
|
+
* the identity.
|
|
108
|
+
*/
|
|
109
|
+
export function identityAddress(identity) {
|
|
110
|
+
return fold(identity.name);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* A path, a URL, or a store directory. The entry is trimmed already, thus a
|
|
114
|
+
* leading space cannot defeat the guard.
|
|
115
|
+
*/
|
|
116
|
+
const LOCATION = /^[.~]|[/\\]|:\/\//;
|
|
117
|
+
/** The `<word>:` prefix of an entry. The group holds the word before the colon. */
|
|
118
|
+
const PREFIX = /^([A-Za-z][A-Za-z0-9_.-]*):/;
|
|
119
|
+
/** A run of specifier characters. Only the exact run `==` names a version. */
|
|
120
|
+
const SPECIFIER = /[<>!~=]+/;
|
|
121
|
+
/**
|
|
122
|
+
* A run of characters that an exact version cannot hold: a specifier
|
|
123
|
+
* character, a comma, or whitespace. `SPECIFIER` reads the FIRST run only,
|
|
124
|
+
* thus `numpy==1.26,<2` gives the version `1.26,<2` without this guard, and
|
|
125
|
+
* the compound range then rides into the pool as a version string.
|
|
126
|
+
*/
|
|
127
|
+
const VERSION_INTRUDER = /[<>!~=,\s]+/;
|
|
128
|
+
/**
|
|
129
|
+
* Read one entry of the grammar `[python:|r:]<spelling>[==<version>]`.
|
|
130
|
+
*
|
|
131
|
+
* The entry trims once, and the parts keep what the trim left. Every reader of
|
|
132
|
+
* the grammar in the harness calls this function: a second parser would
|
|
133
|
+
* disagree with this one, and the plan validation that passed would then meet
|
|
134
|
+
* a link pass that refuses.
|
|
135
|
+
*
|
|
136
|
+
* @param entry One package entry, as a plan, a tool call, or a host wrote it.
|
|
137
|
+
*/
|
|
138
|
+
export function parseQuery(entry) {
|
|
139
|
+
const trimmed = entry.trim();
|
|
140
|
+
if (trimmed === "")
|
|
141
|
+
return err({ type: "empty" });
|
|
142
|
+
if (LOCATION.test(trimmed))
|
|
143
|
+
return err({ type: "location", entry: trimmed });
|
|
144
|
+
const prefix = PREFIX.exec(trimmed);
|
|
145
|
+
const word = prefix?.[1];
|
|
146
|
+
if (word !== undefined && word !== "python" && word !== "r") {
|
|
147
|
+
return err({ type: "unknown_prefix", prefix: word, entry: trimmed });
|
|
148
|
+
}
|
|
149
|
+
const track = word === "python" ? "python" : word === "r" ? "r" : undefined;
|
|
150
|
+
const rest = track === undefined ? trimmed : trimmed.slice(track.length + 1);
|
|
151
|
+
if (rest === "")
|
|
152
|
+
return err({ type: "empty" });
|
|
153
|
+
const specifier = SPECIFIER.exec(rest);
|
|
154
|
+
if (specifier === null)
|
|
155
|
+
return ok({ spelling: rest, ...(track === undefined ? {} : { track }) });
|
|
156
|
+
if (specifier[0] !== "==") {
|
|
157
|
+
return err({ type: "unsupported_specifier", specifier: specifier[0], entry: trimmed });
|
|
158
|
+
}
|
|
159
|
+
// Both halves trim: `numpy == 1.26.4` is one ask, and an untrimmed half
|
|
160
|
+
// makes the spelling `numpy ` and the version ` 1.26.4`, neither of which
|
|
161
|
+
// any index holds.
|
|
162
|
+
const spelling = rest.slice(0, specifier.index).trim();
|
|
163
|
+
if (spelling === "")
|
|
164
|
+
return err({ type: "empty" });
|
|
165
|
+
// `name==` names no version, thus the query pins none and the pool answers
|
|
166
|
+
// the newest. The round-trip law holds, because `formatQuery` writes the
|
|
167
|
+
// specifier only beside a version.
|
|
168
|
+
const version = rest.slice(specifier.index + 2).trim();
|
|
169
|
+
const intruder = VERSION_INTRUDER.exec(version);
|
|
170
|
+
if (intruder !== null) {
|
|
171
|
+
return err({ type: "unsupported_specifier", specifier: intruder[0], entry: trimmed });
|
|
172
|
+
}
|
|
173
|
+
return ok({
|
|
174
|
+
spelling,
|
|
175
|
+
...(track === undefined ? {} : { track }),
|
|
176
|
+
...(version === "" ? {} : { version }),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Write a query in the grammar. The prefix rides only beside a track, and
|
|
181
|
+
* `==<version>` only beside a version. For every query,
|
|
182
|
+
* `parseQuery(formatQuery(query))` gives that query again.
|
|
183
|
+
*/
|
|
184
|
+
export function formatQuery(query) {
|
|
185
|
+
const prefix = query.track === undefined ? "" : `${query.track}:`;
|
|
186
|
+
const version = query.version === undefined ? "" : `==${query.version}`;
|
|
187
|
+
return `${prefix}${query.spelling}${version}`;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* The sole R identity of the pool that folds to this fold, or none. Two such
|
|
191
|
+
* identities give no suggestion, because a guess between them is a coin flip.
|
|
192
|
+
*/
|
|
193
|
+
function soleRSuggestion(pool, spellingFold) {
|
|
194
|
+
const candidates = pool.rIdentitiesFoldingTo(spellingFold);
|
|
195
|
+
return candidates.length === 1 ? candidates[0] : undefined;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Resolve one query against a pool.
|
|
199
|
+
*
|
|
200
|
+
* The version takes no part, because the caller holds the versions: the graph
|
|
201
|
+
* orders them, and this function answers which package the query names.
|
|
202
|
+
*
|
|
203
|
+
* The ladder, in order:
|
|
204
|
+
*
|
|
205
|
+
* 1. The query names a track: the identity of that track, when the pool holds
|
|
206
|
+
* it. Otherwise unknown, with the R suggestion when the track is `r`.
|
|
207
|
+
* 2. The pool holds the R identity of the spelling and the Python identity of
|
|
208
|
+
* its fold: the R identity when the spelling differs from its fold, because
|
|
209
|
+
* an uppercase letter or a dot is evidence of an R spelling. Otherwise
|
|
210
|
+
* ambiguous.
|
|
211
|
+
* 3. The pool holds the R identity: the R identity.
|
|
212
|
+
* 4. The pool holds the Python identity: the Python identity.
|
|
213
|
+
* 5. Exactly one R identity of the pool folds to the fold of the spelling:
|
|
214
|
+
* unknown, with that identity as the suggestion.
|
|
215
|
+
* 6. Otherwise: unknown.
|
|
216
|
+
*
|
|
217
|
+
* A silent Python-first pick is a fault: it sends `decoupleR` to the Python
|
|
218
|
+
* `decoupler`, and the R package never links.
|
|
219
|
+
*/
|
|
220
|
+
export function resolveQuery(query, pool) {
|
|
221
|
+
const spellingFold = fold(query.spelling);
|
|
222
|
+
if (query.track !== undefined) {
|
|
223
|
+
const identity = identityOf(query.track, query.spelling);
|
|
224
|
+
if (pool.has(identity))
|
|
225
|
+
return { kind: "resolved", identity };
|
|
226
|
+
if (query.track !== "r")
|
|
227
|
+
return { kind: "unknown" };
|
|
228
|
+
const suggestion = soleRSuggestion(pool, spellingFold);
|
|
229
|
+
return suggestion === undefined ? { kind: "unknown" } : { kind: "unknown", suggestion };
|
|
230
|
+
}
|
|
231
|
+
const r = rIdentity(query.spelling);
|
|
232
|
+
const python = pythonIdentity(query.spelling);
|
|
233
|
+
const holdsR = pool.has(r);
|
|
234
|
+
const holdsPython = pool.has(python);
|
|
235
|
+
if (holdsR && holdsPython) {
|
|
236
|
+
return query.spelling === spellingFold ? { kind: "ambiguous", python, r } : { kind: "resolved", identity: r };
|
|
237
|
+
}
|
|
238
|
+
if (holdsR)
|
|
239
|
+
return { kind: "resolved", identity: r };
|
|
240
|
+
if (holdsPython)
|
|
241
|
+
return { kind: "resolved", identity: python };
|
|
242
|
+
const suggestion = soleRSuggestion(pool, spellingFold);
|
|
243
|
+
return suggestion === undefined ? { kind: "unknown" } : { kind: "unknown", suggestion };
|
|
244
|
+
}
|
package/dist/sandbox/types.d.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { z } from "zod";
|
|
12
12
|
import type { ResourceSpec } from "../config/resource-limits.js";
|
|
13
|
+
import type { PackageQuery } from "./package-identity.js";
|
|
13
14
|
export declare const SandboxBackend: z.ZodEnum<{
|
|
14
15
|
docker: "docker";
|
|
15
16
|
k8s: "k8s";
|
|
@@ -82,22 +83,12 @@ export type FarmSource = {
|
|
|
82
83
|
readonly resolve: ResolveAnalysisFarm;
|
|
83
84
|
};
|
|
84
85
|
/**
|
|
85
|
-
* One
|
|
86
|
-
* version. `ecosystem` qualifies a name that both tracks can hold. Without
|
|
87
|
-
* it, a name that both tracks hold comes back as a `collision` outcome.
|
|
88
|
-
*/
|
|
89
|
-
export interface PackageRequest {
|
|
90
|
-
readonly name: string;
|
|
91
|
-
readonly version?: string;
|
|
92
|
-
readonly ecosystem?: "python" | "r";
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* One outcome per request, index-aligned with the request array.
|
|
86
|
+
* One outcome per query, index-aligned with the query array.
|
|
96
87
|
*
|
|
97
|
-
* `
|
|
98
|
-
* caller quotes it into a remedy (`inflexa store add <
|
|
99
|
-
* is case-sensitive with dots, thus a normalized echo would name a
|
|
100
|
-
* that no repository holds.
|
|
88
|
+
* `spelling` echoes the spelling of its query verbatim, never a normalized
|
|
89
|
+
* form. A caller quotes it into a remedy (`inflexa store add <spelling>`), and
|
|
90
|
+
* an R name is case-sensitive with dots, thus a normalized echo would name a
|
|
91
|
+
* package that no repository holds.
|
|
101
92
|
*
|
|
102
93
|
* - `linked` — the pool held the package, and this call linked it.
|
|
103
94
|
* - `present` — the farm linked it already.
|
|
@@ -106,12 +97,13 @@ export interface PackageRequest {
|
|
|
106
97
|
* the realization gives one, classifies the miss in host terms — in
|
|
107
98
|
* flight, failed with a recorded reason, or never requested — and the
|
|
108
99
|
* launch refusal renders it beside the name.
|
|
109
|
-
* - `collision` — the
|
|
110
|
-
* versions of one distribution, or one
|
|
111
|
-
* outcome is terminal for the
|
|
100
|
+
* - `collision` — the query resolves to two store directories: two
|
|
101
|
+
* versions of one distribution, or one spelling that both tracks hold. The
|
|
102
|
+
* outcome is terminal for the query. `detail`, when the realization
|
|
112
103
|
* gives one, names the two pins and the packages that need each side —
|
|
113
104
|
* without it, a caller must guess which package pulls each pin, and a
|
|
114
|
-
* wrong guess sends it into store surgery.
|
|
105
|
+
* wrong guess sends it into store surgery. For one spelling in two tracks
|
|
106
|
+
* the detail names the two identity keys instead.
|
|
115
107
|
* - `unavailable` — the link pass itself could not answer: an unreadable
|
|
116
108
|
* dependency graph, a locked farm. The reason says why. It says NOTHING
|
|
117
109
|
* about the presence of the package, and it must never render as an
|
|
@@ -120,25 +112,25 @@ export interface PackageRequest {
|
|
|
120
112
|
*/
|
|
121
113
|
export type PackageRequestOutcome = {
|
|
122
114
|
readonly kind: "linked";
|
|
123
|
-
readonly
|
|
115
|
+
readonly spelling: string;
|
|
124
116
|
readonly version: string;
|
|
125
117
|
} | {
|
|
126
118
|
readonly kind: "present";
|
|
127
|
-
readonly
|
|
119
|
+
readonly spelling: string;
|
|
128
120
|
readonly version: string;
|
|
129
121
|
} | {
|
|
130
122
|
readonly kind: "absent";
|
|
131
|
-
readonly
|
|
123
|
+
readonly spelling: string;
|
|
132
124
|
readonly acquisitionPossible: boolean;
|
|
133
125
|
readonly detail?: string;
|
|
134
126
|
} | {
|
|
135
127
|
readonly kind: "collision";
|
|
136
|
-
readonly
|
|
128
|
+
readonly spelling: string;
|
|
137
129
|
readonly storeDirs: readonly [string, string];
|
|
138
130
|
readonly detail?: string;
|
|
139
131
|
} | {
|
|
140
132
|
readonly kind: "unavailable";
|
|
141
|
-
readonly
|
|
133
|
+
readonly spelling: string;
|
|
142
134
|
readonly reason: string;
|
|
143
135
|
};
|
|
144
136
|
/**
|
|
@@ -146,8 +138,11 @@ export type PackageRequestOutcome = {
|
|
|
146
138
|
* packages into the farm of the analysis. It never installs, downloads, or
|
|
147
139
|
* acquires. A link is live in a sandbox that already runs, because the farm
|
|
148
140
|
* rides a bind mount.
|
|
141
|
+
*
|
|
142
|
+
* The seam speaks the query of the `package-identity` capability, thus the
|
|
143
|
+
* host resolves the identity and the harness never folds a name.
|
|
149
144
|
*/
|
|
150
|
-
export type ExtendAnalysisFarm = (analysisId: string,
|
|
145
|
+
export type ExtendAnalysisFarm = (analysisId: string, queries: readonly PackageQuery[]) => Promise<readonly PackageRequestOutcome[]>;
|
|
151
146
|
/**
|
|
152
147
|
* Per-sandbox-machine liveness verdict. `oomKilled` is meaningful only when
|
|
153
148
|
* `alive` is false: true when the backend reports the machine was killed for
|
|
@@ -8,9 +8,9 @@ import type { AnalysisPlan } from "./workflow-state.js";
|
|
|
8
8
|
* and the hardcoded sandbox turn budget. */
|
|
9
9
|
export declare const PlanStepSchema: z.ZodObject<{
|
|
10
10
|
name: z.ZodString;
|
|
11
|
-
id: z.ZodString;
|
|
12
11
|
context: z.ZodOptional<z.ZodString>;
|
|
13
12
|
description: z.ZodOptional<z.ZodString>;
|
|
13
|
+
id: z.ZodString;
|
|
14
14
|
track: z.ZodString;
|
|
15
15
|
step_type: z.ZodString;
|
|
16
16
|
question: z.ZodString;
|
|
@@ -41,9 +41,9 @@ export declare const PlannerPlanSchema: z.ZodObject<{
|
|
|
41
41
|
title: z.ZodString;
|
|
42
42
|
steps: z.ZodArray<z.ZodObject<{
|
|
43
43
|
name: z.ZodString;
|
|
44
|
-
id: z.ZodString;
|
|
45
44
|
context: z.ZodOptional<z.ZodString>;
|
|
46
45
|
description: z.ZodOptional<z.ZodString>;
|
|
46
|
+
id: z.ZodString;
|
|
47
47
|
track: z.ZodString;
|
|
48
48
|
step_type: z.ZodString;
|
|
49
49
|
question: z.ZodString;
|
|
@@ -83,9 +83,9 @@ export declare const PlanningAgentOutputSchema: z.ZodObject<{
|
|
|
83
83
|
title: z.ZodString;
|
|
84
84
|
steps: z.ZodArray<z.ZodObject<{
|
|
85
85
|
name: z.ZodString;
|
|
86
|
-
id: z.ZodString;
|
|
87
86
|
context: z.ZodOptional<z.ZodString>;
|
|
88
87
|
description: z.ZodOptional<z.ZodString>;
|
|
88
|
+
id: z.ZodString;
|
|
89
89
|
track: z.ZodString;
|
|
90
90
|
step_type: z.ZodString;
|
|
91
91
|
question: z.ZodString;
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { KNOWN_AGENT_IDS } from "../agents/sandbox-catalog.js";
|
|
15
15
|
import { CycleError, DependencyError, topoSortIntoWaves } from "../execution/topo-sort.js";
|
|
16
|
+
import { parseQuery } from "../sandbox/package-identity.js";
|
|
16
17
|
import { isSafeId, STEP_SUBDIRS, SYNTHESIS_STEP_ID } from "../workspace/paths.js";
|
|
17
18
|
const KNOWN_AGENTS = new Set(KNOWN_AGENT_IDS);
|
|
18
19
|
/**
|
|
@@ -24,6 +25,30 @@ const KNOWN_AGENTS = new Set(KNOWN_AGENT_IDS);
|
|
|
24
25
|
* the row's `(run_id, step_id)` primary key (see the harness-workspace-tools spec).
|
|
25
26
|
*/
|
|
26
27
|
const RESERVED_STEP_IDS = new Set([...STEP_SUBDIRS, SYNTHESIS_STEP_ID]);
|
|
28
|
+
/**
|
|
29
|
+
* One issue for one package entry that does not parse. Each message names the
|
|
30
|
+
* step and the entry, because a planner corrects the entry it wrote.
|
|
31
|
+
*
|
|
32
|
+
* The wording carries the reason of each refusal. A location publishes an
|
|
33
|
+
* installer detail as an interface. An unknown `<word>:` prefix rides into the
|
|
34
|
+
* pool as part of the name. A specifier that is not `==` turns a range such as
|
|
35
|
+
* `numpy>=1.26` into a package name, and the pool then refuses a package that
|
|
36
|
+
* it holds.
|
|
37
|
+
*/
|
|
38
|
+
function describeParseError(stepId, entry, error) {
|
|
39
|
+
switch (error.type) {
|
|
40
|
+
case "empty":
|
|
41
|
+
return `Step "${stepId}" names an empty package entry — name each package as a requirement (a bare name, or name==version)`;
|
|
42
|
+
case "location":
|
|
43
|
+
return (`Step "${stepId}" names a package location "${entry}" — name each package as a requirement ` +
|
|
44
|
+
`(a bare name, or name==version), never a path, a URL, or a store directory`);
|
|
45
|
+
case "unknown_prefix":
|
|
46
|
+
return (`Step "${stepId}" names the ecosystem of "${entry}" with a prefix the link pass cannot read — ` +
|
|
47
|
+
`the permitted prefixes are "python:" and "r:", and a bare name searches both tracks`);
|
|
48
|
+
case "unsupported_specifier":
|
|
49
|
+
return (`Step "${stepId}" pins "${entry}" with a specifier the link pass cannot honor — ` + `use a bare name, or name==version with one exact version`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
27
52
|
/** Derive a filesystem-safe output prefix from a step ID. */
|
|
28
53
|
export function deriveOutputPrefix(stepId) {
|
|
29
54
|
return stepId
|
|
@@ -109,23 +134,15 @@ export function validatePlan(plan, options) {
|
|
|
109
134
|
`step-directory convention; "${SYNTHESIS_STEP_ID}" is the run-level synthesis phase)`);
|
|
110
135
|
}
|
|
111
136
|
}
|
|
112
|
-
// 6.
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
// The specifier check exists because the link pass splits on `==` only:
|
|
117
|
-
// an unrefused `numpy>=1.26` becomes a package NAME, and the pool then
|
|
118
|
-
// refuses a package it holds.
|
|
137
|
+
// 6. Each package entry is a query of the one grammar. The validation IS
|
|
138
|
+
// the parse, thus no second reader of the grammar can disagree with the
|
|
139
|
+
// link pass. An absent array passes, because stored plans from before
|
|
140
|
+
// the field carry none.
|
|
119
141
|
for (const step of plan.steps) {
|
|
120
142
|
for (const entry of step.packages ?? []) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
else if (/[<>!~]/.test(entry) || (entry.includes("=") && !entry.includes("=="))) {
|
|
126
|
-
errors.push(`Step "${step.id}" pins "${entry}" with a specifier the link pass cannot honor — ` +
|
|
127
|
-
`use a bare name, or name==version with one exact version`);
|
|
128
|
-
}
|
|
143
|
+
const parsed = parseQuery(entry);
|
|
144
|
+
if (parsed.isErr())
|
|
145
|
+
errors.push(describeParseError(step.id, entry, parsed.error));
|
|
129
146
|
}
|
|
130
147
|
}
|
|
131
148
|
// 7. Step-id path safety. The id becomes the `runs/{runId}/{stepId}` directory
|
|
@@ -31,7 +31,9 @@ export const AnalysisStepSchema = z.object({
|
|
|
31
31
|
packages: z
|
|
32
32
|
.array(z.string())
|
|
33
33
|
.optional()
|
|
34
|
-
.describe('The packages this step imports, each as a
|
|
34
|
+
.describe('The packages this step imports, each as a query in the grammar [python:|r:]<name>[==<version>]: a bare name ("scanpy"), a name with one exact version ("numpy==1.26.4"), or either behind a track prefix ("python:igraph", "r:decoupleR==2.17.0"). ' +
|
|
35
|
+
"A bare name searches both tracks. " +
|
|
36
|
+
"When the package census shows one name under the Python section and under the R section, write the prefixed form — a bare both-track name refuses the launch. " +
|
|
35
37
|
"Never a path, a URL, or a store directory. An empty array means the step needs nothing beyond the baked toolchain. " +
|
|
36
38
|
"The set is not a promise of completeness — the execution agent can still link a missing package."),
|
|
37
39
|
depends_on: z
|