@inflexa-ai/harness 0.19.1 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/agents/report-session-agent.d.ts +7 -0
  2. package/dist/agents/report-session-agent.js +2 -1
  3. package/dist/auth/local-run-authorizer.d.ts +3 -2
  4. package/dist/auth/local-run-authorizer.js +4 -2
  5. package/dist/execution/run-authorizer.d.ts +18 -0
  6. package/dist/execution/run-canceler.d.ts +67 -0
  7. package/dist/execution/run-canceler.js +130 -0
  8. package/dist/index.d.ts +5 -2
  9. package/dist/index.js +6 -1
  10. package/dist/literature/sources/pubmed.js +2 -1
  11. package/dist/report-render/assets.d.ts +42 -0
  12. package/dist/report-render/assets.js +61 -0
  13. package/dist/report-render/chart.js +31 -10
  14. package/dist/report-render/design.d.ts +120 -0
  15. package/dist/report-render/design.js +808 -0
  16. package/dist/report-render/page.d.ts +32 -115
  17. package/dist/report-render/page.js +130 -273
  18. package/dist/report-render/render.js +34 -11
  19. package/dist/report-render/views/chart-view.d.ts +6 -3
  20. package/dist/report-render/views/chart-view.js +10 -5
  21. package/dist/report-render/views/page-view.d.ts +19 -8
  22. package/dist/report-render/views/page-view.js +31 -11
  23. package/dist/report-render/views/prose.d.ts +1 -0
  24. package/dist/report-render/views/prose.js +7 -13
  25. package/dist/report-render/views/references-view.js +5 -5
  26. package/dist/report-render/views/values.d.ts +15 -6
  27. package/dist/report-render/views/values.js +29 -10
  28. package/dist/runtime/assemble.d.ts +10 -0
  29. package/dist/runtime/assemble.js +1 -0
  30. package/dist/sandbox/k8s-client.js +12 -1
  31. package/dist/sandbox/types.d.ts +4 -0
  32. package/dist/state/analyses.d.ts +3 -26
  33. package/dist/state/analyses.js +6 -64
  34. package/dist/state/index.d.ts +2 -2
  35. package/dist/state/index.js +2 -2
  36. package/dist/state/init.js +3 -3
  37. package/dist/state/runs.d.ts +10 -0
  38. package/dist/state/runs.js +21 -0
  39. package/dist/tasks/data-profile.d.ts +7 -0
  40. package/dist/tasks/data-profile.js +14 -0
  41. package/dist/tools/approval/gateway.d.ts +8 -1
  42. package/dist/tools/approval/gateway.js +9 -2
  43. package/dist/tools/approval/queries.d.ts +9 -2
  44. package/dist/tools/approval/queries.js +12 -4
  45. package/dist/tools/report-session/index.d.ts +1 -1
  46. package/dist/tools/report-session/index.js +1 -1
  47. package/dist/tools/report-session/preview-report.d.ts +11 -1
  48. package/dist/tools/report-session/preview-report.js +35 -12
  49. package/dist/workflows/sandbox-step.d.ts +7 -0
  50. package/dist/workflows/sandbox-step.js +19 -2
  51. package/package.json +5 -1
@@ -30,6 +30,7 @@ import type { ThreadStore } from "../memory/thread-store.js";
30
30
  import type { ReferenceResolver } from "../report-model/reference-resolver.js";
31
31
  import type { ReportVersionStore } from "../state/report-versions.js";
32
32
  import type { ReportSessionStateGateway } from "../tools/report-authoring/authoring-tools.js";
33
+ import { type ResolvePageAsset } from "../tools/report-session/index.js";
33
34
  /** Canonical agent id -- distinct from the id of the old templating agent. */
34
35
  export declare const REPORT_SESSION_AGENT_ID: "report-session";
35
36
  /**
@@ -65,6 +66,12 @@ export interface ReportSessionAgentDeps {
65
66
  analysisId: string;
66
67
  auth: AuthContext;
67
68
  }) => ReferenceResolver;
69
+ /**
70
+ * Page-asset lookup -- the preview tool maps the module specifier of a manifest
71
+ * entry onto a file on disk. Omitted falls back to the module resolution of the
72
+ * installation of the harness.
73
+ */
74
+ readonly resolvePageAsset?: ResolvePageAsset;
68
75
  /** Operational logging seam; omitted falls back to no-op. */
69
76
  readonly logger?: Logger;
70
77
  }
@@ -33,7 +33,7 @@ export const REPORT_SESSION_AGENT_ID = "report-session";
33
33
  const REPORT_SESSION_MAX_ITERATIONS = 50;
