@inflexa-ai/harness 0.20.0 → 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/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/mount-plan.d.ts +45 -7
- package/dist/sandbox/mount-plan.js +43 -5
- package/dist/sandbox/types.d.ts +6 -0
- 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/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/workspace/paths.d.ts +50 -0
- package/dist/workspace/paths.js +63 -0
- package/package.json +2 -1
|
@@ -7,17 +7,209 @@
|
|
|
7
7
|
*
|
|
8
8
|
* The artifact ledger holds one row for each path, and it keeps no history. Thus the set at a past
|
|
9
9
|
* moment is not recoverable later, and the pin must run at the anchor itself.
|
|
10
|
+
*
|
|
11
|
+
* The snapshot carries the citation evidence beside the artifact map. The pinned set is the literature
|
|
12
|
+
* that the synthesis engaged, and two fields of a run synthesis carry it: the key references and the
|
|
13
|
+
* references of each finding. The records live in the run tree on disk. Thus the pin reads that tree
|
|
14
|
+
* through the workspace-root seam of the caller.
|
|
15
|
+
*
|
|
16
|
+
* A reference that carries a short citation also pins a bibliography record beside its key. The key list
|
|
17
|
+
* keeps the membership role, thus the record map is optional and it changes no resolution.
|
|
10
18
|
*/
|
|
11
19
|
import { err, ok } from "neverthrow";
|
|
20
|
+
import { open } from "node:fs/promises";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
import { tryFs } from "../lib/fs-result.js";
|
|
12
23
|
import { queryAnalysisArtifacts } from "../state/artifacts.js";
|
|
24
|
+
import { queryRunsByAnalysis } from "../state/runs.js";
|
|
25
|
+
import { isSafeId, runDir } from "../workspace/paths.js";
|
|
26
|
+
/** The file name of the synthesis record inside the run directory. */
|
|
27
|
+
const SYNTHESIS_FILE = "synthesis.json";
|
|
28
|
+
/**
|
|
29
|
+
* The cap of one synthesis read. A synthesis record is small by construction, and the cap bounds the
|
|
30
|
+
* cost of a file that is not. A file over the cap gives no key, because a cut JSON text cannot parse.
|
|
31
|
+
*/
|
|
32
|
+
const SYNTHESIS_CAP_BYTES = 1024 * 1024;
|
|
33
|
+
/** The page size of the run listing. The listing walks the pages until a short page ends the walk. */
|
|
34
|
+
const RUN_PAGE_SIZE = 200;
|
|
35
|
+
/**
|
|
36
|
+
* Read a synthesis record under the cap, or give `undefined` when the bytes do not come back.
|
|
37
|
+
*
|
|
38
|
+
* The read takes a bounded byte window, thus a file of any size costs the cap. An absent file, a genuine
|
|
39
|
+
* read fault, and a file over the cap each give `undefined`, because the collection reports no error for
|
|
40
|
+
* one record.
|
|
41
|
+
*/
|
|
42
|
+
async function readSynthesisText(absolute) {
|
|
43
|
+
return tryFs("pinReportSnapshot.readSynthesis", async () => {
|
|
44
|
+
const handle = await open(absolute, "r");
|
|
45
|
+
try {
|
|
46
|
+
// The window takes one byte more than the cap. A read that fills it states that the file
|
|
47
|
+
// holds more bytes than the cap admits, thus the two conditions stay apart.
|
|
48
|
+
const buffer = Buffer.alloc(SYNTHESIS_CAP_BYTES + 1);
|
|
49
|
+
// One read of a network-backed file can give fewer bytes than the window holds. Such a
|
|
50
|
+
// short read cuts the text in the middle of the JSON, and the record then parses to no
|
|
51
|
+
// key at all. Thus the loop reads again from the offset that the last read reached, and
|
|
52
|
+
// it stops when the window fills or when a read gives no byte.
|
|
53
|
+
let filled = 0;
|
|
54
|
+
while (filled < buffer.length) {
|
|
55
|
+
const { bytesRead } = await handle.read(buffer, filled, buffer.length - filled, filled);
|
|
56
|
+
if (bytesRead === 0) {
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
filled += bytesRead;
|
|
60
|
+
}
|
|
61
|
+
return filled > SYNTHESIS_CAP_BYTES ? undefined : buffer.subarray(0, filled).toString("utf8");
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
await handle.close();
|
|
65
|
+
}
|
|
66
|
+
}, { path: absolute, onAbsent: () => undefined }).unwrapOr(undefined);
|
|
67
|
+
}
|
|
68
|
+
/** The trimmed text of a field, or `undefined` when the field is not a non-empty string. */
|
|
69
|
+
function trimmedText(value) {
|
|
70
|
+
if (typeof value !== "string") {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
const text = value.trim();
|
|
74
|
+
return text.length > 0 ? text : undefined;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The bibliography record of one reference entry, or `undefined` when the entry carries no citation text.
|
|
78
|
+
*
|
|
79
|
+
* A key reference carries `citation` and `description`. A reference of a finding carries `citation` and a
|
|
80
|
+
* narrower `relevance`, thus it gives the short citation alone. A record without a citation would state
|
|
81
|
+
* nothing, thus such an entry gives a bare key.
|
|
82
|
+
*/
|
|
83
|
+
function recordOf(reference) {
|
|
84
|
+
const citation = trimmedText(reference.citation);
|
|
85
|
+
if (citation === undefined) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
const description = trimmedText(reference.description);
|
|
89
|
+
return description === undefined ? { citation } : { citation, description };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Take one `pmid:` citation from each entry of one reference list, and add it to `entries`.
|
|
93
|
+
*
|
|
94
|
+
* The walk is lenient: a value that is not a list, an entry that is not an object, and a PMID that is
|
|
95
|
+
* not a non-empty string each give no citation.
|
|
96
|
+
*/
|
|
97
|
+
function pushPmidCitations(list, entries) {
|
|
98
|
+
if (!Array.isArray(list)) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
for (const reference of list) {
|
|
102
|
+
if (typeof reference !== "object" || reference === null) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const pmid = reference.pmid;
|
|
106
|
+
if (typeof pmid !== "string") {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const id = pmid.trim();
|
|
110
|
+
if (id.length > 0) {
|
|
111
|
+
const record = recordOf(reference);
|
|
112
|
+
entries.push(record === undefined ? { key: `pmid:${id}` } : { key: `pmid:${id}`, record });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
13
116
|
/**
|
|
14
|
-
*
|
|
117
|
+
* The citations of one synthesis text, in the order that the record carries them.
|
|
118
|
+
*
|
|
119
|
+
* The extraction is lenient: it parses the JSON, and it takes each PMID of `keyReferences` and of the
|
|
120
|
+
* references of each finding. The key references are the papers that the synthesis names as primary, and
|
|
121
|
+
* the references of the findings are the superset. Thus a citation over either one resolves. A
|
|
122
|
+
* whole-schema parse would empty the citation list of the whole analysis for one record that a different
|
|
123
|
+
* schema version wrote. Malformed JSON gives no citation.
|
|
124
|
+
*
|
|
125
|
+
* The key references come first, because the caller keeps the first record of a key and the curated
|
|
126
|
+
* description sits there.
|
|
127
|
+
*/
|
|
128
|
+
function citationsOf(text) {
|
|
129
|
+
let parsed;
|
|
130
|
+
try {
|
|
131
|
+
parsed = JSON.parse(text);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
const entries = [];
|
|
140
|
+
pushPmidCitations(parsed.keyReferences, entries);
|
|
141
|
+
const findings = parsed.findings;
|
|
142
|
+
if (Array.isArray(findings)) {
|
|
143
|
+
for (const finding of findings) {
|
|
144
|
+
if (typeof finding !== "object" || finding === null) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
pushPmidCitations(finding.references, entries);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return entries;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Collect the citation evidence of one analysis from the synthesis record of each of its runs.
|
|
154
|
+
*
|
|
155
|
+
* The pinned set is the literature that the synthesis engaged, and both fields of a record carry it: the
|
|
156
|
+
* key references and the references of each finding. The keys dedupe, and they sort in code-unit order.
|
|
157
|
+
* Thus one disk state gives one list, and a second pin over that state gives the same list.
|
|
158
|
+
*
|
|
159
|
+
* The first record of a key wins. The key references of a record walk before its findings, thus the
|
|
160
|
+
* curated description survives a duplicate that names the same paper with a narrower text.
|
|
161
|
+
*
|
|
162
|
+
* A run listing that fails fails the collection, because a store fault is not absence. Each other fault
|
|
163
|
+
* is a normal condition: an unresolvable workspace root, an absent record, an unreadable record, and a
|
|
164
|
+
* malformed record each give no key and no error.
|
|
165
|
+
*/
|
|
166
|
+
async function collectCitations(pool, resolveWorkspaceRoot, analysisId) {
|
|
167
|
+
let root;
|
|
168
|
+
try {
|
|
169
|
+
root = resolveWorkspaceRoot(analysisId);
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
// The seam signals an unresolvable resource by a throw. The artifact map still states the
|
|
173
|
+
// membership of the session, thus the fault costs the citation list alone.
|
|
174
|
+
return ok({ keys: [], records: new Map() });
|
|
175
|
+
}
|
|
176
|
+
const keys = new Set();
|
|
177
|
+
const records = new Map();
|
|
178
|
+
for (let offset = 0;; offset += RUN_PAGE_SIZE) {
|
|
179
|
+
const page = await queryRunsByAnalysis(pool, analysisId, { limit: RUN_PAGE_SIZE, offset });
|
|
180
|
+
if (page.isErr()) {
|
|
181
|
+
return err({ kind: "run-listing-failed", cause: page.error.cause });
|
|
182
|
+
}
|
|
183
|
+
for (const run of page.value) {
|
|
184
|
+
// The path builder refuses an id that can escape the run tree. A refusal is a throw, thus
|
|
185
|
+
// the guard keeps the collection on the value channel.
|
|
186
|
+
if (!isSafeId(run.runId)) {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const text = await readSynthesisText(join(root, runDir(run.runId), SYNTHESIS_FILE));
|
|
190
|
+
if (text === undefined) {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
for (const entry of citationsOf(text)) {
|
|
194
|
+
keys.add(entry.key);
|
|
195
|
+
if (entry.record !== undefined && !records.has(entry.key)) {
|
|
196
|
+
records.set(entry.key, entry.record);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (page.value.length < RUN_PAGE_SIZE) {
|
|
201
|
+
return ok({ keys: [...keys].sort(), records });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Pin the snapshot of one analysis from the artifact ledger and from the synthesis records of its runs.
|
|
15
207
|
*
|
|
16
208
|
* The pin copies no cell and no byte. An entry pins identity alone, thus the snapshot grows with the
|
|
17
209
|
* count of artifacts and never with the size of the data. A read of an artifact belongs to the value
|
|
18
210
|
* tier, which runs one time for each report version.
|
|
19
211
|
*/
|
|
20
|
-
export async function pinReportSnapshot(pool, analysisId) {
|
|
212
|
+
export async function pinReportSnapshot(pool, analysisId, options = {}) {
|
|
21
213
|
let ledgerRows;
|
|
22
214
|
try {
|
|
23
215
|
ledgerRows = await queryAnalysisArtifacts(pool, analysisId);
|
|
@@ -33,8 +225,14 @@ export async function pinReportSnapshot(pool, analysisId) {
|
|
|
33
225
|
for (const row of ledgerRows) {
|
|
34
226
|
artifacts[row.path] = { hash: row.hash, fileType: row.fileType };
|
|
35
227
|
}
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
|
|
228
|
+
// A citation reference resolves against this list: the resolver refuses a key that the list does not
|
|
229
|
+
// hold. Thus the pin must state which external ids the analysis engaged, and a session with an empty
|
|
230
|
+
// list can bind no citation block.
|
|
231
|
+
if (options.resolveWorkspaceRoot === undefined) {
|
|
232
|
+
return ok({ artifacts, citations: [] });
|
|
233
|
+
}
|
|
234
|
+
const citations = await collectCitations(pool, options.resolveWorkspaceRoot, analysisId);
|
|
235
|
+
// A collection that recorded nothing stores no map. Thus the snapshot of an analysis whose synthesis
|
|
236
|
+
// carries no citation text reads the same as a pin that predates the map.
|
|
237
|
+
return citations.map(({ keys, records }) => records.size === 0 ? { artifacts, citations: keys } : { artifacts, citations: keys, citationRecords: Object.fromEntries(records) });
|
|
40
238
|
}
|
|
@@ -25,7 +25,7 @@ import { allWithConcurrency } from "../lib/async-utils.js";
|
|
|
25
25
|
import { computeSha256File } from "../lib/fs-helpers.js";
|
|
26
26
|
import { resolveWorkspacePath } from "../workspace/paths.js";
|
|
27
27
|
import { asFiniteNumber, cellMatchesFilterValue, checkCitationAssertion, checkValueAssertion } from "./assert-rules.js";
|
|
28
|
-
import { columnsHeldByNoRow, fileTypeHoldsNoCell, snapshotEntry, } from "./reference-resolver.js";
|
|
28
|
+
import { applyRowBound, columnsHeldByNoRow, fileTypeHoldsNoCell, snapshotEntry, } from "./reference-resolver.js";
|
|
29
29
|
/**
|
|
30
30
|
* The default cap on the file size that the host fast path reads. A larger file takes the fall-through.
|
|
31
31
|
* The cap gates the cell extraction only. The identity compare streams the hash at every size.
|
|
@@ -623,10 +623,21 @@ export function createProductionResolver(deps) {
|
|
|
623
623
|
if (gated.isErr()) {
|
|
624
624
|
return err(gated.error);
|
|
625
625
|
}
|
|
626
|
-
|
|
626
|
+
// The bound ranks the rows as the artifact holds them, thus a bound over a column that the subset
|
|
627
|
+
// below leaves out still ranks them. An unknown bound column addresses nothing, exactly as an
|
|
628
|
+
// unknown subset column does.
|
|
629
|
+
const bound = reference.rowBound;
|
|
630
|
+
const allRows = gated.value;
|
|
631
|
+
if (bound !== undefined && columnsHeldByNoRow(allRows, [bound.column]).length > 0) {
|
|
632
|
+
return fail(reference, "locator-out-of-range", `the row bound names column ${bound.column}, which the table at ${reference.path} does not hold`);
|
|
633
|
+
}
|
|
634
|
+
const rows = bound === undefined ? allRows : applyRowBound(allRows, bound);
|
|
635
|
+
// The page states the shown count against the count of the artifact. This read is the one place that
|
|
636
|
+
// holds both, thus a bounded table carries the pre-bound total out with its rows.
|
|
637
|
+
const total = bound === undefined ? {} : { total: allRows.length };
|
|
627
638
|
const columns = reference.columns;
|
|
628
639
|
if (columns === undefined) {
|
|
629
|
-
return ok({ type: "table", rows });
|
|
640
|
+
return ok({ type: "table", rows, ...total });
|
|
630
641
|
}
|
|
631
642
|
// A name that no row holds addresses nothing. Without this check a projection onto an invented
|
|
632
643
|
// column gives an empty cell for each row and still resolves, which grounding exists to reject.
|
|
@@ -645,7 +656,7 @@ export function createProductionResolver(deps) {
|
|
|
645
656
|
}
|
|
646
657
|
return projected;
|
|
647
658
|
});
|
|
648
|
-
return ok({ type: "table", rows: projectedRows, columns });
|
|
659
|
+
return ok({ type: "table", rows: projectedRows, columns, ...total });
|
|
649
660
|
}
|
|
650
661
|
async function resolveArtifactFile(reference, snapshot) {
|
|
651
662
|
const identity = await getIdentity(reference.path, snapshot);
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* and a fixture realization can read an in-memory snapshot. The two obey the same contract.
|
|
7
7
|
*/
|
|
8
8
|
import type { Result } from "neverthrow";
|
|
9
|
-
import type { Reference, UnresolvedReference } from "../contracts/report-reference.js";
|
|
9
|
+
import type { Reference, RowBound, UnresolvedReference } from "../contracts/report-reference.js";
|
|
10
10
|
/**
|
|
11
11
|
* One pinned artifact: its content hash, and its rows as plain cells.
|
|
12
12
|
*
|
|
@@ -31,20 +31,39 @@ export interface ArtifactSnapshot {
|
|
|
31
31
|
fileType?: string | null;
|
|
32
32
|
rows?: Array<Record<string, string | number>>;
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* One pinned citation record: the short citation of the paper, and the description that the synthesis
|
|
36
|
+
* gave. The record is the bibliography of a citation key, and the key alone states the membership.
|
|
37
|
+
*/
|
|
38
|
+
export interface CitationRecord {
|
|
39
|
+
citation: string;
|
|
40
|
+
description?: string;
|
|
41
|
+
}
|
|
42
|
+
/** The pinned citation records, keyed by the citation key, for example `pmid:12345`. */
|
|
43
|
+
export type CitationRecords = Record<string, CitationRecord>;
|
|
34
44
|
/**
|
|
35
45
|
* The pinned evidence that a resolver reads. The `artifacts` map is keyed by the analysis-relative path,
|
|
36
46
|
* the same path that an artifact reference names and the same key as `cortex_artifacts.path`. That path
|
|
37
47
|
* is unique across the analysis, thus one map holds the artifacts of every run without a collision.
|
|
38
48
|
* `citations` holds each known external id as an `idKind:id` string, for example `pmid:12345`.
|
|
49
|
+
*
|
|
50
|
+
* `citationRecords` carries the bibliography of a key that the synthesis described. The key list keeps the
|
|
51
|
+
* membership role, thus a key with no record is a pinned citation and a stored pin with no map reads the
|
|
52
|
+
* same as one that predates the map.
|
|
39
53
|
*/
|
|
40
54
|
export interface ReportSnapshot {
|
|
41
55
|
artifacts: Record<string, ArtifactSnapshot>;
|
|
42
56
|
citations?: string[];
|
|
57
|
+
citationRecords?: CitationRecords;
|
|
43
58
|
}
|
|
44
59
|
/**
|
|
45
60
|
* The concrete value that a reference resolves to. A scalar comes from one artifact cell or one
|
|
46
61
|
* derivation. A table comes from a whole-table artifact. A file echoes the pin of a whole-file artifact,
|
|
47
62
|
* for example an image. A citation echo confirms that an external id is in the pinned evidence.
|
|
63
|
+
*
|
|
64
|
+
* A table carries `total` when a row bound cut the rows. The resolution is the one step that reads the
|
|
65
|
+
* whole artifact, thus it is the one step that knows the count before the cut. A table with no bound
|
|
66
|
+
* carries none, and the row count answers for it.
|
|
48
67
|
*/
|
|
49
68
|
export type ResolvedValue = {
|
|
50
69
|
type: "scalar";
|
|
@@ -53,6 +72,7 @@ export type ResolvedValue = {
|
|
|
53
72
|
type: "table";
|
|
54
73
|
rows: Array<Record<string, string | number>>;
|
|
55
74
|
columns?: string[];
|
|
75
|
+
total?: number;
|
|
56
76
|
} | {
|
|
57
77
|
type: "file";
|
|
58
78
|
path: string;
|
|
@@ -92,6 +112,23 @@ export interface ReferenceResolver {
|
|
|
92
112
|
* result table is a real scientific outcome that must not fail for the shape of its emptiness.
|
|
93
113
|
*/
|
|
94
114
|
export declare function columnsHeldByNoRow(rows: Array<Record<string, string | number>>, columns: readonly string[]): string[];
|
|
115
|
+
/**
|
|
116
|
+
* Apply a row bound to a table: rank the rows by the bound column, and keep the first `count` of them.
|
|
117
|
+
*
|
|
118
|
+
* The kind of the column decides one time, over the whole column, and never for one pair. A column is
|
|
119
|
+
* numeric when one cell of it reads as a finite number. A text-backed artifact holds every cell as a
|
|
120
|
+
* string, thus the parse is what tells a measurement from a label, and a code-unit order would put `"10"`
|
|
121
|
+
* between `"1"` and `"2"`. A column where nothing parses ranks by the code-unit order of its text.
|
|
122
|
+
*
|
|
123
|
+
* The sort is stable by construction. The walk carries the source index of each row and it breaks a tie on
|
|
124
|
+
* that index, thus two rows of equal rank keep the order of the file and two runs over one table give one
|
|
125
|
+
* result. `Array.prototype.sort` is specified as stable in the current language, and the explicit tiebreak
|
|
126
|
+
* states the property at the site that depends on it.
|
|
127
|
+
*
|
|
128
|
+
* The bound reads the rows as they arrive. A caller that projects onto a column subset projects after the
|
|
129
|
+
* bound, thus a bound over a column that the subset leaves out still ranks the rows.
|
|
130
|
+
*/
|
|
131
|
+
export declare function applyRowBound(rows: Array<Record<string, string | number>>, bound: RowBound): Array<Record<string, string | number>>;
|
|
95
132
|
/**
|
|
96
133
|
* Give back `true` for a file type that holds no cell for a reference to address.
|
|
97
134
|
*
|
|
@@ -117,3 +154,15 @@ export declare function fileTypeHoldsNoCell(fileType: string | null | undefined)
|
|
|
117
154
|
* disagree about membership.
|
|
118
155
|
*/
|
|
119
156
|
export declare function snapshotEntry(snapshot: ReportSnapshot, path: string): ArtifactSnapshot | undefined;
|
|
157
|
+
/**
|
|
158
|
+
* Give back the citation record at `key`, or `undefined` when the map holds no such own key.
|
|
159
|
+
*
|
|
160
|
+
* An agent authors the citation key of a block, thus the key is untrusted text. The map can be a plain
|
|
161
|
+
* object from a `JSON.parse` of a stored snapshot, and a bracket lookup with a key such as `constructor`
|
|
162
|
+
* finds an inherited member. The `Object.hasOwn` guard admits an own key only, thus an inherited member
|
|
163
|
+
* reads as absent.
|
|
164
|
+
*
|
|
165
|
+
* The card and the appendix both read this one lookup. Thus the two surfaces can never disagree about the
|
|
166
|
+
* bibliography of one key.
|
|
167
|
+
*/
|
|
168
|
+
export declare function citationRecordOf(records: CitationRecords | undefined, key: string): CitationRecord | undefined;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* belief points at. The harness declares the seam here, thus a production realization can read storage,
|
|
6
6
|
* and a fixture realization can read an in-memory snapshot. The two obey the same contract.
|
|
7
7
|
*/
|
|
8
|
+
import { asFiniteNumber } from "./assert-rules.js";
|
|
8
9
|
/**
|
|
9
10
|
* Give back each name in `columns` that no row holds, in the order of `columns`.
|
|
10
11
|
*
|
|
@@ -19,7 +20,85 @@ export function columnsHeldByNoRow(rows, columns) {
|
|
|
19
20
|
if (rows.length === 0) {
|
|
20
21
|
return [];
|
|
21
22
|
}
|
|
22
|
-
|
|
23
|
+
// An agent authors a column name, thus the name is untrusted text. A row can be a plain object from a
|
|
24
|
+
// `JSON.parse` of a stored snapshot, and `in` finds an inherited member such as `constructor`. The name
|
|
25
|
+
// would then read as a column, and the read of the cell would give a function. The own-key test admits
|
|
26
|
+
// a real column alone.
|
|
27
|
+
return columns.filter((column) => !rows.some((row) => Object.hasOwn(row, column)));
|
|
28
|
+
}
|
|
29
|
+
/** The cell of one row under an own key, or `undefined` when the row holds no such column. */
|
|
30
|
+
function cellOf(row, column) {
|
|
31
|
+
return Object.hasOwn(row, column) ? row[column] : undefined;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The code-unit rank of the string form of two cells.
|
|
35
|
+
*
|
|
36
|
+
* The comparison never calls `localeCompare`, because the ICU of a host decides that order and the bounded
|
|
37
|
+
* rows reach the bytes of the data asset. Thus one table gives one payload and one asset name on every
|
|
38
|
+
* host.
|
|
39
|
+
*/
|
|
40
|
+
function rankText(left, right) {
|
|
41
|
+
const leftText = String(left);
|
|
42
|
+
const rightText = String(right);
|
|
43
|
+
if (leftText < rightText)
|
|
44
|
+
return -1;
|
|
45
|
+
if (leftText > rightText)
|
|
46
|
+
return 1;
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* A row that holds no rank in the column, which the bound keeps last in either direction.
|
|
51
|
+
*
|
|
52
|
+
* A row that the column does not reach carries no value to rank. In a numeric column a cell that reads as
|
|
53
|
+
* no number carries none either: a sentinel such as `NA` is the absence of a measurement, and a rank for it
|
|
54
|
+
* against a real value would be invented. Thus a descending bound of the top 20 rows gives 20 measurements,
|
|
55
|
+
* and never 20 sentinels.
|
|
56
|
+
*/
|
|
57
|
+
function holdsNoRank(entry, numeric) {
|
|
58
|
+
return entry.cell === undefined || (numeric && entry.value === undefined);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The rank of two rows that both hold one. A numeric column compares the parsed numbers, and a column
|
|
62
|
+
* where nothing parses compares the code-unit order of the text.
|
|
63
|
+
*/
|
|
64
|
+
function rankRankable(left, right, numeric) {
|
|
65
|
+
if (numeric && left.value !== undefined && right.value !== undefined) {
|
|
66
|
+
return left.value - right.value;
|
|
67
|
+
}
|
|
68
|
+
return rankText(left.cell, right.cell);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Apply a row bound to a table: rank the rows by the bound column, and keep the first `count` of them.
|
|
72
|
+
*
|
|
73
|
+
* The kind of the column decides one time, over the whole column, and never for one pair. A column is
|
|
74
|
+
* numeric when one cell of it reads as a finite number. A text-backed artifact holds every cell as a
|
|
75
|
+
* string, thus the parse is what tells a measurement from a label, and a code-unit order would put `"10"`
|
|
76
|
+
* between `"1"` and `"2"`. A column where nothing parses ranks by the code-unit order of its text.
|
|
77
|
+
*
|
|
78
|
+
* The sort is stable by construction. The walk carries the source index of each row and it breaks a tie on
|
|
79
|
+
* that index, thus two rows of equal rank keep the order of the file and two runs over one table give one
|
|
80
|
+
* result. `Array.prototype.sort` is specified as stable in the current language, and the explicit tiebreak
|
|
81
|
+
* states the property at the site that depends on it.
|
|
82
|
+
*
|
|
83
|
+
* The bound reads the rows as they arrive. A caller that projects onto a column subset projects after the
|
|
84
|
+
* bound, thus a bound over a column that the subset leaves out still ranks the rows.
|
|
85
|
+
*/
|
|
86
|
+
export function applyRowBound(rows, bound) {
|
|
87
|
+
const descending = (bound.order ?? "desc") === "desc";
|
|
88
|
+
const ranked = rows.map((row, index) => {
|
|
89
|
+
const cell = cellOf(row, bound.column);
|
|
90
|
+
return { row, index, cell, value: cell === undefined ? undefined : asFiniteNumber(cell) };
|
|
91
|
+
});
|
|
92
|
+
const numeric = ranked.some((entry) => entry.value !== undefined);
|
|
93
|
+
// A row with no rank stays behind every ranked row, and the direction never lifts it. The two lists
|
|
94
|
+
// keep their source order, thus the tail is as stable as the head.
|
|
95
|
+
const rankable = ranked.filter((entry) => !holdsNoRank(entry, numeric));
|
|
96
|
+
const unrankable = ranked.filter((entry) => holdsNoRank(entry, numeric));
|
|
97
|
+
const sorted = [...rankable].sort((left, right) => {
|
|
98
|
+
const rank = rankRankable(left, right, numeric);
|
|
99
|
+
return rank === 0 ? left.index - right.index : descending ? -rank : rank;
|
|
100
|
+
});
|
|
101
|
+
return [...sorted, ...unrankable].slice(0, bound.count).map((entry) => entry.row);
|
|
23
102
|
}
|
|
24
103
|
/**
|
|
25
104
|
* The file type of an entry that holds no cell for a reference to address.
|
|
@@ -58,3 +137,17 @@ export function fileTypeHoldsNoCell(fileType) {
|
|
|
58
137
|
export function snapshotEntry(snapshot, path) {
|
|
59
138
|
return Object.hasOwn(snapshot.artifacts, path) ? snapshot.artifacts[path] : undefined;
|
|
60
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Give back the citation record at `key`, or `undefined` when the map holds no such own key.
|
|
142
|
+
*
|
|
143
|
+
* An agent authors the citation key of a block, thus the key is untrusted text. The map can be a plain
|
|
144
|
+
* object from a `JSON.parse` of a stored snapshot, and a bracket lookup with a key such as `constructor`
|
|
145
|
+
* finds an inherited member. The `Object.hasOwn` guard admits an own key only, thus an inherited member
|
|
146
|
+
* reads as absent.
|
|
147
|
+
*
|
|
148
|
+
* The card and the appendix both read this one lookup. Thus the two surfaces can never disagree about the
|
|
149
|
+
* bibliography of one key.
|
|
150
|
+
*/
|
|
151
|
+
export function citationRecordOf(records, key) {
|
|
152
|
+
return records !== undefined && Object.hasOwn(records, key) ? records[key] : undefined;
|
|
153
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The hash stamp of the land path, and the hash elision of the read.
|
|
3
|
+
*
|
|
4
|
+
* An artifact reference pins a path and a content hash. The pinned snapshot already maps each path onto
|
|
5
|
+
* its hash, thus the author of a block gives the path alone. The stamp fills the hash from the snapshot,
|
|
6
|
+
* and a reference that names the path lands.
|
|
7
|
+
*
|
|
8
|
+
* The stamp also drops a hash that a payload carries. The snapshot owns the value. Thus an echoed binding
|
|
9
|
+
* and a mistyped one both land with the hash of the snapshot entry, and the structural tier reads no stale
|
|
10
|
+
* pin on the land path.
|
|
11
|
+
*
|
|
12
|
+
* The walk is structural, because it runs before the grammar parse. It reads the artifact-reference shape
|
|
13
|
+
* alone: a pinned kind and a string path. Every other value passes through untouched, thus a nested block,
|
|
14
|
+
* a derivation input, and a plain field are all safe.
|
|
15
|
+
*
|
|
16
|
+
* A reference whose path the snapshot does not hold refuses here. Without this arm the parse refuses the
|
|
17
|
+
* absent hash, and that message sends the author to the wrong repair.
|
|
18
|
+
*
|
|
19
|
+
* The elision walks the same shape in the other direction: a read of a landed block gives each binding
|
|
20
|
+
* with no hash. Thus one module holds both directions, and the two agree on which object carries a pin.
|
|
21
|
+
*/
|
|
22
|
+
import { type Result } from "neverthrow";
|
|
23
|
+
import type { DraftRefusal } from "./draft-operations.js";
|
|
24
|
+
import { type ReportSnapshot } from "./reference-resolver.js";
|
|
25
|
+
/**
|
|
26
|
+
* Stamp the hash of each artifact reference in a block payload, from the snapshot entry at its path.
|
|
27
|
+
*
|
|
28
|
+
* The membership read is `snapshotEntry`, because the path is author text. An own key alone counts, thus
|
|
29
|
+
* an inherited member of a plain object reads as absent and the stamp agrees with the structural tier.
|
|
30
|
+
*
|
|
31
|
+
* The whole call refuses when one reference names a path that the snapshot does not hold. A partial stamp
|
|
32
|
+
* beside a refusal would land nothing, thus the refusal carries every unknown path at one time and the
|
|
33
|
+
* author repairs them together.
|
|
34
|
+
*/
|
|
35
|
+
export declare function stampReferenceHashes(payload: unknown, snapshot: ReportSnapshot): Result<unknown, DraftRefusal>;
|
|
36
|
+
/**
|
|
37
|
+
* Give one value with the stamped hash of each artifact reference removed.
|
|
38
|
+
*
|
|
39
|
+
* The snapshot owns the hash, thus a read that echoed one would give an agent a value to mistype. A binding
|
|
40
|
+
* still gives its path and its locator, which are what an agent authored and what it changes.
|
|
41
|
+
*
|
|
42
|
+
* The walk builds a copy on the changed path only. A branch that carries no pin comes back as the same
|
|
43
|
+
* reference. Thus the read costs the size of the pins, and never the size of the block.
|
|
44
|
+
*/
|
|
45
|
+
export declare function stripReferenceHashes(value: unknown): unknown;
|