@inflexa-ai/harness 0.19.2 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/conversation-agent.d.ts +26 -14
- package/dist/agents/conversation-agent.js +12 -23
- package/dist/agents/report-session-agent.d.ts +31 -5
- package/dist/agents/report-session-agent.js +31 -6
- package/dist/app/chat-turn.js +3 -1
- package/dist/app/message-assembly.d.ts +15 -1
- package/dist/app/message-assembly.js +22 -8
- package/dist/app/report-session-runtime.d.ts +21 -4
- package/dist/app/report-session-runtime.js +53 -6
- package/dist/app/spawn-report-session.d.ts +174 -17
- package/dist/app/spawn-report-session.js +233 -21
- package/dist/contracts/chat-events.d.ts +7 -2
- package/dist/contracts/message.d.ts +10 -0
- package/dist/contracts/report-blocks.d.ts +743 -19
- package/dist/contracts/report-blocks.js +186 -11
- package/dist/contracts/report-reference.d.ts +88 -0
- package/dist/contracts/report-reference.js +48 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +20 -0
- package/dist/lib/chrome.d.ts +26 -6
- package/dist/lib/chrome.js +99 -32
- package/dist/lib/eyes.d.ts +57 -0
- package/dist/lib/eyes.js +45 -0
- package/dist/lib/page-capture.d.ts +14 -0
- package/dist/lib/page-capture.js +56 -2
- package/dist/literature/sources/pubmed.js +2 -1
- package/dist/loop/run-agent.js +58 -23
- package/dist/loop/tool-detail.d.ts +19 -2
- package/dist/loop/tool-detail.js +35 -2
- package/dist/memory/content-to-cortex.d.ts +3 -2
- package/dist/memory/content-to-cortex.js +21 -12
- package/dist/memory/conversation-display-replay.js +14 -7
- package/dist/memory/thread-history.d.ts +60 -3
- package/dist/memory/thread-history.js +55 -16
- package/dist/memory/thread-store.d.ts +11 -1
- package/dist/memory/thread-store.js +12 -2
- package/dist/prompts/conversation.d.ts +1 -1
- package/dist/prompts/conversation.js +23 -6
- package/dist/prompts/report-session.d.ts +1 -1
- package/dist/prompts/report-session.js +183 -9
- package/dist/report-model/authoring-grammar.d.ts +463 -0
- package/dist/report-model/authoring-grammar.js +77 -0
- package/dist/report-model/block-walk.d.ts +57 -14
- package/dist/report-model/block-walk.js +116 -11
- package/dist/report-model/draft-finish.d.ts +22 -4
- package/dist/report-model/draft-finish.js +39 -7
- package/dist/report-model/draft-operations.d.ts +3 -0
- package/dist/report-model/draft-operations.js +19 -5
- package/dist/report-model/draft-read.d.ts +8 -5
- package/dist/report-model/draft-read.js +21 -5
- package/dist/report-model/fixture-resolver.js +15 -4
- package/dist/report-model/pin-snapshot.d.ts +24 -6
- package/dist/report-model/pin-snapshot.js +204 -6
- package/dist/report-model/production-resolver.js +15 -4
- package/dist/report-model/reference-resolver.d.ts +50 -1
- package/dist/report-model/reference-resolver.js +94 -1
- package/dist/report-model/reference-stamp.d.ts +45 -0
- package/dist/report-model/reference-stamp.js +173 -0
- package/dist/report-model/structural-validation.d.ts +4 -1
- package/dist/report-model/structural-validation.js +48 -3
- package/dist/report-model/validate.d.ts +1 -1
- package/dist/report-model/validate.js +1 -1
- package/dist/report-render/asset-lookup.d.ts +19 -0
- package/dist/report-render/asset-lookup.js +37 -0
- package/dist/report-render/assets.d.ts +45 -1
- package/dist/report-render/assets.js +65 -6
- package/dist/report-render/chart-presets.d.ts +102 -0
- package/dist/report-render/chart-presets.js +205 -0
- package/dist/report-render/chart.d.ts +112 -10
- package/dist/report-render/chart.js +1164 -41
- package/dist/report-render/design.d.ts +99 -0
- package/dist/report-render/design.js +289 -107
- package/dist/report-render/number-format.d.ts +117 -0
- package/dist/report-render/number-format.js +479 -0
- package/dist/report-render/page.d.ts +121 -5
- package/dist/report-render/page.js +689 -7
- package/dist/report-render/references.d.ts +70 -16
- package/dist/report-render/references.js +54 -22
- package/dist/report-render/render.d.ts +23 -5
- package/dist/report-render/render.js +81 -22
- package/dist/report-render/table-data.d.ts +92 -0
- package/dist/report-render/table-data.js +129 -0
- package/dist/report-render/types.d.ts +15 -0
- package/dist/report-render/value-bridge.js +6 -2
- package/dist/report-render/views/chart-view.d.ts +12 -5
- package/dist/report-render/views/chart-view.js +14 -8
- package/dist/report-render/views/page-view.d.ts +36 -15
- package/dist/report-render/views/page-view.js +45 -23
- package/dist/report-render/views/prose.d.ts +12 -2
- package/dist/report-render/views/prose.js +29 -7
- package/dist/report-render/views/references-view.d.ts +22 -8
- package/dist/report-render/views/references-view.js +101 -13
- package/dist/report-render/views/values.d.ts +128 -13
- package/dist/report-render/views/values.js +199 -18
- package/dist/runtime/assemble.d.ts +39 -5
- package/dist/runtime/assemble.js +47 -6
- package/dist/sandbox/client.d.ts +10 -1
- package/dist/sandbox/create-sandbox.d.ts +9 -4
- package/dist/sandbox/create-sandbox.js +22 -16
- package/dist/sandbox/docker-client.js +10 -7
- package/dist/sandbox/k8s-client.js +11 -18
- package/dist/sandbox/mount-plan.d.ts +45 -7
- package/dist/sandbox/mount-plan.js +43 -5
- package/dist/sandbox/types.d.ts +10 -6
- package/dist/state/init.js +38 -9
- package/dist/state/report-session-state.d.ts +42 -1
- package/dist/state/report-session-state.js +70 -2
- package/dist/state/report-versions.d.ts +22 -17
- package/dist/state/report-versions.js +48 -35
- package/dist/state/snapshot-parse.js +16 -1
- package/dist/tasks/data-profile.d.ts +7 -4
- package/dist/tasks/data-profile.js +9 -9
- package/dist/tools/define-tool.d.ts +32 -0
- package/dist/tools/define-tool.js +4 -0
- package/dist/tools/detail-resolver.d.ts +7 -2
- package/dist/tools/detail-resolver.js +7 -2
- package/dist/tools/iterate-report.d.ts +1 -1
- package/dist/tools/report/preview-snapshot.js +1 -0
- package/dist/tools/report-authoring/authoring-tools.d.ts +15 -6
- package/dist/tools/report-authoring/authoring-tools.js +99 -18
- package/dist/tools/report-session/derive-table.d.ts +162 -0
- package/dist/tools/report-session/derive-table.js +459 -0
- package/dist/tools/report-session/examine-page.d.ts +48 -18
- package/dist/tools/report-session/examine-page.js +204 -24
- package/dist/tools/report-session/index.d.ts +2 -0
- package/dist/tools/report-session/index.js +2 -0
- package/dist/tools/report-session/list-artifacts.d.ts +97 -0
- package/dist/tools/report-session/list-artifacts.js +197 -0
- package/dist/tools/report-session/preview-report.d.ts +6 -0
- package/dist/tools/report-session/preview-report.js +180 -28
- package/dist/tools/report-session/record-version.d.ts +22 -10
- package/dist/tools/report-session/record-version.js +99 -14
- package/dist/tools/start-report-session.d.ts +115 -0
- package/dist/tools/start-report-session.js +179 -0
- package/dist/workflows/sandbox-step.d.ts +7 -4
- package/dist/workflows/sandbox-step.js +10 -12
- package/dist/workspace/paths.d.ts +50 -0
- package/dist/workspace/paths.js +63 -0
- package/package.json +2 -1
|
@@ -6,6 +6,12 @@
|
|
|
6
6
|
* resolves each reference through the injected resolver, bridges the resolved values into the render model,
|
|
7
7
|
* and renders the page.
|
|
8
8
|
*
|
|
9
|
+
* The renderer writes no file. It gives the page and the data asset of each table, and this tool stages
|
|
10
|
+
* them beside the figures. It also writes the script of each derivation that the document references, from
|
|
11
|
+
* the text of the durable record. The shipped libraries and fonts stage under `assets/deps/`, and each
|
|
12
|
+
* report-side file at the root of `assets/`. The stage is authoritative over that root: what this preview
|
|
13
|
+
* wrote stays, and each other file goes.
|
|
14
|
+
*
|
|
9
15
|
* The page and its staged assets land in `report-sessions/{threadId}/` under the workspace root. That
|
|
10
16
|
* namespace belongs to this path alone, thus the tool never writes under the old `previews/` or `reports/`
|
|
11
17
|
* trees. The result carries the absolute page path, thus a local host shows the page with no seam.
|
|
@@ -25,19 +31,21 @@
|
|
|
25
31
|
* control-flow exception propagate.
|
|
26
32
|
*/
|
|
27
33
|
import { err, ok } from "neverthrow";
|
|
28
|
-
import { copyFile, mkdir, writeFile } from "node:fs/promises";
|
|
29
|
-
import { createRequire } from "node:module";
|
|
34
|
+
import { copyFile, mkdir, readdir, rm, writeFile } from "node:fs/promises";
|
|
30
35
|
import { extname, join } from "node:path";
|
|
36
|
+
import { relative as relativePosix } from "node:path/posix";
|
|
31
37
|
import { z } from "zod";
|
|
32
|
-
import { resolveWorkspacePath } from "../../workspace/paths.js";
|
|
38
|
+
import { reportSessionDir, resolveWorkspacePath } from "../../workspace/paths.js";
|
|
33
39
|
import { createNoopLogger } from "../../lib/console-logger.js";
|
|
34
40
|
import { describeFsError, tryFsWrite } from "../../lib/fs-result.js";
|
|
35
41
|
import { defaultErrorFields } from "../../lib/logger.js";
|
|
42
|
+
import { referencedPaths, walkBlocks } from "../../report-model/block-walk.js";
|
|
36
43
|
import { computeDraftHash } from "../../report-model/draft-hash.js";
|
|
37
44
|
import { finishDraft } from "../../report-model/draft-finish.js";
|
|
38
45
|
import { resolveDocumentReferences } from "../../report-model/validate.js";
|
|
39
46
|
import { bridgeValues } from "../../report-render/value-bridge.js";
|
|
40
|
-
import {
|
|
47
|
+
import { resolvePageAssetFromInstallation } from "../../report-render/asset-lookup.js";
|
|
48
|
+
import { ASSETS_DIR, DEPS_DIR, derivationScriptName, PAGE_ASSETS, stagedSource, tableSidecarName } from "../../report-render/assets.js";
|
|
41
49
|
import { renderReportPage } from "../../report-render/render.js";
|
|
42
50
|
import { defineTool } from "../define-tool.js";
|
|
43
51
|
import { openReportThread } from "../report-authoring/authoring-tools.js";
|
|
@@ -58,13 +66,6 @@ function assetFileName(file) {
|
|
|
58
66
|
function figureSourcePolicy(file) {
|
|
59
67
|
return `assets/${assetFileName(file)}`;
|
|
60
68
|
}
|
|
61
|
-
/**
|
|
62
|
-
* The resolver of a package source. A manifest entry names a module specifier, and the resolution reads
|
|
63
|
-
* the installation of the harness. Thus the staged bytes are the bytes of the pinned version.
|
|
64
|
-
*/
|
|
65
|
-
const moduleResolver = createRequire(import.meta.url);
|
|
66
|
-
/** The default asset lookup: the module resolution of the installation of the harness. */
|
|
67
|
-
const resolvePageAssetFromInstallation = (specifier) => moduleResolver.resolve(specifier);
|
|
68
69
|
/**
|
|
69
70
|
* Pair each block with the resolved value of its binding, in document order.
|
|
70
71
|
*
|
|
@@ -110,6 +111,77 @@ function collectResolutions(blocks, resolvedByBlock) {
|
|
|
110
111
|
}
|
|
111
112
|
return resolutions;
|
|
112
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* The sidecar of each table block of the document, in document order.
|
|
116
|
+
*
|
|
117
|
+
* A table card offers the whole table as a download, and the download is the pinned file itself and never a
|
|
118
|
+
* re-serialization of it. The card spells the staged name through `tableSidecarName`, thus the stage reads
|
|
119
|
+
* that same function and the link and the file cannot disagree.
|
|
120
|
+
*
|
|
121
|
+
* A chart binds a table too, and it holds no card. Thus the walk reads the table blocks alone.
|
|
122
|
+
*/
|
|
123
|
+
function collectTableSidecars(blocks) {
|
|
124
|
+
const sidecars = [];
|
|
125
|
+
const visit = (block) => {
|
|
126
|
+
if (block.kind === "section") {
|
|
127
|
+
for (const child of block.blocks) {
|
|
128
|
+
visit(child);
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (block.kind === "table") {
|
|
133
|
+
sidecars.push({ blockId: block.id, name: tableSidecarName(block.binding.hash, block.binding.path), path: block.binding.path });
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
for (const block of blocks) {
|
|
137
|
+
visit(block);
|
|
138
|
+
}
|
|
139
|
+
return sidecars;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* The derivation records that the document references, in the order that they landed.
|
|
143
|
+
*
|
|
144
|
+
* A binding names an output path, thus the used set is the intersection of the recorded paths and the paths
|
|
145
|
+
* of the document. The record of a dropped block stays in the durable state, and this walk leaves it out.
|
|
146
|
+
* Thus the page states the chain of what it shows, and the stage writes the script of what it states.
|
|
147
|
+
*/
|
|
148
|
+
function referencedDerivations(document, derivations) {
|
|
149
|
+
if (derivations.length === 0) {
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
const named = referencedPaths(walkBlocks(document.sections).references);
|
|
153
|
+
return derivations.filter((record) => named.has(record.outputPath));
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* The chain of one record, with the two relative links that its appendix entry carries.
|
|
157
|
+
*
|
|
158
|
+
* The script link names the staged asset through the shared name function, thus the link and the file that
|
|
159
|
+
* the stage writes cannot disagree. The output link is the path of the derived file against the directory of
|
|
160
|
+
* the page. The derived table already sits inside the session directory, thus the link needs no copy.
|
|
161
|
+
*/
|
|
162
|
+
function chainOf(record, sessionDir) {
|
|
163
|
+
return {
|
|
164
|
+
outputPath: record.outputPath,
|
|
165
|
+
sources: record.sources,
|
|
166
|
+
scriptHash: record.scriptHash,
|
|
167
|
+
scriptSource: stagedSource(derivationScriptName(record.scriptHash)),
|
|
168
|
+
outputSource: relativePosix(sessionDir, record.outputPath),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* The script of each referenced derivation, keyed by its staged name.
|
|
173
|
+
*
|
|
174
|
+
* Two records of one script text carry one hash, thus they take one name and the map holds one entry. The
|
|
175
|
+
* name is content-addressed, thus an amended script stages beside no stale copy of itself and the sweep
|
|
176
|
+
* removes the name that the new page does not reference.
|
|
177
|
+
*/
|
|
178
|
+
function scriptAssets(records) {
|
|
179
|
+
const scripts = new Map();
|
|
180
|
+
for (const record of records) {
|
|
181
|
+
scripts.set(derivationScriptName(record.scriptHash), record.script);
|
|
182
|
+
}
|
|
183
|
+
return scripts;
|
|
184
|
+
}
|
|
113
185
|
/**
|
|
114
186
|
* Resolve each reference of the document, and split the resolutions from the unresolved references.
|
|
115
187
|
*
|
|
@@ -136,9 +208,14 @@ async function resolveDocument(document, resolver, snapshot) {
|
|
|
136
208
|
* thus the containment test runs before any copy and reuses the one workspace-path resolver. A source
|
|
137
209
|
* outside the root refuses and names the block, and no copy runs.
|
|
138
210
|
*
|
|
139
|
-
* The page references the chart runtime and the fonts under
|
|
140
|
-
*
|
|
141
|
-
*
|
|
211
|
+
* The page references the chart runtime and the fonts under `assets/deps/`, and each report-side file at
|
|
212
|
+
* the root of `assets/`. Each manifest entry names its own subpath, thus the copy loop reads the manifest
|
|
213
|
+
* and it spells no layout of its own. The manifest is never empty, thus both directories exist beside every
|
|
214
|
+
* page.
|
|
215
|
+
*
|
|
216
|
+
* The data assets and the derivation scripts are the parts that this preview produces rather than the disk.
|
|
217
|
+
* They write after the copies and before the page. The sweep then runs, and the root of the directory holds
|
|
218
|
+
* the closure of this page alone.
|
|
142
219
|
*/
|
|
143
220
|
async function renderToWorkspace(args) {
|
|
144
221
|
let root;
|
|
@@ -148,31 +225,47 @@ async function renderToWorkspace(args) {
|
|
|
148
225
|
catch (cause) {
|
|
149
226
|
return err({ kind: "fs", error: { type: "read_failed", op: "preview.resolveWorkspaceRoot", path: args.analysisId, cause } });
|
|
150
227
|
}
|
|
151
|
-
const sessionDir = join(root,
|
|
152
|
-
|
|
228
|
+
const sessionDir = join(root, reportSessionDir(args.threadId));
|
|
229
|
+
// The page addresses each staged file through `assetSource`, which spells the same segment. Thus the
|
|
230
|
+
// directory that receives the copies comes from that one constant, and never from a literal here.
|
|
231
|
+
const assetsDir = join(sessionDir, ASSETS_DIR);
|
|
232
|
+
const depsDir = join(assetsDir, DEPS_DIR);
|
|
153
233
|
const pagePath = join(sessionDir, "index.html");
|
|
154
234
|
// Each bound figure, contained and deduplicated by its staged name, mapped to its host source.
|
|
155
|
-
const
|
|
235
|
+
const reportFiles = new Map();
|
|
156
236
|
for (const resolution of args.resolutions) {
|
|
157
237
|
if (resolution.kind !== "figure" || resolution.resolved.type !== "file") {
|
|
158
238
|
continue;
|
|
159
239
|
}
|
|
160
240
|
const file = resolution.resolved;
|
|
161
241
|
const name = assetFileName(file);
|
|
162
|
-
if (
|
|
242
|
+
if (reportFiles.has(name)) {
|
|
163
243
|
continue;
|
|
164
244
|
}
|
|
165
245
|
const resolved = resolveWorkspacePath({ workspaceRoot: root, analysisId: args.analysisId, path: file.path });
|
|
166
246
|
if (resolved.kind !== "ok") {
|
|
167
247
|
return err({ kind: "figure-out-of-scope", blockId: resolution.blockId, path: file.path });
|
|
168
248
|
}
|
|
169
|
-
|
|
249
|
+
reportFiles.set(name, resolved.absolute);
|
|
170
250
|
}
|
|
171
|
-
// Each
|
|
172
|
-
//
|
|
251
|
+
// Each table sidecar: the pinned bytes themselves, under the name that the download link of the card
|
|
252
|
+
// spells. The path of a snapshot entry is untrusted, thus the containment test runs here too.
|
|
253
|
+
for (const sidecar of args.sidecars) {
|
|
254
|
+
if (reportFiles.has(sidecar.name)) {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
const resolved = resolveWorkspacePath({ workspaceRoot: root, analysisId: args.analysisId, path: sidecar.path });
|
|
258
|
+
if (resolved.kind !== "ok") {
|
|
259
|
+
return err({ kind: "figure-out-of-scope", blockId: sidecar.blockId, path: sidecar.path });
|
|
260
|
+
}
|
|
261
|
+
reportFiles.set(sidecar.name, resolved.absolute);
|
|
262
|
+
}
|
|
263
|
+
// Each manifest entry, mapped from its staged subpath to the file that the asset lookup gives. A
|
|
264
|
+
// specifier that does not resolve is a fault of the installation, thus it rides the `fs` kind.
|
|
265
|
+
const depsFiles = new Map();
|
|
173
266
|
for (const asset of PAGE_ASSETS) {
|
|
174
267
|
try {
|
|
175
|
-
|
|
268
|
+
depsFiles.set(asset.file, args.resolvePageAsset(asset.specifier));
|
|
176
269
|
}
|
|
177
270
|
catch (cause) {
|
|
178
271
|
return err({ kind: "fs", error: { type: "read_failed", op: "preview.resolveAsset", path: asset.specifier, cause } });
|
|
@@ -182,22 +275,66 @@ async function renderToWorkspace(args) {
|
|
|
182
275
|
if (madeSession.isErr()) {
|
|
183
276
|
return err({ kind: "fs", error: madeSession.error });
|
|
184
277
|
}
|
|
185
|
-
|
|
278
|
+
// The recursive make covers the assets directory itself, thus one call answers for both levels.
|
|
279
|
+
const madeAssets = await tryFsWrite("preview.mkdir", () => mkdir(depsDir, { recursive: true }), { path: depsDir });
|
|
186
280
|
if (madeAssets.isErr()) {
|
|
187
281
|
return err({ kind: "fs", error: madeAssets.error });
|
|
188
282
|
}
|
|
189
|
-
for (const [name, source] of
|
|
283
|
+
for (const [name, source] of [...reportFiles, ...depsFiles]) {
|
|
190
284
|
const copied = await tryFsWrite("preview.copyFile", () => copyFile(source, join(assetsDir, name)), { path: source });
|
|
191
285
|
if (copied.isErr()) {
|
|
192
286
|
return err({ kind: "fs", error: copied.error });
|
|
193
287
|
}
|
|
194
288
|
}
|
|
289
|
+
// A data asset is source text that the renderer derived, and a script is the text of a durable record.
|
|
290
|
+
// Neither one sits on disk under its staged name, thus both write and neither one copies.
|
|
291
|
+
const texts = [...args.dataAssets.map((asset) => [asset.name, asset.bytes]), ...args.scripts];
|
|
292
|
+
for (const [name, text] of texts) {
|
|
293
|
+
const assetPath = join(assetsDir, name);
|
|
294
|
+
const wroteAsset = await tryFsWrite("preview.writeFile", () => writeFile(assetPath, text, "utf8"), { path: assetPath });
|
|
295
|
+
if (wroteAsset.isErr()) {
|
|
296
|
+
return err({ kind: "fs", error: wroteAsset.error });
|
|
297
|
+
}
|
|
298
|
+
}
|
|
195
299
|
const wrote = await tryFsWrite("preview.writeFile", () => writeFile(pagePath, args.page, "utf8"), { path: pagePath });
|
|
196
300
|
if (wrote.isErr()) {
|
|
197
301
|
return err({ kind: "fs", error: wrote.error });
|
|
198
302
|
}
|
|
303
|
+
const staged = new Set([...reportFiles.keys(), ...texts.map(([name]) => name)]);
|
|
304
|
+
await sweepAssets(assetsDir, staged, args.logger);
|
|
199
305
|
return ok(pagePath);
|
|
200
306
|
}
|
|
307
|
+
/**
|
|
308
|
+
* Remove each report-side file of the assets directory that this preview did not stage.
|
|
309
|
+
*
|
|
310
|
+
* The stage is authoritative: the root of the directory holds the closure of the page and nothing else. A
|
|
311
|
+
* block that goes leaves a stale data asset, a stale sidecar, a stale figure, and the script of a derivation
|
|
312
|
+
* that nothing binds. Each one costs disk and misleads a reader who opens the directory. The staged set is
|
|
313
|
+
* what this run wrote, thus the sweep needs no read of the page.
|
|
314
|
+
*
|
|
315
|
+
* The `deps/` directory holds the shipped libraries and fonts. The manifest governs that set, and the stage
|
|
316
|
+
* writes each entry of it on every run. Thus the sweep passes over the directory whole.
|
|
317
|
+
*
|
|
318
|
+
* A sweep fault costs the cleanup alone. The page and its assets are on disk and complete, thus a failed
|
|
319
|
+
* listing and a failed removal each log and the preview still reports the page.
|
|
320
|
+
*/
|
|
321
|
+
async function sweepAssets(assetsDir, staged, logger) {
|
|
322
|
+
const listed = await tryFsWrite("preview.readdir", () => readdir(assetsDir), { path: assetsDir });
|
|
323
|
+
if (listed.isErr()) {
|
|
324
|
+
logger.warn("the assets directory did not list", { path: assetsDir, detail: describeFsError(listed.error) });
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
for (const entry of listed.value) {
|
|
328
|
+
if (entry === DEPS_DIR || staged.has(entry)) {
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
const stale = join(assetsDir, entry);
|
|
332
|
+
const removed = await tryFsWrite("preview.rm", () => rm(stale, { force: true, recursive: true }), { path: stale });
|
|
333
|
+
if (removed.isErr()) {
|
|
334
|
+
logger.warn("a stale asset did not go", { path: stale, detail: describeFsError(removed.error) });
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
201
338
|
/**
|
|
202
339
|
* Make the render-and-preview tool over the session-state gateway and the render seams.
|
|
203
340
|
*
|
|
@@ -217,14 +354,19 @@ export function createPreviewReportTool(deps) {
|
|
|
217
354
|
inputSchema: previewReportInput,
|
|
218
355
|
executionMode: "inline",
|
|
219
356
|
describeCall: "none",
|
|
357
|
+
// The page path is the one fact of a preview that a watcher wants, and it exists only in the
|
|
358
|
+
// result. Each other arm is a degraded condition whose kind already names it, thus the kind
|
|
359
|
+
// stands as the line. A stamp failure is such an arm: the page landed, but the marker did not,
|
|
360
|
+
// and a line that named the page would read as a clean pass.
|
|
361
|
+
describeResult: (_input, result) => (result.outcome === "rendered" ? `page ${result.pagePath}` : result.outcome),
|
|
220
362
|
execute: async (_input, ctx) => {
|
|
221
363
|
const opened = await openReportThread(deps.gateway, ctx.session.scope);
|
|
222
364
|
if (opened.isErr()) {
|
|
223
365
|
return ok({ outcome: "refused", refusal: opened.error });
|
|
224
366
|
}
|
|
225
|
-
const { threadId, analysisId, state } = opened.value;
|
|
367
|
+
const { threadId, analysisId, state, derivations } = opened.value;
|
|
226
368
|
const { document: draft, snapshot } = state;
|
|
227
|
-
const finished = finishDraft(draft, snapshot);
|
|
369
|
+
const finished = finishDraft(draft, snapshot, derivations);
|
|
228
370
|
if (!finished.valid) {
|
|
229
371
|
return ok({ outcome: "gaps", gaps: finished.gaps });
|
|
230
372
|
}
|
|
@@ -251,7 +393,13 @@ export function createPreviewReportTool(deps) {
|
|
|
251
393
|
if (bridged.isErr()) {
|
|
252
394
|
return ok({ outcome: "bridge-mismatch", mismatches: bridged.error });
|
|
253
395
|
}
|
|
254
|
-
|
|
396
|
+
// The citation records are the bibliography of the pin, and the derivation records are the chain
|
|
397
|
+
// of each derived path. Both ride the render call and never the value map, because the appendix
|
|
398
|
+
// reads them beside the cards and the value map is keyed by block. The chain carries the two
|
|
399
|
+
// relative links of its entry, thus the renderer states them and it stages nothing.
|
|
400
|
+
const used = referencedDerivations(document, derivations);
|
|
401
|
+
const sessionDir = reportSessionDir(threadId);
|
|
402
|
+
const rendered = renderReportPage(document, bridged.value, snapshot.citationRecords, used.map((record) => chainOf(record, sessionDir)));
|
|
255
403
|
if (rendered.isErr()) {
|
|
256
404
|
return ok({ outcome: "render-problems", problems: rendered.error });
|
|
257
405
|
}
|
|
@@ -261,7 +409,11 @@ export function createPreviewReportTool(deps) {
|
|
|
261
409
|
analysisId,
|
|
262
410
|
threadId,
|
|
263
411
|
resolutions,
|
|
264
|
-
|
|
412
|
+
sidecars: collectTableSidecars(document.sections),
|
|
413
|
+
dataAssets: rendered.value.dataAssets,
|
|
414
|
+
scripts: scriptAssets(used),
|
|
415
|
+
page: rendered.value.html,
|
|
416
|
+
logger,
|
|
265
417
|
});
|
|
266
418
|
if (written.isErr()) {
|
|
267
419
|
const failure = written.error;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The record tool of a report session.
|
|
3
3
|
*
|
|
4
|
-
* The tool records one version of the report.
|
|
5
|
-
* the
|
|
6
|
-
*
|
|
4
|
+
* The tool records one version of the report. A version that the gate did not accept must never stand, thus
|
|
5
|
+
* the tool runs the whole gate first, and only a pass reaches the store. The gate and the look rule run on
|
|
6
|
+
* every record, the first and each later one alike.
|
|
7
7
|
*
|
|
8
8
|
* The gate has three parts, in order of cost. The finish gates the schema, the unique ids, and the
|
|
9
9
|
* structural tier, and a gap list returns as data. The look-before-record rule then compares the seen hash
|
|
@@ -18,8 +18,15 @@
|
|
|
18
18
|
* before the resolver constructs.
|
|
19
19
|
*
|
|
20
20
|
* On a pass the store records the document that the gate validated, the pinned snapshot, and the anchor from
|
|
21
|
-
* the thread row.
|
|
22
|
-
*
|
|
21
|
+
* the thread row. A record on a thread that holds a version replaces that version whole, under the same
|
|
22
|
+
* version id. Thus the loop is unbounded: the agent amends, looks again, and records again, and the stored
|
|
23
|
+
* version equals the page that the last look accepted. A refused record leaves the stored version as it was.
|
|
24
|
+
* The outcome of a pass carries the version id, and it names a replacement.
|
|
25
|
+
*
|
|
26
|
+
* The prune runs after the version lands. It removes the output file of each derivation that the recorded
|
|
27
|
+
* document does not reference, under the `derived/` directory of the session alone. The records stay
|
|
28
|
+
* append-only, because the bytes are reproducible from the script and the sources. A failed removal logs and
|
|
29
|
+
* changes no outcome.
|
|
23
30
|
*/
|
|
24
31
|
import { z } from "zod";
|
|
25
32
|
import type { AuthContext } from "../../auth/types.js";
|
|
@@ -29,6 +36,7 @@ import { type FinishGap } from "../../report-model/draft-finish.js";
|
|
|
29
36
|
import type { ReferenceResolver } from "../../report-model/reference-resolver.js";
|
|
30
37
|
import { type ResolutionFailure, type SchemaIssue } from "../../report-model/validate.js";
|
|
31
38
|
import type { ReportVersionStore } from "../../state/report-versions.js";
|
|
39
|
+
import { type ResolveWorkspaceRoot } from "../../workspace/paths.js";
|
|
32
40
|
import { type Tool } from "../define-tool.js";
|
|
33
41
|
import { type ReportSessionStateGateway, type SessionRefusal } from "../report-authoring/authoring-tools.js";
|
|
34
42
|
/** The empty input. The tool records the current draft of the thread, thus it needs no field. */
|
|
@@ -41,8 +49,8 @@ export type RecordVersionInput = z.infer<typeof recordVersionInput>;
|
|
|
41
49
|
* `gaps` names each completeness gap of the draft. `never-seen` means that no eyes ran on the current draft.
|
|
42
50
|
* `stale-look` means that the eyes ran, and the agent then changed the draft. `root-unresolvable` means that
|
|
43
51
|
* the resolver construction cannot resolve the workspace root. `invalid` names each gate failure, and a
|
|
44
|
-
* resolution failure carries the block that holds it. `
|
|
45
|
-
*
|
|
52
|
+
* resolution failure carries the block that holds it. `recorded` carries the version id, and `replaced` is
|
|
53
|
+
* true when the record wrote over the earlier version of the thread.
|
|
46
54
|
*/
|
|
47
55
|
export type RecordVersionResult = {
|
|
48
56
|
outcome: "refused";
|
|
@@ -64,27 +72,31 @@ export type RecordVersionResult = {
|
|
|
64
72
|
schemaIssues?: SchemaIssue[];
|
|
65
73
|
duplicateIds?: string[];
|
|
66
74
|
resolutionFailures?: ResolutionFailure[];
|
|
67
|
-
} | {
|
|
68
|
-
outcome: "already-recorded";
|
|
69
75
|
} | {
|
|
70
76
|
outcome: "record-failed";
|
|
71
77
|
detail: string;
|
|
72
78
|
} | {
|
|
73
79
|
outcome: "recorded";
|
|
74
80
|
versionId: string;
|
|
81
|
+
replaced: boolean;
|
|
75
82
|
};
|
|
76
83
|
/**
|
|
77
84
|
* The construction deps of the record tool.
|
|
78
85
|
*
|
|
79
|
-
* `store` is the
|
|
86
|
+
* `store` is the version store. `threads` reads the anchor of the report thread, thus the
|
|
80
87
|
* version carries the parent conversation and the transcript position. `makeResolver` is optional, because a
|
|
81
88
|
* resolver realization can be absent, and the gate needs one. It binds one analysis, thus the tool makes the
|
|
82
89
|
* resolver over the scope of the call.
|
|
90
|
+
*
|
|
91
|
+
* `resolveWorkspaceRoot` maps the analysis of the call onto its workspace root. The prune reaches the
|
|
92
|
+
* derived directory of the session under that root, thus the seam is mandatory: a composition that bound
|
|
93
|
+
* none would record a version and leave every unused derivation on disk.
|
|
83
94
|
*/
|
|
84
95
|
export interface RecordVersionToolDeps {
|
|
85
96
|
readonly gateway: ReportSessionStateGateway;
|
|
86
97
|
readonly store: ReportVersionStore;
|
|
87
98
|
readonly threads: Pick<ThreadStore, "getThread">;
|
|
99
|
+
readonly resolveWorkspaceRoot: ResolveWorkspaceRoot;
|
|
88
100
|
readonly makeResolver?: (scope: {
|
|
89
101
|
analysisId: string;
|
|
90
102
|
auth: AuthContext;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The record tool of a report session.
|
|
3
3
|
*
|
|
4
|
-
* The tool records one version of the report.
|
|
5
|
-
* the
|
|
6
|
-
*
|
|
4
|
+
* The tool records one version of the report. A version that the gate did not accept must never stand, thus
|
|
5
|
+
* the tool runs the whole gate first, and only a pass reaches the store. The gate and the look rule run on
|
|
6
|
+
* every record, the first and each later one alike.
|
|
7
7
|
*
|
|
8
8
|
* The gate has three parts, in order of cost. The finish gates the schema, the unique ids, and the
|
|
9
9
|
* structural tier, and a gap list returns as data. The look-before-record rule then compares the seen hash
|
|
@@ -18,16 +18,28 @@
|
|
|
18
18
|
* before the resolver constructs.
|
|
19
19
|
*
|
|
20
20
|
* On a pass the store records the document that the gate validated, the pinned snapshot, and the anchor from
|
|
21
|
-
* the thread row.
|
|
22
|
-
*
|
|
21
|
+
* the thread row. A record on a thread that holds a version replaces that version whole, under the same
|
|
22
|
+
* version id. Thus the loop is unbounded: the agent amends, looks again, and records again, and the stored
|
|
23
|
+
* version equals the page that the last look accepted. A refused record leaves the stored version as it was.
|
|
24
|
+
* The outcome of a pass carries the version id, and it names a replacement.
|
|
25
|
+
*
|
|
26
|
+
* The prune runs after the version lands. It removes the output file of each derivation that the recorded
|
|
27
|
+
* document does not reference, under the `derived/` directory of the session alone. The records stay
|
|
28
|
+
* append-only, because the bytes are reproducible from the script and the sources. A failed removal logs and
|
|
29
|
+
* changes no outcome.
|
|
23
30
|
*/
|
|
24
31
|
import { ok } from "neverthrow";
|
|
32
|
+
import { rm } from "node:fs/promises";
|
|
33
|
+
import { resolve as resolvePath, sep } from "node:path";
|
|
25
34
|
import { z } from "zod";
|
|
26
35
|
import { createNoopLogger } from "../../lib/console-logger.js";
|
|
27
36
|
import { describeDbError } from "../../lib/db-result.js";
|
|
37
|
+
import { describeFsError, tryFsWrite } from "../../lib/fs-result.js";
|
|
38
|
+
import { referencedPaths, walkBlocks } from "../../report-model/block-walk.js";
|
|
28
39
|
import { finishDraft } from "../../report-model/draft-finish.js";
|
|
29
40
|
import { computeDraftHash } from "../../report-model/draft-hash.js";
|
|
30
41
|
import { validateReport } from "../../report-model/validate.js";
|
|
42
|
+
import { reportSessionDerivedDir, resolveWorkspacePath } from "../../workspace/paths.js";
|
|
31
43
|
import { defineTool } from "../define-tool.js";
|
|
32
44
|
import { openReportThread } from "../report-authoring/authoring-tools.js";
|
|
33
45
|
/** The empty input. The tool records the current draft of the thread, thus it needs no field. */
|
|
@@ -41,12 +53,66 @@ function describeRecordFailure(error) {
|
|
|
41
53
|
return "the pinned snapshot is malformed";
|
|
42
54
|
case "parent_analysis_mismatch":
|
|
43
55
|
return "the parent version belongs to a different analysis";
|
|
44
|
-
case "thread_already_holds_version":
|
|
45
|
-
return "the thread already holds a recorded version";
|
|
46
56
|
default:
|
|
47
57
|
return describeDbError(error);
|
|
48
58
|
}
|
|
49
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Remove the output file of each derivation that the recorded document does not reference.
|
|
62
|
+
*
|
|
63
|
+
* The unused set is a set difference: a derivation is used when a binding of the recorded document names its
|
|
64
|
+
* output path. The document is the one that the gate validated, thus the prune reads what the version holds
|
|
65
|
+
* and never the draft.
|
|
66
|
+
*
|
|
67
|
+
* The prune reaches the `derived/` directory of the session alone. A record whose path resolves outside that
|
|
68
|
+
* directory is skipped, because this tool never removes a file that it does not own. A stale record from an
|
|
69
|
+
* earlier layout and a crafted path both land in that arm.
|
|
70
|
+
*
|
|
71
|
+
* The version already stands at this point. Thus each failure costs the cleanup alone: an unresolvable root,
|
|
72
|
+
* a path that escapes, and a failed removal each log and change no outcome. The records stay, and the bytes
|
|
73
|
+
* are reproducible from the script and the sources. The scope of the prune resolves inside the same guard as
|
|
74
|
+
* the root, because both builders throw and a throw here would read as a failure of a call that succeeded.
|
|
75
|
+
*/
|
|
76
|
+
async function pruneUnusedDerivations(args) {
|
|
77
|
+
if (args.derivations.length === 0) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const named = referencedPaths(walkBlocks(args.document.sections).references);
|
|
81
|
+
const unused = args.derivations.filter((record) => !named.has(record.outputPath));
|
|
82
|
+
if (unused.length === 0) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
let root;
|
|
86
|
+
let derivedDir;
|
|
87
|
+
try {
|
|
88
|
+
root = args.resolveWorkspaceRoot(args.analysisId);
|
|
89
|
+
derivedDir = resolvePath(root, reportSessionDerivedDir(args.threadId));
|
|
90
|
+
}
|
|
91
|
+
catch (cause) {
|
|
92
|
+
args.logger.warn("the unused derivations did not prune", {
|
|
93
|
+
threadId: args.threadId,
|
|
94
|
+
analysisId: args.analysisId,
|
|
95
|
+
...args.logger.errorFields(cause),
|
|
96
|
+
});
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
for (const record of unused) {
|
|
100
|
+
const resolved = resolveWorkspacePath({ workspaceRoot: root, analysisId: args.analysisId, path: record.outputPath });
|
|
101
|
+
if (resolved.kind !== "ok" || !resolved.absolute.startsWith(derivedDir + sep)) {
|
|
102
|
+
args.logger.warn("an unused derivation sits outside the derived directory of the session", {
|
|
103
|
+
threadId: args.threadId,
|
|
104
|
+
analysisId: args.analysisId,
|
|
105
|
+
path: record.outputPath,
|
|
106
|
+
});
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
// An absent file is the normal condition of a prune that ran before, thus `force` treats it as done.
|
|
110
|
+
const removed = await tryFsWrite("record.rm", () => rm(resolved.absolute, { force: true }), { path: resolved.absolute });
|
|
111
|
+
if (removed.isErr()) {
|
|
112
|
+
args.logger.warn("an unused derivation did not go", { path: resolved.absolute, detail: describeFsError(removed.error) });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
50
116
|
/**
|
|
51
117
|
* Make the record tool over the session-state gateway, the version store, the thread store, and the
|
|
52
118
|
* resolver.
|
|
@@ -61,18 +127,27 @@ export function createRecordVersionTool(deps) {
|
|
|
61
127
|
description: "Record the current draft as one report version. The tool runs the whole gate first: it finishes the draft, " +
|
|
62
128
|
"resolves each reference, matches each chart encoding, and matches each assert. An incomplete draft gives back the gap list, " +
|
|
63
129
|
"and a failed reference gives back the block that broke. The tool records a version only after the eyes look at the current page. " +
|
|
64
|
-
"A thread holds one version, thus a
|
|
130
|
+
"A thread holds one version, thus a later record replaces it whole. Amend the draft, look at the page again, and record again. " +
|
|
131
|
+
"After the version lands, the tool removes the file of each derived table that no block of the recorded report binds.",
|
|
65
132
|
inputSchema: recordVersionInput,
|
|
66
133
|
executionMode: "inline",
|
|
67
134
|
describeCall: "none",
|
|
135
|
+
// The version id is the one durable product of the first record, thus the created arm names it. A
|
|
136
|
+
// later record writes over that same id, thus the line names the update and never reads as a
|
|
137
|
+
// refusal. Each other arm is a gate that refused, and the kind of the arm is what a watcher reads.
|
|
138
|
+
describeResult: (_input, result) => {
|
|
139
|
+
if (result.outcome !== "recorded")
|
|
140
|
+
return result.outcome;
|
|
141
|
+
return result.replaced ? "version updated" : `version ${result.versionId}`;
|
|
142
|
+
},
|
|
68
143
|
execute: async (_input, ctx) => {
|
|
69
144
|
const opened = await openReportThread(deps.gateway, ctx.session.scope);
|
|
70
145
|
if (opened.isErr()) {
|
|
71
146
|
return ok({ outcome: "refused", refusal: opened.error });
|
|
72
147
|
}
|
|
73
|
-
const { threadId, analysisId, state, seenDocumentHash } = opened.value;
|
|
148
|
+
const { threadId, analysisId, state, seenDocumentHash, derivations } = opened.value;
|
|
74
149
|
const { document: draft, snapshot } = state;
|
|
75
|
-
const finished = finishDraft(draft, snapshot);
|
|
150
|
+
const finished = finishDraft(draft, snapshot, derivations);
|
|
76
151
|
if (!finished.valid) {
|
|
77
152
|
return ok({ outcome: "gaps", gaps: finished.gaps });
|
|
78
153
|
}
|
|
@@ -127,10 +202,20 @@ export function createRecordVersionTool(deps) {
|
|
|
127
202
|
parentThreadId: thread.value.parentThreadId,
|
|
128
203
|
parentSeq: thread.value.parentSeq,
|
|
129
204
|
});
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
205
|
+
if (recorded.isOk()) {
|
|
206
|
+
// The version stands from here. The prune reclaims the bytes of each derivation that the
|
|
207
|
+
// recorded document ignores, and it decides nothing about the outcome. It runs on each
|
|
208
|
+
// record, thus an output that an amend unbound goes at the next record.
|
|
209
|
+
await pruneUnusedDerivations({
|
|
210
|
+
resolveWorkspaceRoot: deps.resolveWorkspaceRoot,
|
|
211
|
+
analysisId,
|
|
212
|
+
threadId,
|
|
213
|
+
document: finished.document,
|
|
214
|
+
derivations,
|
|
215
|
+
logger,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
return recorded.match((ref) => ok({ outcome: "recorded", versionId: ref.versionId, replaced: ref.outcome === "replaced" }), (error) => {
|
|
134
219
|
logger.warn("the version did not record", { threadId, analysisId, reason: error.type });
|
|
135
220
|
return ok({ outcome: "record-failed", detail: describeRecordFailure(error) });
|
|
136
221
|
});
|