34
34
  /** Build the report `AgentDefinition` with every tool bound to its deps. */
35
35
  export function createReportSessionAgent(deps) {
36
- const { model, pool, embedding, workspaceFs, gateway, resolveWorkspaceRoot, store, threads, chrome, makeResolver, logger } = deps;
36
+ const { model, pool, embedding, workspaceFs, gateway, resolveWorkspaceRoot, store, threads, chrome, makeResolver, resolvePageAsset, logger } = deps;
37
37
  const authoring = createReportAuthoringTools(gateway);
38
38
  const tools = [
39
39
  // The read surface toward the analysis. It roams the tree read-only.
@@ -59,6 +59,7 @@ export function createReportSessionAgent(deps) {
59
59
  gateway,
60
60
  resolveWorkspaceRoot,
61
61
  ...(makeResolver ? { makeResolver } : {}),
62
+ ...(resolvePageAsset ? { resolvePageAsset } : {}),
62
63
  ...(logger ? { logger } : {}),
63
64
  }),
64
65
  // The eyes tool. It opens the rendered page in headless Chrome.
@@ -2,8 +2,9 @@
2
2
  * LocalRunAuthorizer — the OSS realization of the `RunAuthorizer` seam.
3
3
  *
4
4
  * Issues a durable `RunSession` straight from the async-edge input: no remote
5
- * authorization, no credential read. It owns nothing revocable, so `revoke` is
6
- * a no-op. The opaque caller `auth` is forwarded untouched — local code never
5
+ * authorization, no credential read. It owns nothing revocable (it mints no
6
+ * jti), so `revoke` and `revokeByJti` are no-ops. The opaque caller `auth` is
7
+ * forwarded untouched — local code never
7
8
  * inspects it (no `getAuth`).
8
9
  */
9
10
  import type { RunAuthorizer } from "../execution/run-authorizer.js";
@@ -2,8 +2,9 @@
2
2
  * LocalRunAuthorizer — the OSS realization of the `RunAuthorizer` seam.
3
3
  *
4
4
  * Issues a durable `RunSession` straight from the async-edge input: no remote
5
- * authorization, no credential read. It owns nothing revocable, so `revoke` is
6
- * a no-op. The opaque caller `auth` is forwarded untouched — local code never
5
+ * authorization, no credential read. It owns nothing revocable (it mints no
6
+ * jti), so `revoke` and `revokeByJti` are no-ops. The opaque caller `auth` is
7
+ * forwarded untouched — local code never
7
8
  * inspects it (no `getAuth`).
8
9
  */
9
10
  export function createLocalRunAuthorizer() {
@@ -19,5 +20,6 @@ export function createLocalRunAuthorizer() {
19
20
  return { runSession, ownsMandate: false }; // oss-core-managed-ok
20
21
  },
21
22
  async revoke() { },
23
+ async revokeByJti() { },
22
24
  };
23
25
  }
@@ -15,6 +15,16 @@
15
15
  * realization injected at the composition root.
16
16
  */
17
17
  import type { AuthContext, Provenance, RunFrame, RunSession, Scope } from "../auth/types.js";
18
+ /**
19
+ * Locates a run credential when no `RunAuthorization` exists: the persisted
20
+ * jti from `cortex_runs` plus the revoking caller's opaque `auth` — the sole
21
+ * carrier of credential/org behind a session, downcast only by managed
22
+ * realizations.
23
+ */
24
+ export interface RevokeByJtiRef {
25
+ readonly jti: string;
26
+ readonly auth: AuthContext;
27
+ }
18
28
  /** What an async edge knows when it authorizes a run: the opaque caller auth,
19
29
  * the scope + provenance to stamp on the run, and the run frame to mint for. */
20
30
  export interface AuthorizeRunInput {
@@ -40,4 +50,12 @@ export interface RunAuthorization {
40
50
  export interface RunAuthorizer {
41
51
  authorize(input: AuthorizeRunInput): Promise<RunAuthorization>;
42
52
  revoke(authorization: RunAuthorization, reason: string): Promise<void>;
53
+ /**
54
+ * Out-of-band revocation for paths holding no `RunAuthorization`. Normal
55
+ * terminal paths use `revoke(authorization, reason)` under the run
56
+ * credential; `revokeByJti` covers external cancellation, where the run
57
+ * credential's JWT was never persisted and only its jti survives on
58
+ * `cortex_runs`. Local/OSS authorizers no-op — they mint no jti.
59
+ */
60
+ revokeByJti(ref: RevokeByJtiRef, reason: string): Promise<void>;
43
61
  }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * RunCanceler — the encapsulated external-cancel path for an `executeAnalysis` run.
3
+ *
4
+ * A cancelled DBOS workflow can never execute another step, so the body's own
5
+ * terminal block (`collectAndComplete`) is unreachable on external cancel and
6
+ * nothing converges: the run row stays `running` (blocking same-plan relaunch
7
+ * through `queryActiveRun`'s active-status dedup), pending step rows never
8
+ * sweep, the running charge never closes, and the run mandate is never
9
+ * revoked. This module is the host-side counterpart: cancel the engine's
10
+ * parent + children, then converge each ledger the workflow would have.
11
+ *
12
+ * The engine capability stays internal — the production default is the
13
+ * purger's `DBOSClient`-backed child-cascading cancel, which needs no launched
14
+ * engine (see `dbos-workflow-purger.ts` for why the static `DBOS` facade is
15
+ * the wrong tool here). `cancelWorkflows` is an optional test seam over it.
16
+ *
17
+ * No stream writes: `DBOS.writeStream` is body-only, so hosts synthesize the
18
+ * terminal frame read-side.
19
+ */
20
+ import type { Pool } from "pg";
21
+ import type { AgentSession } from "../auth/types.js";
22
+ import type { RunCharge } from "../billing/run-charge.js";
23
+ import type { Logger } from "../lib/logger.js";
24
+ import type { RunStatus } from "../state/schema.js";
25
+ import type { RunAuthorizer } from "./run-authorizer.js";
26
+ export declare class UnknownRunError extends Error {
27
+ readonly runId: string;
28
+ constructor(runId: string);
29
+ }
30
+ export interface CancelRunResult {
31
+ readonly runId: string;
32
+ /** The DBOS parent workflow id — equal to `runId` by the launch contract
33
+ * (`execute_analysis` launches with `workflowId: runId`). */
34
+ readonly workflowId: string;
35
+ readonly outcome: "canceled" | "already_terminal";
36
+ /**
37
+ * The run row's status after the cancel. `canceled` when this cancel won
38
+ * the row; the run's own terminal status when it completed concurrently
39
+ * (the conditional write refuses to clobber it).
40
+ */
41
+ readonly finalStatus: RunStatus;
42
+ /** What actually converged. `mandate` is vacuously true for a row with no
43
+ * persisted jti — nothing to revoke, so hosts alert on any false flag. */
44
+ readonly converged: {
45
+ readonly steps: boolean;
46
+ readonly charge: boolean;
47
+ readonly mandate: boolean;
48
+ };
49
+ }
50
+ export interface RunCancelerDeps {
51
+ readonly pool: Pool;
52
+ readonly runCharge: RunCharge;
53
+ readonly runAuthorizer: RunAuthorizer;
54
+ readonly logger?: Logger;
55
+ /** Test seam over the engine cancel. Production default: the workflow
56
+ * purger's `DBOSClient`-backed cancel, child-cascading and launch-free. */
57
+ readonly cancelWorkflows?: (workflowIds: readonly string[]) => Promise<void>;
58
+ }
59
+ export interface RunCanceler {
60
+ /**
61
+ * Cancel the run's workflows and converge its ledgers. Rejects when the
62
+ * engine cancel itself fails (retry-safe: nothing converged) or the run is
63
+ * unknown; convergence-phase failures resolve with their flag false.
64
+ */
65
+ cancel(runId: string, session: AgentSession): Promise<CancelRunResult>;
66
+ }
67
+ export declare function createRunCanceler(deps: RunCancelerDeps): RunCanceler;
@@ -0,0 +1,130 @@
1
+ /**
2
+ * RunCanceler — the encapsulated external-cancel path for an `executeAnalysis` run.
3
+ *
4
+ * A cancelled DBOS workflow can never execute another step, so the body's own
5
+ * terminal block (`collectAndComplete`) is unreachable on external cancel and
6
+ * nothing converges: the run row stays `running` (blocking same-plan relaunch
7
+ * through `queryActiveRun`'s active-status dedup), pending step rows never
8
+ * sweep, the running charge never closes, and the run mandate is never
9
+ * revoked. This module is the host-side counterpart: cancel the engine's
10
+ * parent + children, then converge each ledger the workflow would have.
11
+ *
12
+ * The engine capability stays internal — the production default is the
13
+ * purger's `DBOSClient`-backed child-cascading cancel, which needs no launched
14
+ * engine (see `dbos-workflow-purger.ts` for why the static `DBOS` facade is
15
+ * the wrong tool here). `cancelWorkflows` is an optional test seam over it.
16
+ *
17
+ * No stream writes: `DBOS.writeStream` is body-only, so hosts synthesize the
18
+ * terminal frame read-side.
19
+ */
20
+ import { createNoopLogger } from "../lib/console-logger.js";
21
+ import { unwrapOrThrow } from "../lib/result.js";
22
+ import { markRunCanceledIfActive, queryRun } from "../state/runs.js";
23
+ import { queryStepsByRun, sweepPendingStepExecutions } from "../state/step-executions.js";
24
+ import { createDbosWorkflowPurger } from "./dbos-workflow-purger.js";
25
+ /** The reason stamped on the run row and the revoke — the same literal
26
+ * `collectAndComplete` records for a cancel it observes from inside. */
27
+ const EXTERNAL_CANCEL_REASON = "external_cancel";
28
+ export class UnknownRunError extends Error {
29
+ runId;
30
+ constructor(runId) {
31
+ super(`no run exists for id ${runId}`);
32
+ this.runId = runId;
33
+ this.name = "UnknownRunError";
34
+ }
35
+ }
36
+ const TERMINAL = new Set(["completed", "partial", "failed", "canceled"]);
37
+ export function createRunCanceler(deps) {
38
+ const { pool, runCharge, runAuthorizer } = deps;
39
+ const logger = (deps.logger ?? createNoopLogger()).named("run-canceler");
40
+ // Lazy so a canceler built for a host that never cancels constructs no client;
41
+ // one purger per factory (its client registers pool handlers — see its comment).
42
+ let engineCancel = deps.cancelWorkflows;
43
+ const cancelWorkflows = async (workflowIds) => {
44
+ engineCancel ??= (() => {
45
+ const purger = createDbosWorkflowPurger({ pool, logger });
46
+ return async (ids) => {
47
+ unwrapOrThrow(await purger.cancel([...ids]));
48
+ };
49
+ })();
50
+ await engineCancel(workflowIds);
51
+ };
52
+ /** Persisted child workflow ids of steps not yet completed. */
53
+ const incompleteChildIds = async (runId) => unwrapOrThrow(await queryStepsByRun(pool, runId)).flatMap((step) => step.childWorkflowId !== null && step.completedAt === null ? [step.childWorkflowId] : []);
54
+ return {
55
+ async cancel(runId, session) {
56
+ const row = unwrapOrThrow(await queryRun(pool, runId));
57
+ if (row === null)
58
+ throw new UnknownRunError(runId);
59
+ const workflowId = runId;
60
+ if (TERMINAL.has(row.status)) {
61
+ return {
62
+ runId,
63
+ workflowId,
64
+ outcome: "already_terminal",
65
+ finalStatus: row.status,
66
+ converged: { steps: false, charge: false, mandate: false },
67
+ };
68
+ }
69
+ // The child-cascading cancel walks the engine's own parent ledger, which
70
+ // has a row the moment a child starts — that covers a child whose
71
+ // mark-running step has not yet committed its child_workflow_id. The
72
+ // persisted ids and the post-cancel re-query are belt-and-braces over
73
+ // both ledgers' lag.
74
+ const knownChildren = await incompleteChildIds(runId);
75
+ await cancelWorkflows([workflowId, ...knownChildren]);
76
+ try {
77
+ const late = (await incompleteChildIds(runId)).filter((id) => !knownChildren.includes(id));
78
+ if (late.length > 0)
79
+ await cancelWorkflows(late);
80
+ }
81
+ catch (err) {
82
+ logger.error("late-child cancel sweep failed", { runId, ...logger.errorFields(err) });
83
+ }
84
+ // Convergence — each phase isolated so one failure skips nothing after it.
85
+ let transitioned = false;
86
+ try {
87
+ transitioned = unwrapOrThrow(await markRunCanceledIfActive(pool, runId, EXTERNAL_CANCEL_REASON));
88
+ }
89
+ catch (err) {
90
+ logger.error("markRunCanceledIfActive failed", { runId, ...logger.errorFields(err) });
91
+ }
92
+ let steps = false;
93
+ try {
94
+ unwrapOrThrow(await sweepPendingStepExecutions(pool, runId));
95
+ steps = true;
96
+ }
97
+ catch (err) {
98
+ logger.error("pending-step sweep failed", { runId, ...logger.errorFields(err) });
99
+ }
100
+ // Best-effort: the billing authority self-heals (defensive open + stale
101
+ // reaper), so a failed close loses attribution only.
102
+ let charge = false;
103
+ try {
104
+ await runCharge.close({ analysisId: row.analysisId, runId, reason: "canceled", session });
105
+ charge = true;
106
+ }
107
+ catch (err) {
108
+ logger.error("running-charge close failed", { runId, ...logger.errorFields(err) });
109
+ }
110
+ let mandate = row.mandateJti === null;
111
+ if (row.mandateJti !== null) {
112
+ try {
113
+ await runAuthorizer.revokeByJti({ jti: row.mandateJti, auth: session.auth }, EXTERNAL_CANCEL_REASON);
114
+ mandate = true;
115
+ }
116
+ catch (err) {
117
+ logger.error("mandate revoke failed", { runId, jti: row.mandateJti, ...logger.errorFields(err) });
118
+ }
119
+ }
120
+ // No transition means the run reached a terminal state on its own (or the
121
+ // write failed) — report the row as it stands rather than claiming `canceled`.
122
+ let finalStatus = "canceled";
123
+ if (!transitioned) {
124
+ const reread = unwrapOrThrow(await queryRun(pool, runId));
125
+ finalStatus = reread?.status ?? "canceled";
126
+ }
127
+ return { runId, workflowId, outcome: "canceled", finalStatus, converged: { steps, charge, mandate } };
128
+ },
129
+ };
130
+ }
package/dist/index.d.ts CHANGED
@@ -7,13 +7,14 @@
7
7
  export { assembleCoreRuntime } from "./runtime/assemble.js";
8
8
  export type { CoreRuntime, CoreRuntimeDeps, CoreWorkflowDeps, RegisteredWorkflows, ConversationAssemblyDeps, SandboxStepCallable, MakeReportReferenceResolver, ReportResolverScope, } from "./runtime/assemble.js";
9
9
  export type { ReferenceResolver, ReportSnapshot, ResolvedValue } from "./report-model/reference-resolver.js";
10
+ export type { ResolvePageAsset } from "./tools/report-session/index.js";
10
11
  export type { ThreadAgentResolver, UnregisteredThreadType } from "./runtime/assemble.js";
11
12
  export { bootHarness } from "./runtime/boot.js";
12
13
  export type { BootHarnessDeps, BootedHarness } from "./runtime/boot.js";
13
14
  export { createConversationAgent } from "./agents/conversation-agent.js";
14
15
  export type { ConversationAgentDeps } from "./agents/conversation-agent.js";
15
16
  export { createLocalRunAuthorizer } from "./auth/local-run-authorizer.js";
16
- export type { RunAuthorizer, AuthorizeRunInput, RunAuthorization } from "./execution/run-authorizer.js";
17
+ export type { RunAuthorizer, AuthorizeRunInput, RunAuthorization, RevokeByJtiRef } from "./execution/run-authorizer.js";
17
18
  export { createNoopBillingResolver } from "./billing/noop-resolver.js";
18
19
  export type { ResolveBilling, ResolvableSession, BillingMap, BillingHeaders, BillingFetchResult } from "./billing/resolver.js";
19
20
  export { createNoopRunCharge } from "./billing/noop-run-charge.js";
@@ -31,6 +32,8 @@ export type { RunLauncher, LaunchOptions } from "./execution/run-launcher.js";
31
32
  export { createDbosWorkflowPurger } from "./execution/dbos-workflow-purger.js";
32
33
  export type { DbosWorkflowPurgerDeps } from "./execution/dbos-workflow-purger.js";
33
34
  export type { WorkflowPurger } from "./execution/workflow-purger.js";
35
+ export { createRunCanceler, UnknownRunError } from "./execution/run-canceler.js";
36
+ export type { RunCanceler, RunCancelerDeps, CancelRunResult } from "./execution/run-canceler.js";
34
37
  export { defineTool, isToolError } from "./tools/define-tool.js";
35
38
  export type { Tool, ToolDefinition, ToolContext, ToolError } from "./tools/define-tool.js";
36
39
  export { normalizeDetail, DETAIL_MAX_LENGTH } from "./loop/tool-detail.js";
@@ -137,7 +140,7 @@ export type { ValidatePlanOptions } from "./schemas/validate-plan.js";
137
140
  export type { ValidationResult } from "./schemas/validate-plan.js";
138
141
  export { upsertPlan, loadPlan } from "./state/plans.js";
139
142
  export type { UpsertPlanInput } from "./state/plans.js";
140
- export { insertRun, reserveRunById, queryActiveRun, queryActiveRunsByAnalysis, updateRunStatus, queryRun, queryRunsByAnalysis, RunDedupCollisionError, RunIdentityCollisionError, } from "./state/runs.js";
143
+ export { insertRun, reserveRunById, queryActiveRun, queryActiveRunsByAnalysis, updateRunStatus, markRunCanceledIfActive, queryRun, queryRunsByAnalysis, RunDedupCollisionError, RunIdentityCollisionError, } from "./state/runs.js";
141
144
  export type { InsertRunInput, RunReservation } from "./state/runs.js";
142
145
  export { queryStepsByRun } from "./state/step-executions.js";
143
146
  export type { CortexRunRow, StepExecutionRow, RunStatus } from "./state/schema.js";
package/dist/index.js CHANGED
@@ -31,6 +31,11 @@ export { createDbosRunLauncher } from "./execution/dbos-run-launcher.js";
31
31
  // Seam: workflow-ledger reclamation — cancels and deletes an analysis's rows in
32
32
  // the durability engine's own ledger, so no caller names an engine type.
33
33
  export { createDbosWorkflowPurger } from "./execution/dbos-workflow-purger.js";
34
+ // Run cancellation — the encapsulated external-cancel path: engine cancel
35
+ // (parent + children) plus convergence of the run row, pending step rows,
36
+ // running charge, and run mandate. Hosts call this from their cancel route
37
+ // instead of hand-rolling `DBOS.cancelWorkflow` + partial cleanup.
38
+ export { createRunCanceler, UnknownRunError } from "./execution/run-canceler.js";
34
39
  // Tool primitive.
35
40
  export { defineTool, isToolError } from "./tools/define-tool.js";
36
41
  // Tool-call-detail authoring aids for an embedder that contributes a host tool.
@@ -224,7 +229,7 @@ export { validatePlan } from "./schemas/validate-plan.js";
224
229
  // caller-derived id and inserts-if-absent (the deterministic-id intake path);
225
230
  // the run-state calls mirror the chat trigger's reserve → dedup → status flow.
226
231
  export { upsertPlan, loadPlan } from "./state/plans.js";
227
- export { insertRun, reserveRunById, queryActiveRun, queryActiveRunsByAnalysis, updateRunStatus, queryRun, queryRunsByAnalysis, RunDedupCollisionError, RunIdentityCollisionError, } from "./state/runs.js";
232
+ export { insertRun, reserveRunById, queryActiveRun, queryActiveRunsByAnalysis, updateRunStatus, markRunCanceledIfActive, queryRun, queryRunsByAnalysis, RunDedupCollisionError, RunIdentityCollisionError, } from "./state/runs.js";
228
233
  export { queryStepsByRun } from "./state/step-executions.js";
229
234
  // Host-neutral reference-data catalog and shared installation receipts. Each
230
235
  // artifact names the third-party upstream it is fetched from; embedders own all
@@ -11,7 +11,8 @@ const IdConvResponseSchema = z.object({
11
11
  });
12
12
  export function ncbiUrl(apiKey, base, params) {
13
13
  const url = new URL(base);
14
- if (apiKey !== undefined)
14
+ // The URL must not include an empty key. NCBI rejects `api_key=` with HTTP 400.
15
+ if (apiKey)
15
16
  url.searchParams.set("api_key", apiKey);
16
17
  for (const [key, value] of Object.entries(params))
17
18
  if (value !== undefined)
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The asset manifest of the page.
3
+ *
4
+ * The page loads its chart runtime and its fonts from the sibling `assets/` directory. Thus each entry
5
+ * names the staged file and the module specifier of its source. The caller resolves each specifier against
6
+ * its own installation, and it copies the bytes beside the page.
7
+ *
8
+ * The module holds data alone. The renderer reads no file, thus the page stays a pure function of its
9
+ * inputs.
10
+ */
11
+ /** The directory of the staged assets, relative to the page. */
12
+ export declare const ASSETS_DIR = "assets";
13
+ /**
14
+ * One staged asset. `file` is the basename under the assets directory, and `specifier` is the module
15
+ * specifier of the source file inside the installed package.
16
+ */
17
+ export interface PageAsset {
18
+ readonly file: string;
19
+ readonly specifier: string;
20
+ }
21
+ /** The relative source of a staged asset. The page directory is self-contained, thus the source is `assets/<file>`. */
22
+ export declare function assetSource(asset: PageAsset): string;
23
+ /** The chart runtime. The page-side bootstrap reads the `echarts` global that this file declares. */
24
+ export declare const ECHARTS_ASSET: PageAsset;
25
+ /** The sans font. The file is the latin subset of the variable font, thus one file carries each weight. */
26
+ export declare const SANS_FONT_ASSET: PageAsset;
27
+ /**
28
+ * The mono font at weight 400, for the body of a data cell.
29
+ *
30
+ * Fontsource publishes no variable IBM Plex Mono, thus each weight is a separate file and a separate entry.
31
+ * The identity uses four weights: 400 for a data cell, 500 for a tag, 600 for a table header and a badge,
32
+ * and 700 for a metric value.
33
+ */
34
+ export declare const MONO_FONT_400_ASSET: PageAsset;
35
+ /** The mono font at weight 500, for a tag and a stat-card label. */
36
+ export declare const MONO_FONT_500_ASSET: PageAsset;
37
+ /** The mono font at weight 600, for a table header, a section label, and the panel badge. */
38
+ export declare const MONO_FONT_600_ASSET: PageAsset;
39
+ /** The mono font at weight 700, for a metric value. */
40
+ export declare const MONO_FONT_700_ASSET: PageAsset;
41
+ /** Each asset that the caller stages beside the page. */
42
+ export declare const PAGE_ASSETS: readonly PageAsset[];
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The asset manifest of the page.
3
+ *
4
+ * The page loads its chart runtime and its fonts from the sibling `assets/` directory. Thus each entry
5
+ * names the staged file and the module specifier of its source. The caller resolves each specifier against
6
+ * its own installation, and it copies the bytes beside the page.
7
+ *
8
+ * The module holds data alone. The renderer reads no file, thus the page stays a pure function of its
9
+ * inputs.
10
+ */
11
+ /** The directory of the staged assets, relative to the page. */
12
+ export const ASSETS_DIR = "assets";
13
+ /** The relative source of a staged asset. The page directory is self-contained, thus the source is `assets/<file>`. */
14
+ export function assetSource(asset) {
15
+ return `${ASSETS_DIR}/${asset.file}`;
16
+ }
17
+ /** The chart runtime. The page-side bootstrap reads the `echarts` global that this file declares. */
18
+ export const ECHARTS_ASSET = {
19
+ file: "echarts.min.js",
20
+ specifier: "echarts/dist/echarts.min.js",
21
+ };
22
+ /** The sans font. The file is the latin subset of the variable font, thus one file carries each weight. */
23
+ export const SANS_FONT_ASSET = {
24
+ file: "space-grotesk-latin-wght-normal.woff2",
25
+ specifier: "@fontsource-variable/space-grotesk/files/space-grotesk-latin-wght-normal.woff2",
26
+ };
27
+ /**
28
+ * The mono font at weight 400, for the body of a data cell.
29
+ *
30
+ * Fontsource publishes no variable IBM Plex Mono, thus each weight is a separate file and a separate entry.
31
+ * The identity uses four weights: 400 for a data cell, 500 for a tag, 600 for a table header and a badge,
32
+ * and 700 for a metric value.
33
+ */
34
+ export const MONO_FONT_400_ASSET = {
35
+ file: "ibm-plex-mono-latin-400-normal.woff2",
36
+ specifier: "@fontsource/ibm-plex-mono/files/ibm-plex-mono-latin-400-normal.woff2",
37
+ };
38
+ /** The mono font at weight 500, for a tag and a stat-card label. */
39
+ export const MONO_FONT_500_ASSET = {
40
+ file: "ibm-plex-mono-latin-500-normal.woff2",
41
+ specifier: "@fontsource/ibm-plex-mono/files/ibm-plex-mono-latin-500-normal.woff2",
42
+ };
43
+ /** The mono font at weight 600, for a table header, a section label, and the panel badge. */
44
+ export const MONO_FONT_600_ASSET = {
45
+ file: "ibm-plex-mono-latin-600-normal.woff2",
46
+ specifier: "@fontsource/ibm-plex-mono/files/ibm-plex-mono-latin-600-normal.woff2",
47
+ };
48
+ /** The mono font at weight 700, for a metric value. */
49
+ export const MONO_FONT_700_ASSET = {
50
+ file: "ibm-plex-mono-latin-700-normal.woff2",
51
+ specifier: "@fontsource/ibm-plex-mono/files/ibm-plex-mono-latin-700-normal.woff2",
52
+ };
53
+ /** Each asset that the caller stages beside the page. */
54
+ export const PAGE_ASSETS = [
55
+ ECHARTS_ASSET,
56
+ SANS_FONT_ASSET,
57
+ MONO_FONT_400_ASSET,
58
+ MONO_FONT_500_ASSET,
59
+ MONO_FONT_600_ASSET,
60
+ MONO_FONT_700_ASSET,
61
+ ];
@@ -20,6 +20,23 @@ import { normalizeEchartSpec } from "../tools/display/normalize-echart-spec.js";
20
20
  * scale maps the low value to `#440154` and the high value to `#fde725`.
21
21
  */
22
22
  const VIRIDIS = ["#440154", "#482777", "#3e4989", "#31688e", "#26828e", "#1f9e89", "#35b779", "#6ece58", "#b5de2b", "#fde725"];
23
+ /**
24
+ * The distance in pixels between the x axis line and its name.
25
+ *
26
+ * The name sits under the middle of the axis, thus the gap must clear the axis labels below the line. The
27
+ * value is a fixed constant, thus the derivation stays deterministic.
28
+ */
29
+ const X_AXIS_NAME_GAP = 34;
30
+ /**
31
+ * The name fields of an x axis.
32
+ *
33
+ * `normalizeEchartSpec` holds the grid to a right margin of 5 percent. The ECharts default `nameLocation`
34
+ * of `"end"` puts the name at the right end of the axis, thus the name runs past that margin and the panel
35
+ * clips it. A centered name sits under the middle of the axis, and no margin can cut it.
36
+ */
37
+ function xAxisName(column) {
38
+ return { name: column, nameLocation: "middle", nameGap: X_AXIS_NAME_GAP };
39
+ }
23
40
  /**
24
41
  * Derive the ECharts option for a chart block, then pass it through `normalizeEchartSpec`.
25
42
  *
@@ -68,7 +85,7 @@ function deriveBar(block, rows, columns) {
68
85
  data: groupRows.map((row) => [row[x], row[y]]),
69
86
  }));
70
87
  return ok({
71
- xAxis: { type: "category", data: categories, name: x },
88
+ xAxis: { type: "category", data: categories, ...xAxisName(x) },
72
89
  yAxis: { type: "value", name: y },
73
90
  series,
74
91
  });
@@ -90,8 +107,8 @@ function deriveLine(block, rows, columns) {
90
107
  data: sortByX(groupRows.map((row) => [row[x], row[y]])),
91
108
  }));
92
109
  return ok({
93
- xAxis: inferAxis(rows, x),
94
- yAxis: inferAxis(rows, y),
110
+ xAxis: inferAxis(rows, x, "x"),
111
+ yAxis: inferAxis(rows, y, "y"),
95
112
  series,
96
113
  });
97
114
  }
@@ -113,8 +130,8 @@ function deriveScatter(block, rows, columns) {
113
130
  data: groupRows.map((row) => [row[x], row[y]]),
114
131
  }));
115
132
  return ok({
116
- xAxis: inferAxis(rows, x),
117
- yAxis: inferAxis(rows, y),
133
+ xAxis: inferAxis(rows, x, "x"),
134
+ yAxis: inferAxis(rows, y, "y"),
118
135
  series,
119
136
  });
120
137
  }
@@ -199,7 +216,7 @@ function deriveBox(block, rows, columns) {
199
216
  }
200
217
  }
201
218
  return ok({
202
- xAxis: { type: "category", data: categories, name: x },
219
+ xAxis: { type: "category", data: categories, ...xAxisName(x) },
203
220
  yAxis: { type: "value", name: y },
204
221
  series,
205
222
  });
@@ -242,7 +259,7 @@ function deriveHeatmap(block, rows, columns) {
242
259
  const min = finite.length > 0 ? Math.min(...finite) : 0;
243
260
  const max = finite.length > 0 ? Math.max(...finite) : 1;
244
261
  return ok({
245
- xAxis: { type: "category", data: xCategories.map(String), name: x, splitArea: { show: true } },
262
+ xAxis: { type: "category", data: xCategories.map(String), ...xAxisName(x), splitArea: { show: true } },
246
263
  yAxis: { type: "category", data: yCategories.map(String), name: y, splitArea: { show: true } },
247
264
  visualMap: {
248
265
  type: "continuous",
@@ -355,12 +372,16 @@ function numericColumn(rows, column) {
355
372
  /**
356
373
  * The axis for a line or a scatter column. A column with any non-numeric string cell is a category axis
357
374
  * with its distinct values as strings. Any other column is a value axis with `scale: true`.
375
+ *
376
+ * The `axis` argument names the channel that the column feeds. An x column takes the centered name, and a
377
+ * y column keeps the default name placement.
358
378
  */
359
- function inferAxis(rows, column) {
379
+ function inferAxis(rows, column, axis) {
380
+ const nameFields = axis === "x" ? xAxisName(column) : { name: column };
360
381
  if (rows.some((row) => isNonNumericString(row[column]))) {
361
- return { type: "category", data: firstAppearance(rows.map((row) => row[column])).map(String), name: column };
382
+ return { type: "category", data: firstAppearance(rows.map((row) => row[column])).map(String), ...nameFields };
362
383
  }
363
- return { type: "value", scale: true, name: column };
384
+ return { type: "value", scale: true, ...nameFields };
364
385
  }
365
386
  /**
366
387
  * Build one series per group, in first-appearance order of the group values. A chart with no group