@tangle-network/agent-app 0.44.8 → 0.44.10

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 (36) hide show
  1. package/dist/assistant/index.d.ts +4 -4
  2. package/dist/assistant/index.js +6 -6
  3. package/dist/{attachment-validation-DRuLEjAE.d.ts → attachment-validation-yJ6Id4pR.d.ts} +1 -1
  4. package/dist/chat-routes/index.d.ts +28 -5
  5. package/dist/chat-routes/index.js +45 -5
  6. package/dist/chat-routes/index.js.map +1 -1
  7. package/dist/chat-store/index.d.ts +3 -3
  8. package/dist/chat-store/index.js +2 -2
  9. package/dist/{chunk-V55WJSR4.js → chunk-5GA3MKOC.js} +2 -2
  10. package/dist/{chunk-UDSY2F6N.js → chunk-72TPRENZ.js} +21 -8
  11. package/dist/chunk-72TPRENZ.js.map +1 -0
  12. package/dist/{chunk-AFNTRJQ7.js → chunk-FEUW4G2L.js} +2 -2
  13. package/dist/{chunk-TKRI463Z.js → chunk-J6TNPRQN.js} +4 -4
  14. package/dist/{chunk-F2CBC4DY.js → chunk-V3HO43PG.js} +15 -1
  15. package/dist/chunk-V3HO43PG.js.map +1 -0
  16. package/dist/{chunk-UP33Z633.js → chunk-ZRSUCTST.js} +2 -2
  17. package/dist/{parts-Co_lFgLI.d.ts → parts-DIKcC1p6.d.ts} +1 -1
  18. package/dist/{queue-CD5-_VTX.d.ts → queue-DBkulxW1.d.ts} +1 -1
  19. package/dist/turn-health/index.d.ts +149 -13
  20. package/dist/turn-health/index.js +192 -12
  21. package/dist/turn-health/index.js.map +1 -1
  22. package/dist/{types-XIZqIdX1.d.ts → types-DeEOhQbv.d.ts} +25 -3
  23. package/dist/web-react/index.d.ts +6 -6
  24. package/dist/web-react/index.js +6 -6
  25. package/dist/work-product/index.d.ts +73 -5
  26. package/dist/work-product/index.js +131 -17
  27. package/dist/work-product/index.js.map +1 -1
  28. package/dist/work-product-react/index.d.ts +1 -1
  29. package/dist/work-product-react/index.js +3 -3
  30. package/package.json +1 -1
  31. package/dist/chunk-F2CBC4DY.js.map +0 -1
  32. package/dist/chunk-UDSY2F6N.js.map +0 -1
  33. /package/dist/{chunk-V55WJSR4.js.map → chunk-5GA3MKOC.js.map} +0 -0
  34. /package/dist/{chunk-AFNTRJQ7.js.map → chunk-FEUW4G2L.js.map} +0 -0
  35. /package/dist/{chunk-TKRI463Z.js.map → chunk-J6TNPRQN.js.map} +0 -0
  36. /package/dist/{chunk-UP33Z633.js.map → chunk-ZRSUCTST.js.map} +0 -0
@@ -1,12 +1,12 @@
1
- import { C as ChatMessagePart } from '../parts-Co_lFgLI.js';
2
- export { a as ChatAttachmentKind, b as ChatAttachmentPart, c as ChatFilePart, d as ChatImagePart, e as ChatInteractionPart, f as ChatMentionKind, g as ChatMentionPart, h as ChatNoticePart, i as ChatPartTime, j as ChatPlanPart, k as ChatReasoningPart, l as ChatStepFinishPart, m as ChatStepStartPart, n as ChatSubtaskPart, o as ChatTextPart, p as ChatToolPart, q as ChatToolState, r as ChatToolStatus, s as ChatUsageTokens, t as ChatWorkProductPart, D as DEFAULT_ATTACHMENT_PROMPT_HEADER, S as StorableHarnessPartKind, u as attachmentInputToPart, v as attachmentKindForMime, w as attachmentPartsFromMessageParts, x as buildAttachmentPromptBlock, y as historyContentWithAttachments, z as isChatAttachmentPart, A as isChatInteractionPart, B as isChatMentionPart, E as isChatPlanPart, F as isChatStepFinishPart, G as isChatTextPart, H as isChatToolPart, I as isChatWorkProductPart, J as mentionInputToPart, K as mentionPartsFromMessageParts, L as toChatMessageParts } from '../parts-Co_lFgLI.js';
1
+ import { C as ChatMessagePart } from '../parts-DIKcC1p6.js';
2
+ export { a as ChatAttachmentKind, b as ChatAttachmentPart, c as ChatFilePart, d as ChatImagePart, e as ChatInteractionPart, f as ChatMentionKind, g as ChatMentionPart, h as ChatNoticePart, i as ChatPartTime, j as ChatPlanPart, k as ChatReasoningPart, l as ChatStepFinishPart, m as ChatStepStartPart, n as ChatSubtaskPart, o as ChatTextPart, p as ChatToolPart, q as ChatToolState, r as ChatToolStatus, s as ChatUsageTokens, t as ChatWorkProductPart, D as DEFAULT_ATTACHMENT_PROMPT_HEADER, S as StorableHarnessPartKind, u as attachmentInputToPart, v as attachmentKindForMime, w as attachmentPartsFromMessageParts, x as buildAttachmentPromptBlock, y as historyContentWithAttachments, z as isChatAttachmentPart, A as isChatInteractionPart, B as isChatMentionPart, E as isChatPlanPart, F as isChatStepFinishPart, G as isChatTextPart, H as isChatToolPart, I as isChatWorkProductPart, J as mentionInputToPart, K as mentionPartsFromMessageParts, L as toChatMessageParts } from '../parts-DIKcC1p6.js';
3
3
  import * as drizzle_orm from 'drizzle-orm';
4
4
  import * as drizzle_orm_sqlite_core from 'drizzle-orm/sqlite-core';
5
5
  import { SQLiteColumnBuilderBase, AnySQLiteTable, AnySQLiteColumn, BaseSQLiteDatabase } from 'drizzle-orm/sqlite-core';
6
6
  export { a as attachmentPartKey } from '../stream-normalizer-DnuqkZvw.js';
7
7
  import '@tangle-network/agent-interface';
8
8
  import '../contract-CEewO6DI.js';
9
- import '../types-XIZqIdX1.js';
9
+ import '../types-DeEOhQbv.js';
10
10
  import '../plans/index.js';
11
11
 
12
12
  /**
@@ -16,8 +16,8 @@ import {
16
16
  mentionInputToPart,
17
17
  mentionPartsFromMessageParts,
18
18
  toChatMessageParts
19
- } from "../chunk-AFNTRJQ7.js";
20
- import "../chunk-F2CBC4DY.js";
19
+ } from "../chunk-FEUW4G2L.js";
20
+ import "../chunk-V3HO43PG.js";
21
21
  import {
22
22
  attachmentPartKey
23
23
  } from "../chunk-5EPIPT4V.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  formatBytes
3
- } from "./chunk-AFNTRJQ7.js";
3
+ } from "./chunk-FEUW4G2L.js";
4
4
 
5
5
  // src/chat-routes/binary-sniff.ts
6
6
  function bytesStartWith(bytes, offset, signature) {
@@ -236,4 +236,4 @@ export {
236
236
  attachmentTotalSizeErrorMessage,
237
237
  createSandboxFileIndexRoute
238
238
  };
239
- //# sourceMappingURL=chunk-V55WJSR4.js.map
239
+ //# sourceMappingURL=chunk-5GA3MKOC.js.map
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  parseReviewQueueItem
3
- } from "./chunk-UP33Z633.js";
3
+ } from "./chunk-ZRSUCTST.js";
4
4
  import {
5
5
  persistedPartToWorkProduct
6
- } from "./chunk-F2CBC4DY.js";
6
+ } from "./chunk-V3HO43PG.js";
7
7
 
8
8
  // src/web-react/work-product.tsx
9
9
  import { useCallback, useEffect, useState } from "react";
@@ -226,11 +226,24 @@ function EvidenceLineageTable({ evidence, resolveSourceUrl, className }) {
226
226
  /* @__PURE__ */ jsx("td", { className: "py-2 pr-3 font-mono text-xs text-foreground", children: entry.target }),
227
227
  /* @__PURE__ */ jsxs("td", { className: "py-2 pr-3 text-[13px] leading-snug text-foreground", children: [
228
228
  entry.claim,
229
- entry.locator.quote && /* @__PURE__ */ jsxs("span", { className: "mt-0.5 block border-l-2 border-border pl-2 text-xs italic text-muted-foreground", children: [
230
- "\u201C",
231
- entry.locator.quote,
232
- "\u201D"
233
- ] })
229
+ entry.locator.quote && // The basis is what a reviewer weighs, so it has to be
230
+ // visible per row, not only in the aggregate check detail.
231
+ // A platform-sliced quote came out of the document's own
232
+ // bytes; a model-typed one was proved to occur but was
233
+ // transcribed. Same text, different strength of evidence.
234
+ /* @__PURE__ */ jsxs(
235
+ "span",
236
+ {
237
+ className: `mt-0.5 block border-l-2 pl-2 text-xs italic ${entry.locator.quoteBasis === "span" ? "border-primary/60 text-foreground" : "border-border text-muted-foreground"}`,
238
+ title: entry.locator.quoteBasis === "span" ? "Sliced from the source document by the platform" : entry.locator.quoteBasis === "model" ? "Quoted by the agent, verified to occur in the source" : "Unverified quote",
239
+ children: [
240
+ "\u201C",
241
+ entry.locator.quote,
242
+ "\u201D",
243
+ entry.locator.quoteBasis && /* @__PURE__ */ jsx("span", { className: "ml-1.5 align-middle text-[10px] not-italic uppercase tracking-wide text-muted-foreground", children: entry.locator.quoteBasis === "span" ? "from source" : "verified" })
244
+ ]
245
+ }
246
+ )
234
247
  ] }),
235
248
  /* @__PURE__ */ jsxs("td", { className: "py-2 text-xs", children: [
236
249
  url ? /* @__PURE__ */ jsx(
@@ -328,4 +341,4 @@ export {
328
341
  QualityCheckList,
329
342
  ProvenanceStamp
330
343
  };
331
- //# sourceMappingURL=chunk-UDSY2F6N.js.map
344
+ //# sourceMappingURL=chunk-72TPRENZ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/web-react/work-product.tsx"],"sourcesContent":["/**\n * Work-product review surfaces — the sandbox-ui-FREE half of the vertical:\n *\n * - {@link ReviewQueuePanel}: the practice/matters/campaign review queue over\n * a `fetchQueue(cursor)` data port (the `AgentActivityPanel` shape).\n * - {@link WorkProductCard}: the persisted `type:'work_product'` transcript\n * anchor rendered inside `ChatMessages` — chat stays the driver surface.\n * - {@link EvidenceLineageTable}: target → claim → source rows with\n * click-through via an injected `resolveSourceUrl` (the\n * `MessageAttachments.resolveFileUrl` pattern).\n * - {@link ExceptionList} / {@link QualityCheckList}: severity-badged\n * exception rows and pass/fail check rows.\n * - {@link ProvenanceStamp}: profileHash prefix + runId + serving-model\n * chips + the optional backtest slot (\"182 cases · composite 0.81 ·\n * trust PASS\" — or \"quality: unverified\" when the trust gate failed).\n *\n * Anything hosting sandbox-ui (DiffView, CodeView, PillTabs) lives in\n * `./work-product-react` instead — this module must stay importable without\n * the optional sandbox-ui peer.\n */\n\nimport { useCallback, useEffect, useState } from 'react'\n\nimport {\n parseReviewQueueItem,\n type ReviewQueueItem,\n type ReviewQueueState,\n} from '../work-product/queue'\nimport type {\n EvidenceEntry,\n ExceptionEntry,\n ProfileBacktestSummary,\n QualityCheck,\n WorkProductPersistedPart,\n WorkProductProvenance,\n WorkProductStatus,\n} from '../work-product/types'\nimport { persistedPartToWorkProduct } from '../work-product/types'\n\nexport { parseReviewQueueItem }\nexport type { ReviewQueueItem, ReviewQueueState }\n\n// ── shared presentation vocabulary ────────────────────────────────────────\n\nconst STATE_LABELS: Record<ReviewQueueState, string> = {\n intake: 'Intake',\n missing_info: 'Missing info',\n working: 'Working',\n ready_for_review: 'Ready for review',\n changes_requested: 'Changes requested',\n approved: 'Approved',\n blocked: 'Blocked',\n}\n\nconst STATE_TONES: Record<ReviewQueueState, string> = {\n intake: 'bg-muted/40 text-muted-foreground',\n missing_info: 'bg-warning/10 text-warning',\n working: 'bg-primary/10 text-primary',\n ready_for_review: 'bg-success/10 text-success',\n changes_requested: 'bg-warning/10 text-warning',\n approved: 'bg-success/10 text-success',\n blocked: 'bg-destructive/10 text-destructive',\n}\n\nconst STATUS_LABELS: Record<WorkProductStatus, string> = {\n draft: 'Draft',\n blocked: 'Blocked',\n ready: 'Ready for review',\n changes_requested: 'Changes requested',\n approved: 'Approved',\n superseded: 'Superseded',\n}\n\nconst STATUS_TONES: Record<WorkProductStatus, string> = {\n draft: 'bg-primary/10 text-primary',\n blocked: 'bg-destructive/10 text-destructive',\n ready: 'bg-success/10 text-success',\n changes_requested: 'bg-warning/10 text-warning',\n approved: 'bg-success/10 text-success',\n superseded: 'bg-muted/40 text-muted-foreground',\n}\n\n/** Human label for a queue state. */\nexport function reviewQueueStateLabel(state: ReviewQueueState): string {\n return STATE_LABELS[state]\n}\n\n/** Human label for a work-product status. */\nexport function workProductStatusLabel(status: WorkProductStatus): string {\n return STATUS_LABELS[status]\n}\n\nfunction StatePill({ state }: { state: ReviewQueueState }) {\n return (\n <span className={`inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium ${STATE_TONES[state]}`}>\n {STATE_LABELS[state]}\n </span>\n )\n}\n\nfunction StatusPill({ status }: { status: WorkProductStatus }) {\n return (\n <span className={`inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium ${STATUS_TONES[status]}`}>\n {STATUS_LABELS[status]}\n </span>\n )\n}\n\n// ── transcript anchor helpers + card ──────────────────────────────────────\n\n/** Every persisted work-product anchor on one message, re-validated. */\nexport function workProductPartsFromMessageParts(\n parts: ReadonlyArray<Record<string, unknown>> | null | undefined,\n): WorkProductPersistedPart[] {\n if (!parts) return []\n const out: WorkProductPersistedPart[] = []\n for (const part of parts) {\n const typed = persistedPartToWorkProduct(part)\n if (typed) out.push(typed)\n }\n return out\n}\n\n/** Properties for the transcript anchor card rendered in chat */\nexport interface WorkProductCardProps {\n part: WorkProductPersistedPart\n /** Open the work product (queue detail / pane) — the card's only action. */\n onOpen?: (part: WorkProductPersistedPart) => void\n className?: string\n}\n\n/** The chat transcript anchor card for one work-product version — a compact,\n * system-authored pointer, not a parallel review surface. */\nexport function WorkProductCard({ part, onOpen, className }: WorkProductCardProps) {\n return (\n <div\n className={`flex items-center gap-3 rounded-xl border border-border bg-card px-4 py-3 ${className ?? ''}`}\n data-work-product-id={part.ref.id}\n >\n <span className=\"flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary\">\n <svg viewBox=\"0 0 24 24\" className=\"h-4 w-4\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n <path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z\" />\n <polyline points=\"14 2 14 8 20 8\" />\n <path d=\"m9 15 2 2 4-4\" />\n </svg>\n </span>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-semibold text-foreground\">{part.title}</p>\n <p className=\"text-[11px] text-muted-foreground\">\n {part.kind && <span className=\"font-mono\">{part.kind}</span>}\n {part.kind && ' · '}v{part.ref.version}\n </p>\n </div>\n <StatusPill status={part.status} />\n {onOpen && (\n <button\n type=\"button\"\n onClick={() => onOpen(part)}\n className=\"rounded-md border border-border px-2.5 py-1 text-xs font-medium text-foreground transition hover:bg-accent/30\"\n >\n Review\n </button>\n )}\n </div>\n )\n}\n\n// ── review queue panel ────────────────────────────────────────────────────\n\n/** One fetched page of queue items with an optional continuation cursor */\nexport interface ReviewQueuePage {\n items: ReviewQueueItem[]\n /** Opaque continuation token; absent ⇒ no further pages. */\n nextCursor?: string\n}\n\n/** Properties for the review queue panel over a fetch data port */\nexport interface ReviewQueuePanelProps {\n /** Data port — fetch one page of queue items (the projection's output,\n * re-validated here at the JSON boundary). */\n fetchQueue: (cursor?: string) => Promise<ReviewQueuePage>\n /** Open one queue item (navigate to its thread / detail pane). */\n onSelect?: (item: ReviewQueueItem) => void\n title?: string\n emptyLabel?: string\n className?: string\n}\n\n/** Merge a fetched page into held rows: dedupe by scopeKey, incoming wins,\n * newest updatedAt first. Exported for tests. */\nexport function mergeReviewQueuePages(existing: ReviewQueueItem[], incoming: ReviewQueueItem[]): ReviewQueueItem[] {\n const byScope = new Map<string, ReviewQueueItem>()\n for (const item of existing) byScope.set(item.scopeKey, item)\n for (const item of incoming) byScope.set(item.scopeKey, item)\n return [...byScope.values()].sort((a, b) => b.updatedAt - a.updatedAt)\n}\n\n/** The workspace review queue — cursor-paged, refreshable, selection via\n * callback. States, counts, and provenance render from the projection; the\n * panel holds no domain logic. */\nexport function ReviewQueuePanel({\n fetchQueue,\n onSelect,\n title = 'Review queue',\n emptyLabel = 'Nothing awaiting review.',\n className,\n}: ReviewQueuePanelProps) {\n const [items, setItems] = useState<ReviewQueueItem[]>([])\n const [cursor, setCursor] = useState<string | undefined>(undefined)\n const [loading, setLoading] = useState(false)\n const [error, setError] = useState<string | null>(null)\n\n const load = useCallback(\n async (from?: string) => {\n setLoading(true)\n setError(null)\n try {\n const page = await fetchQueue(from)\n const validated = page.items\n .map((item) => parseReviewQueueItem(item))\n .filter((item): item is ReviewQueueItem => item !== null)\n setItems((prev) => mergeReviewQueuePages(from === undefined ? [] : prev, validated))\n setCursor(page.nextCursor)\n } catch (e) {\n setError(e instanceof Error ? e.message : String(e))\n } finally {\n setLoading(false)\n }\n },\n [fetchQueue],\n )\n\n useEffect(() => {\n void load()\n }, [load])\n\n return (\n <div className={`space-y-2 ${className ?? ''}`}>\n <div className=\"flex items-center gap-2\">\n <h2 className=\"flex-1 text-sm font-semibold\">{title}</h2>\n <button\n type=\"button\"\n onClick={() => void load()}\n disabled={loading}\n aria-label=\"Refresh\"\n className=\"rounded-md p-1.5 text-muted-foreground transition hover:bg-accent/30 hover:text-foreground disabled:opacity-50\"\n >\n <svg viewBox=\"0 0 24 24\" className={`h-3.5 w-3.5 ${loading ? 'animate-spin' : ''}`} fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n <path d=\"M21 12a9 9 0 1 1-2.64-6.36\" />\n <polyline points=\"21 3 21 9 15 9\" />\n </svg>\n </button>\n </div>\n {error && <p className=\"rounded-md bg-destructive/10 px-3 py-2 text-xs text-destructive\">{error}</p>}\n {!error && items.length === 0 && !loading && (\n <p className=\"rounded-md border border-dashed border-border px-3 py-4 text-center text-xs text-muted-foreground\">{emptyLabel}</p>\n )}\n <ul className=\"space-y-1.5\">\n {items.map((item) => (\n <li key={item.scopeKey}>\n <button\n type=\"button\"\n onClick={() => onSelect?.(item)}\n className=\"flex w-full items-center gap-3 rounded-lg border border-border bg-card px-3 py-2.5 text-left transition hover:border-primary/40 hover:bg-accent/20\"\n >\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-foreground\">\n {item.workProduct?.title ?? item.scopeKey}\n {item.workProduct && <span className=\"ml-1.5 text-[11px] text-muted-foreground\">v{item.workProduct.version}</span>}\n </p>\n <p className=\"mt-0.5 flex flex-wrap items-center gap-x-2 text-[11px] text-muted-foreground\">\n {item.workProduct?.kind && <span className=\"font-mono\">{item.workProduct.kind}</span>}\n {item.pendingAsk && <span>asks: {item.pendingAsk.title}</span>}\n {item.blockingExceptions > 0 && (\n <span className=\"text-destructive\">{item.blockingExceptions} blocking</span>\n )}\n {item.failedChecks > 0 && <span className=\"text-warning\">{item.failedChecks} failed checks</span>}\n {item.provenance && item.provenance.profileHash && (\n <span className=\"font-mono\">{item.provenance.profileHash.slice(0, 8)}</span>\n )}\n </p>\n </div>\n <StatePill state={item.state} />\n </button>\n </li>\n ))}\n </ul>\n {cursor && (\n <button\n type=\"button\"\n onClick={() => void load(cursor)}\n disabled={loading}\n className=\"w-full rounded-md border border-border px-3 py-1.5 text-xs font-medium text-muted-foreground transition hover:bg-accent/30 disabled:opacity-50\"\n >\n Load more\n </button>\n )}\n </div>\n )\n}\n\n// ── evidence lineage table ────────────────────────────────────────────────\n\n/** Properties for the evidence lineage table with source click-through */\nexport interface EvidenceLineageTableProps {\n evidence: readonly EvidenceEntry[]\n /** Resolve one entry's source document to an openable URL (signed\n * object-store/vault download) — the `resolveFileUrl` pattern. Absent →\n * sources render as plain refs without links. */\n resolveSourceUrl?: (entry: EvidenceEntry) => string\n className?: string\n}\n\nfunction locatorLabel(entry: EvidenceEntry): string | null {\n const parts: string[] = []\n if (entry.locator.page !== undefined) parts.push(`p.${entry.locator.page}`)\n if (entry.locator.range) parts.push(entry.locator.range)\n return parts.length ? parts.join(' · ') : null\n}\n\n/** Target → claim → source lineage rows: the \"every material value traceable\n * to source evidence\" surface. Each row names the artifact target, the\n * claim, and the source document location supporting it. */\nexport function EvidenceLineageTable({ evidence, resolveSourceUrl, className }: EvidenceLineageTableProps) {\n if (evidence.length === 0) {\n return <p className={`text-xs text-muted-foreground ${className ?? ''}`}>No evidence recorded.</p>\n }\n return (\n <div className={`overflow-x-auto ${className ?? ''}`}>\n <table className=\"w-full border-collapse text-left text-sm\">\n <thead>\n <tr className=\"border-b border-border text-[11px] uppercase tracking-wide text-muted-foreground\">\n <th className=\"py-1.5 pr-3 font-medium\">Target</th>\n <th className=\"py-1.5 pr-3 font-medium\">Claim</th>\n <th className=\"py-1.5 font-medium\">Source</th>\n </tr>\n </thead>\n <tbody>\n {evidence.map((entry) => {\n const url = resolveSourceUrl?.(entry)\n const locator = locatorLabel(entry)\n return (\n <tr key={entry.id} className=\"border-b border-border/50 align-top\">\n <td className=\"py-2 pr-3 font-mono text-xs text-foreground\">{entry.target}</td>\n <td className=\"py-2 pr-3 text-[13px] leading-snug text-foreground\">\n {entry.claim}\n {entry.locator.quote && (\n // The basis is what a reviewer weighs, so it has to be\n // visible per row, not only in the aggregate check detail.\n // A platform-sliced quote came out of the document's own\n // bytes; a model-typed one was proved to occur but was\n // transcribed. Same text, different strength of evidence.\n <span\n className={`mt-0.5 block border-l-2 pl-2 text-xs italic ${\n entry.locator.quoteBasis === 'span'\n ? 'border-primary/60 text-foreground'\n : 'border-border text-muted-foreground'\n }`}\n title={\n entry.locator.quoteBasis === 'span'\n ? 'Sliced from the source document by the platform'\n : entry.locator.quoteBasis === 'model'\n ? 'Quoted by the agent, verified to occur in the source'\n : 'Unverified quote'\n }\n >\n “{entry.locator.quote}”\n {entry.locator.quoteBasis && (\n <span className=\"ml-1.5 align-middle text-[10px] not-italic uppercase tracking-wide text-muted-foreground\">\n {entry.locator.quoteBasis === 'span' ? 'from source' : 'verified'}\n </span>\n )}\n </span>\n )}\n </td>\n <td className=\"py-2 text-xs\">\n {url ? (\n <a\n href={url}\n target=\"_blank\"\n rel=\"noreferrer\"\n className=\"font-mono text-primary underline-offset-2 hover:underline\"\n >\n {entry.sourceRef}\n </a>\n ) : (\n <span className=\"font-mono text-muted-foreground\">{entry.sourceRef}</span>\n )}\n {locator && <span className=\"ml-1.5 text-muted-foreground\">{locator}</span>}\n </td>\n </tr>\n )\n })}\n </tbody>\n </table>\n </div>\n )\n}\n\n// ── exceptions + checks ───────────────────────────────────────────────────\n\nconst SEVERITY_TONES: Record<ExceptionEntry['severity'], string> = {\n blocking: 'bg-destructive/10 text-destructive',\n material: 'bg-warning/10 text-warning',\n advisory: 'bg-muted/40 text-muted-foreground',\n}\n\n/** Properties for the severity-badged exception list */\nexport interface ExceptionListProps {\n exceptions: readonly ExceptionEntry[]\n className?: string\n}\n\n/** Severity-badged exception rows with resolution state. */\nexport function ExceptionList({ exceptions, className }: ExceptionListProps) {\n if (exceptions.length === 0) {\n return <p className={`text-xs text-muted-foreground ${className ?? ''}`}>No exceptions flagged.</p>\n }\n return (\n <ul className={`space-y-1.5 ${className ?? ''}`}>\n {exceptions.map((entry) => (\n <li key={entry.id} className=\"flex items-start gap-2.5 rounded-lg border border-border bg-card px-3 py-2\">\n <span className={`mt-0.5 inline-flex shrink-0 items-center rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide ${SEVERITY_TONES[entry.severity]}`}>\n {entry.severity}\n </span>\n <div className=\"min-w-0 flex-1\">\n <p className={`text-[13px] leading-snug ${entry.resolved ? 'text-muted-foreground line-through' : 'text-foreground'}`}>\n {entry.message}\n </p>\n <p className=\"mt-0.5 flex flex-wrap gap-x-2 text-[11px] text-muted-foreground\">\n <span className=\"font-mono\">{entry.kind}</span>\n {entry.targets && entry.targets.length > 0 && <span className=\"font-mono\">{entry.targets.join(', ')}</span>}\n {entry.resolved && <span>resolved{entry.resolvedBy ? ` by ${entry.resolvedBy}` : ''}</span>}\n {entry.resolutionNote && <span>{entry.resolutionNote}</span>}\n </p>\n </div>\n </li>\n ))}\n </ul>\n )\n}\n\n/** Properties for the pass/fail quality check list */\nexport interface QualityCheckListProps {\n checks: readonly QualityCheck[]\n className?: string\n}\n\n/** Pass/fail quality-check rows tagged with their source (agent self-report,\n * platform gate, judge ensemble). */\nexport function QualityCheckList({ checks, className }: QualityCheckListProps) {\n if (checks.length === 0) {\n return <p className={`text-xs text-muted-foreground ${className ?? ''}`}>No checks recorded.</p>\n }\n return (\n <ul className={`space-y-1 ${className ?? ''}`}>\n {checks.map((check) => (\n <li key={check.id} className=\"flex items-start gap-2 rounded-md px-1 py-1\">\n {check.passed ? (\n <svg viewBox=\"0 0 24 24\" className=\"mt-0.5 h-3.5 w-3.5 shrink-0 text-success\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n <polyline points=\"20 6 9 17 4 12\" />\n </svg>\n ) : (\n <svg viewBox=\"0 0 24 24\" className=\"mt-0.5 h-3.5 w-3.5 shrink-0 text-destructive\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" />\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" />\n </svg>\n )}\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-[13px] text-foreground\">\n {check.name}\n <span className=\"ml-1.5 text-[10px] uppercase tracking-wide text-muted-foreground\">{check.source}</span>\n </p>\n {check.detail && <p className=\"text-[11px] leading-snug text-muted-foreground\">{check.detail}</p>}\n </div>\n </li>\n ))}\n </ul>\n )\n}\n\n// ── provenance stamp ──────────────────────────────────────────────────────\n\n/** Properties for the provenance stamp with the optional backtest slot */\nexport interface ProvenanceStampProps {\n provenance: Pick<WorkProductProvenance, 'profileHash' | 'runId' | 'servingModels'> &\n Partial<Pick<WorkProductProvenance, 'costUsd' | 'producedAt'>>\n /** The product-resolved backtest summary for this profile hash. When its\n * trust gate failed, the composite renders as \"quality: unverified\" —\n * never a naked number. */\n backtest?: ProfileBacktestSummary\n className?: string\n}\n\n/** The audit line a reviewer approves against: which configuration produced\n * this document, what served it, and how that configuration measured on its\n * backtest. */\nexport function ProvenanceStamp({ provenance, backtest, className }: ProvenanceStampProps) {\n return (\n <div className={`flex flex-wrap items-center gap-x-2.5 gap-y-1 text-[11px] text-muted-foreground ${className ?? ''}`}>\n <span className=\"font-mono\" title={provenance.profileHash}>\n profile {provenance.profileHash ? provenance.profileHash.slice(0, 10) : '—'}\n </span>\n <span className=\"font-mono\" title={provenance.runId}>\n run {provenance.runId ? provenance.runId.slice(0, 10) : '—'}\n </span>\n {provenance.servingModels.length > 0 ? (\n provenance.servingModels.map((model) => (\n <span key={model} className=\"rounded-full bg-muted/40 px-2 py-0.5 font-mono\">\n {model}\n </span>\n ))\n ) : (\n <span className=\"italic\">serving model pending</span>\n )}\n {typeof provenance.costUsd === 'number' && <span>${provenance.costUsd.toFixed(provenance.costUsd < 0.01 ? 4 : 2)}</span>}\n {backtest &&\n (backtest.trust === 'pass' ? (\n <span className=\"text-success\">\n {backtest.cases} backtest cases · composite {backtest.composite.toFixed(2)} · trust PASS\n </span>\n ) : (\n <span className=\"text-warning\" title={backtest.trustReasons.join('; ')}>\n quality: unverified\n </span>\n ))}\n </div>\n )\n}\n"],"mappings":"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","names":[]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  persistedPartToWorkProduct
3
- } from "./chunk-F2CBC4DY.js";
3
+ } from "./chunk-V3HO43PG.js";
4
4
  import {
5
5
  persistedPartToInteraction
6
6
  } from "./chunk-3ZK5IJSW.js";
@@ -367,4 +367,4 @@ export {
367
367
  buildAttachmentPromptBlock,
368
368
  historyContentWithAttachments
369
369
  };
370
- //# sourceMappingURL=chunk-AFNTRJQ7.js.map
370
+ //# sourceMappingURL=chunk-FEUW4G2L.js.map
@@ -4,7 +4,7 @@ import {
4
4
  import {
5
5
  WorkProductCard,
6
6
  workProductPartsFromMessageParts
7
- } from "./chunk-UDSY2F6N.js";
7
+ } from "./chunk-72TPRENZ.js";
8
8
  import {
9
9
  ATTACHMENT_ACCEPT,
10
10
  ATTACHMENT_MAX_COUNT,
@@ -16,11 +16,11 @@ import {
16
16
  checkAttachmentType,
17
17
  sanitizeAttachmentFileName,
18
18
  sniffBinary
19
- } from "./chunk-V55WJSR4.js";
19
+ } from "./chunk-5GA3MKOC.js";
20
20
  import {
21
21
  attachmentPartsFromMessageParts,
22
22
  formatBytes
23
- } from "./chunk-AFNTRJQ7.js";
23
+ } from "./chunk-FEUW4G2L.js";
24
24
  import {
25
25
  cancelStatusFor,
26
26
  fieldAcceptsFreeText,
@@ -3956,4 +3956,4 @@ export {
3956
3956
  useThinkingSeconds,
3957
3957
  ChatMessages
3958
3958
  };
3959
- //# sourceMappingURL=chunk-TKRI463Z.js.map
3959
+ //# sourceMappingURL=chunk-J6TNPRQN.js.map
@@ -46,6 +46,20 @@ function parseEvidenceInput(raw, path = "entry") {
46
46
  if (typeof locatorRaw.quote !== "string") return fail(`${path}.locator.quote`, "must be a string when present");
47
47
  locator.quote = locatorRaw.quote;
48
48
  }
49
+ if (locatorRaw.span !== void 0) {
50
+ const spanRaw = asRecord(locatorRaw.span);
51
+ if (!spanRaw) return fail(`${path}.locator.span`, "must be an object { start, end } when present");
52
+ for (const field of ["start", "end"]) {
53
+ const value = spanRaw[field];
54
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
55
+ return fail(`${path}.locator.span.${field}`, "must be a non-negative integer character offset");
56
+ }
57
+ }
58
+ if (spanRaw.end <= spanRaw.start) {
59
+ return fail(`${path}.locator.span`, `end (${String(spanRaw.end)}) must be greater than start (${String(spanRaw.start)}) \u2014 the range is half-open [start, end)`);
60
+ }
61
+ locator.span = { start: spanRaw.start, end: spanRaw.end };
62
+ }
49
63
  const entry = {
50
64
  id: record.id.trim(),
51
65
  sourceRef: record.sourceRef.trim(),
@@ -190,4 +204,4 @@ export {
190
204
  workProductToPersistedPart,
191
205
  persistedPartToWorkProduct
192
206
  };
193
- //# sourceMappingURL=chunk-F2CBC4DY.js.map
207
+ //# sourceMappingURL=chunk-V3HO43PG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/work-product/types.ts"],"sourcesContent":["/**\n * The reviewable work-product contract — the one durable object every Tangle\n * agent product converges on: an artifact plus its evidence map (source→field\n * lineage), exceptions, quality checks, version history, and run provenance,\n * produced by an agent and signed off by a professional.\n *\n * Import-free and client-safe by construction: the queue projection and the\n * React surfaces re-validate these shapes at JSON boundaries, so every type\n * and codec here must load in a browser bundle. Every domain word — artifact\n * `kind`, evidence `target`, exception `kind`, check `name`, `scopeKey`\n * format — is a STRING PARAMETER validated against product-supplied\n * vocabularies in the tool layer; nothing domain-specific is baked here.\n */\n\n// ── identity / status ────────────────────────────────────────────────────────\n\n/** Stable pointer at one version of one work product. */\nexport interface WorkProductRef {\n id: string\n version: number\n}\n\nexport type WorkProductStatus =\n | 'draft' // agent is emitting incrementally (queue: working)\n | 'blocked' // unresolved blocking exception (queue: blocked / missing-info)\n | 'ready' // submit_work_product accepted (queue: ready-for-review)\n | 'changes_requested' // reviewer verdict; the correction re-enters chat as a turn\n | 'approved' // reviewer sign-off on THIS version\n | 'superseded' // a newer version replaced this one\n\nconst WORK_PRODUCT_STATUSES: readonly WorkProductStatus[] = [\n 'draft',\n 'blocked',\n 'ready',\n 'changes_requested',\n 'approved',\n 'superseded',\n]\n\n/** Runtime guard for re-validating a status read off a JSON boundary. */\nexport function isWorkProductStatus(value: unknown): value is WorkProductStatus {\n return typeof value === 'string' && (WORK_PRODUCT_STATUSES as readonly string[]).includes(value)\n}\n\n// ── artifact ─────────────────────────────────────────────────────────────────\n\n/** Define the reviewable artifact body with its kind, sources, and structured field map */\nexport interface WorkProductArtifact {\n /** PARAMETER: 'return_package' | 'redline' | 'outreach_campaign' — validated\n * against the product's `artifactKinds` vocabulary at submit. */\n kind: string\n title: string\n /** Vault/object-store ref for the rendered document (large bodies). */\n path?: string\n /** Inline body when small (markdown/JSON). */\n content?: string\n mediaType?: string\n /** Diff-first products (legal): the source document the artifact redlines. */\n baseline?: { path?: string; content?: string }\n /** The structured field map lineage targets anchor to (tax: form-line ids). */\n fields?: Record<string, unknown>\n}\n\n// ── evidence / lineage: source doc + locator → field/claim ───────────────────\n\n/** A half-open `[start, end)` character range into the source document's text.\n * Offsets index exactly the string the product's `readSourceText` seam\n * returns for the same `sourceRef` — which is the string its document-reading\n * tool pages with an `offset`. The PLATFORM slices `locator.quote` out of it,\n * so a span citation cannot name text the document does not contain. */\nexport interface EvidenceSpan {\n start: number\n end: number\n}\n\n/** How `locator.quote` got there. Server-set on every path — never read from\n * model args, because the whole value of the distinction is that a reviewer\n * can trust it:\n * - `span` the platform sliced it out of the source bytes (unfalsifiable)\n * - `model` the model supplied the text and the platform PROVED it occurs */\nexport type QuoteBasis = 'span' | 'model'\n\n/** Point into a source document: page, free-form range, span, verbatim quote */\nexport interface EvidenceLocator {\n page?: number\n /** 'L120-L134' | 'B7' | '¶4' — free-form, non-empty when present. */\n range?: string\n /** Verbatim supporting quote from the source. Model-supplied and verified,\n * or platform-sliced from {@link EvidenceLocator.span} — read `quoteBasis`\n * to tell which. */\n quote?: string\n /** Character range the quote is sliced from. The preferred citation form:\n * two integers cannot be a fabricated quote. */\n span?: EvidenceSpan\n /** Server-set provenance for `quote`; a model-supplied value is discarded. */\n quoteBasis?: QuoteBasis\n}\n\n/** One lineage row: a source document location supporting one artifact claim */\nexport interface EvidenceEntry {\n /** Agent-supplied stable id — the same id re-emitted is an upsert/replace. */\n id: string\n /** Vault path / attachment id / object-store key of the SOURCE document. */\n sourceRef: string\n locator: EvidenceLocator\n /** Artifact field/claim the evidence supports: '1040.line_9' |\n * 'clause:indemnification' | 'icp.employee_count' — product vocabulary. */\n target: string\n /** The value/assertion at the target. */\n claim: string\n /** 0..1; absent = unstated (never defaulted). */\n confidence?: number\n}\n\n// ── exceptions ───────────────────────────────────────────────────────────────\n\nexport type ExceptionSeverity = 'blocking' | 'material' | 'advisory'\n\n/** One flagged problem with the work product, resolvable by agent or reviewer */\nexport interface ExceptionEntry {\n id: string\n severity: ExceptionSeverity\n /** PARAMETER: 'missing_document' | 'inconsistent_source' | … — validated\n * against the product's `exceptionKinds` vocabulary. */\n kind: string\n message: string\n /** Affected artifact targets. */\n targets?: string[]\n resolved: boolean\n resolvedBy?: 'agent' | 'reviewer'\n resolutionNote?: string\n}\n\n/** Unresolved blocking exceptions are what park a work product in `blocked`. */\nexport function unresolvedBlockingExceptions(exceptions: readonly ExceptionEntry[]): ExceptionEntry[] {\n return exceptions.filter((entry) => entry.severity === 'blocking' && !entry.resolved)\n}\n\n// ── quality checks ───────────────────────────────────────────────────────────\n\n/** One named quality verdict on the work product, tagged with who computed it */\nexport interface QualityCheck {\n id: string\n /** Product vocabulary: 'totals_reconcile', 'evidence_coverage', … */\n name: string\n passed: boolean\n detail?: string\n /** agent self-report | shell-computed | eval-ensemble verdict. */\n source: 'agent' | 'platform' | 'judge'\n}\n\n// ── provenance (the backtest spine) ──────────────────────────────────────────\n\n/** The audit triple binding a work product to the exact configuration and run\n * that produced it. `profileHash` is agent-eval's `agentProfileHash()` of the\n * EXACT shipped profile — the same hash that keys scorecard cells, so a\n * reviewer can see the measured backtest history of the configuration that\n * produced the document. */\nexport interface WorkProductProvenance {\n profileHash: string\n /** Chat turnId or mission-step run id. */\n runId: string\n /** Back-filled from the usage receipt / serving-model header at turn\n * completion — only the completed turn knows what actually served. */\n servingModels: string[]\n sessionId?: string\n missionRef?: { missionId: string; stepId: string }\n costUsd?: number\n producedAt: number\n}\n\n/** The product-resolved backtest summary for one profile hash (from its own\n * eval artifacts via agent-eval's `loadScorecard`) — the shell defines only\n * this TYPE and the `ProvenanceStamp` slot that renders it. */\nexport interface ProfileBacktestSummary {\n profileHash: string\n /** Backtest cases behind the composite. */\n cases: number\n composite: number\n /** The eval-campaign trust gate's verdict on whether the composite is\n * allowed to be believed; 'fail' renders as \"quality: unverified\". */\n trust: 'pass' | 'fail'\n trustReasons: string[]\n}\n\n// ── version history ──────────────────────────────────────────────────────────\n\n/** Frozen milestone row for one version of the work product */\nexport interface WorkProductVersionEntry {\n version: number\n status: WorkProductStatus\n provenance: WorkProductProvenance\n /** Frozen snapshot ref of this version's body (object-store key) — the\n * DiffView input for vN-1 vs vN. */\n artifactPath?: string\n reviewedBy?: string\n reviewNote?: string\n at: number\n}\n\n// ── the durable row ──────────────────────────────────────────────────────────\n\n/** Define the durable work product row accumulating artifact, lineage, exceptions, checks, and history */\nexport interface WorkProductRecord {\n /** Minted server-side; the model NEVER supplies ids — it addresses work by\n * `scopeKey`. */\n id: string\n workspaceId: string\n /** The chat thread that drives it (chat stays first). */\n threadId: string | null\n /** Product engagement key: 'return:acme:2025' | 'contract:acme-msa' |\n * 'campaign:cpa-pilots'. */\n scopeKey: string\n status: WorkProductStatus\n version: number\n /** `null` while the draft accumulates (evidence streams in before the\n * artifact arrives); non-null from the submit transition onward. Typed\n * honestly rather than forcing a placeholder artifact on every draft. */\n artifact: WorkProductArtifact | null\n evidence: EvidenceEntry[]\n exceptions: ExceptionEntry[]\n checks: QualityCheck[]\n provenance: WorkProductProvenance\n history: WorkProductVersionEntry[]\n createdAt: number\n updatedAt: number\n}\n\n// ── store port — the /missions MissionStorePort pattern ──────────────────────\n\n/** Fields a guarded write compares against the values the caller read. An\n * absent field is unguarded. A SQL implementation compares like-for-like\n * scalar columns; the in-memory store does the same. */\nexport interface WorkProductUpdateGuard {\n status?: WorkProductStatus\n version?: number\n}\n\n/** Fields a guarded write sets when the guard holds. */\nexport interface WorkProductPatch {\n status?: WorkProductStatus\n version?: number\n artifact?: WorkProductArtifact\n evidence?: EvidenceEntry[]\n exceptions?: ExceptionEntry[]\n checks?: QualityCheck[]\n provenance?: WorkProductProvenance\n history?: WorkProductVersionEntry[]\n updatedAt?: number\n}\n\n/** One audit-trail row, appended after every committed state change. */\nexport interface WorkProductAuditEvent {\n workProductId: string\n workspaceId: string\n /** Machine-readable transition name ('wp.created' | 'wp.evidence' |\n * 'wp.ready' | 'wp.verdict' | …). */\n step: string\n message: string\n metadata: Record<string, unknown>\n at: number\n}\n\n/**\n * Persistence seam — the product implements this over its own tables. The\n * invariant the implementation MUST keep: `update` applies `patch` ONLY when\n * every guard field still equals the stored value, and returns `null` when\n * the guard misses — a typed conflict, never a clobber.\n */\nexport interface WorkProductStorePort {\n load(id: string): Promise<WorkProductRecord | null>\n /** The scope's single OPEN accumulator row — status `draft` or `blocked` —\n * or null when the scope has only reviewed/terminal rows (or none). The\n * `changes_requested` row is found via {@link listByWorkspace}; reopening\n * it is the service's job. */\n findDraft(workspaceId: string, scopeKey: string): Promise<WorkProductRecord | null>\n listByWorkspace(workspaceId: string, opts?: { status?: WorkProductStatus[] }): Promise<WorkProductRecord[]>\n /** `extras` are opaque product-column values written in the SAME statement\n * as the record (single-write creation), or ignored when the table has no\n * extra columns. */\n insert(record: WorkProductRecord, extras?: Record<string, unknown>): Promise<WorkProductRecord>\n /** CAS: apply `patch` ONLY when guard fields equal stored values; null on a\n * miss — typed conflict, never a clobber. */\n update(id: string, guard: WorkProductUpdateGuard, patch: WorkProductPatch): Promise<WorkProductRecord | null>\n appendEvent(event: WorkProductAuditEvent): Promise<void>\n}\n\n// ── input codecs (strict, named-field errors) ────────────────────────────────\n\n/** Validation result whose failure names the exact field, so a tool layer can\n * hand the model a correctable error it can act on. */\nexport type WorkProductParseResult<T> =\n | { ok: true; value: T }\n | { ok: false; field: string; error: string }\n\nfunction fail<T>(field: string, error: string): WorkProductParseResult<T> {\n return { ok: false, field, error }\n}\n\nfunction nonEmptyString(value: unknown): value is string {\n return typeof value === 'string' && value.trim().length > 0\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> | null {\n return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : null\n}\n\n/** Validate one raw evidence entry field-by-field. `path` prefixes the failure\n * field name (e.g. `entries[3]`) so batched calls name the exact offender. */\nexport function parseEvidenceInput(raw: unknown, path = 'entry'): WorkProductParseResult<EvidenceEntry> {\n const record = asRecord(raw)\n if (!record) return fail(path, 'must be an object')\n if (!nonEmptyString(record.id)) return fail(`${path}.id`, 'must be a non-empty string (stable per entry; re-emitting the same id replaces it)')\n if (!nonEmptyString(record.sourceRef)) return fail(`${path}.sourceRef`, 'must be a non-empty source document ref')\n if (!nonEmptyString(record.target)) return fail(`${path}.target`, 'must name the artifact field/claim this evidence supports')\n if (!nonEmptyString(record.claim)) return fail(`${path}.claim`, 'must state the value/assertion at the target')\n const locatorRaw = record.locator === undefined ? {} : asRecord(record.locator)\n if (!locatorRaw) return fail(`${path}.locator`, 'must be an object when present')\n const locator: EvidenceLocator = {}\n if (locatorRaw.page !== undefined) {\n if (typeof locatorRaw.page !== 'number' || !Number.isFinite(locatorRaw.page)) {\n return fail(`${path}.locator.page`, 'must be a finite number when present')\n }\n locator.page = locatorRaw.page\n }\n if (locatorRaw.range !== undefined) {\n if (!nonEmptyString(locatorRaw.range)) return fail(`${path}.locator.range`, 'must be a non-empty string when present')\n locator.range = locatorRaw.range\n }\n if (locatorRaw.quote !== undefined) {\n if (typeof locatorRaw.quote !== 'string') return fail(`${path}.locator.quote`, 'must be a string when present')\n locator.quote = locatorRaw.quote\n }\n if (locatorRaw.span !== undefined) {\n const spanRaw = asRecord(locatorRaw.span)\n if (!spanRaw) return fail(`${path}.locator.span`, 'must be an object { start, end } when present')\n for (const field of ['start', 'end'] as const) {\n const value = spanRaw[field]\n if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {\n return fail(`${path}.locator.span.${field}`, 'must be a non-negative integer character offset')\n }\n }\n if ((spanRaw.end as number) <= (spanRaw.start as number)) {\n return fail(`${path}.locator.span`, `end (${String(spanRaw.end)}) must be greater than start (${String(spanRaw.start)}) — the range is half-open [start, end)`)\n }\n locator.span = { start: spanRaw.start as number, end: spanRaw.end as number }\n }\n // `quoteBasis` is server-set. A model-supplied value is DROPPED rather than\n // rejected: it is not a correctable mistake the model should burn a turn on,\n // and silently honouring it would let a model label an unverified quote\n // 'span'. The tool layer stamps the real basis after resolution.\n const entry: EvidenceEntry = {\n id: (record.id as string).trim(),\n sourceRef: (record.sourceRef as string).trim(),\n locator,\n target: (record.target as string).trim(),\n claim: record.claim as string,\n }\n if (record.confidence !== undefined) {\n if (typeof record.confidence !== 'number' || !(record.confidence >= 0 && record.confidence <= 1)) {\n return fail(`${path}.confidence`, 'must be a number in 0..1 when present')\n }\n entry.confidence = record.confidence\n }\n return { ok: true, value: entry }\n}\n\nconst EXCEPTION_SEVERITIES: readonly ExceptionSeverity[] = ['blocking', 'material', 'advisory']\n\n/** Validate one raw exception entry field-by-field. Kind MEMBERSHIP in the\n * product vocabulary is the tool layer's check (it owns the config). */\nexport function parseExceptionInput(raw: unknown, path = 'exception'): WorkProductParseResult<ExceptionEntry> {\n const record = asRecord(raw)\n if (!record) return fail(path, 'must be an object')\n if (!nonEmptyString(record.id)) return fail(`${path}.id`, 'must be a non-empty string (stable per entry; re-emitting the same id replaces it)')\n if (!EXCEPTION_SEVERITIES.includes(record.severity as ExceptionSeverity)) {\n return fail(`${path}.severity`, `must be one of: ${EXCEPTION_SEVERITIES.join(', ')}`)\n }\n if (!nonEmptyString(record.kind)) return fail(`${path}.kind`, 'must be a non-empty exception kind')\n if (!nonEmptyString(record.message)) return fail(`${path}.message`, 'must describe the problem')\n const entry: ExceptionEntry = {\n id: (record.id as string).trim(),\n severity: record.severity as ExceptionSeverity,\n kind: (record.kind as string).trim(),\n message: record.message as string,\n resolved: record.resolved === true,\n }\n if (record.targets !== undefined) {\n if (!Array.isArray(record.targets) || !record.targets.every(nonEmptyString)) {\n return fail(`${path}.targets`, 'must be an array of non-empty strings when present')\n }\n entry.targets = record.targets as string[]\n }\n if (record.resolvedBy !== undefined) {\n if (record.resolvedBy !== 'agent' && record.resolvedBy !== 'reviewer') {\n return fail(`${path}.resolvedBy`, \"must be 'agent' or 'reviewer' when present\")\n }\n entry.resolvedBy = record.resolvedBy\n }\n if (record.resolutionNote !== undefined) {\n if (typeof record.resolutionNote !== 'string') return fail(`${path}.resolutionNote`, 'must be a string when present')\n entry.resolutionNote = record.resolutionNote\n }\n return { ok: true, value: entry }\n}\n\n/** Validate a raw artifact. Kind MEMBERSHIP in `artifactKinds` is the tool\n * layer's check. An artifact must carry a body: at least one of `path`,\n * `content`, or `fields`. */\nexport function parseArtifactInput(raw: unknown, path = 'artifact'): WorkProductParseResult<WorkProductArtifact> {\n const record = asRecord(raw)\n if (!record) return fail(path, 'must be an object')\n if (!nonEmptyString(record.kind)) return fail(`${path}.kind`, 'must be a non-empty artifact kind')\n if (!nonEmptyString(record.title)) return fail(`${path}.title`, 'must be a non-empty title')\n for (const key of ['path', 'content', 'mediaType'] as const) {\n if (record[key] !== undefined && !nonEmptyString(record[key])) {\n return fail(`${path}.${key}`, 'must be a non-empty string when present')\n }\n }\n let baseline: WorkProductArtifact['baseline']\n if (record.baseline !== undefined) {\n const baselineRecord = asRecord(record.baseline)\n if (!baselineRecord) return fail(`${path}.baseline`, 'must be an object when present')\n baseline = {}\n if (baselineRecord.path !== undefined) {\n if (!nonEmptyString(baselineRecord.path)) return fail(`${path}.baseline.path`, 'must be a non-empty string when present')\n baseline.path = (baselineRecord.path as string).trim()\n }\n if (baselineRecord.content !== undefined) {\n if (typeof baselineRecord.content !== 'string') return fail(`${path}.baseline.content`, 'must be a string when present')\n baseline.content = baselineRecord.content\n }\n }\n let fields: Record<string, unknown> | undefined\n if (record.fields !== undefined) {\n const fieldsRecord = asRecord(record.fields)\n if (!fieldsRecord) return fail(`${path}.fields`, 'must be an object when present')\n fields = fieldsRecord\n }\n if (record.path === undefined && record.content === undefined && fields === undefined) {\n return fail(path, 'must carry a body: at least one of path, content, or fields')\n }\n const artifact: WorkProductArtifact = {\n kind: (record.kind as string).trim(),\n title: (record.title as string).trim(),\n }\n if (record.path !== undefined) artifact.path = (record.path as string).trim()\n if (record.content !== undefined) artifact.content = record.content as string\n if (record.mediaType !== undefined) artifact.mediaType = (record.mediaType as string).trim()\n if (baseline !== undefined) artifact.baseline = baseline\n if (fields !== undefined) artifact.fields = fields\n return { ok: true, value: artifact }\n}\n\n/** Agent self-reported check input for `submit_work_product` (persisted with\n * `source: 'agent'`; `platform`/`judge` sources are never model-suppliable). */\nexport interface AgentCheckInput {\n id: string\n name: string\n passed: boolean\n detail?: string\n}\n\n/** Validate one agent self-check ensuring identifiers, verdict flag, and optional detail are well formed */\nexport function parseAgentCheckInput(raw: unknown, path = 'check'): WorkProductParseResult<AgentCheckInput> {\n const record = asRecord(raw)\n if (!record) return fail(path, 'must be an object')\n if (!nonEmptyString(record.id)) return fail(`${path}.id`, 'must be a non-empty string')\n if (!nonEmptyString(record.name)) return fail(`${path}.name`, 'must be a non-empty check name')\n if (typeof record.passed !== 'boolean') return fail(`${path}.passed`, 'must be a boolean')\n const check: AgentCheckInput = {\n id: (record.id as string).trim(),\n name: (record.name as string).trim(),\n passed: record.passed,\n }\n if (record.detail !== undefined) {\n if (typeof record.detail !== 'string') return fail(`${path}.detail`, 'must be a string when present')\n check.detail = record.detail\n }\n return { ok: true, value: check }\n}\n\n// ── chat transcript anchor (system-authored) ─────────────────────────────────\n\n/**\n * The persisted `type:'work_product'` transcript anchor — the chat card that\n * keeps chat the driver surface for review. The PLATFORM writes it on the\n * ready transition (and updates it on a verdict); no prompt ever teaches an\n * agent to author one. `/chat-store` aliases this as `ChatWorkProductPart` in\n * its stored-part union, exactly like the `interaction`/`plan` members.\n */\nexport interface WorkProductPersistedPart {\n type: 'work_product'\n ref: WorkProductRef\n kind: string\n title: string\n status: WorkProductStatus\n}\n\n/** Project the transcript anchor part from a record. Requires the submitted\n * artifact (the anchor is written on the ready transition, after which\n * `artifact` is always non-null); falls back to the scopeKey label for\n * defensive callers on an accumulating draft. */\nexport function workProductToPersistedPart(record: WorkProductRecord): WorkProductPersistedPart {\n return {\n type: 'work_product',\n ref: { id: record.id, version: record.version },\n kind: record.artifact?.kind ?? '',\n title: record.artifact?.title ?? record.scopeKey,\n status: record.status,\n }\n}\n\n/** Re-validate a stored/wire part into the typed anchor; null for junk. */\nexport function persistedPartToWorkProduct(part: Record<string, unknown>): WorkProductPersistedPart | null {\n if (!part || part.type !== 'work_product') return null\n const ref = asRecord(part.ref)\n if (!ref || !nonEmptyString(ref.id) || typeof ref.version !== 'number' || !Number.isFinite(ref.version)) return null\n if (typeof part.kind !== 'string' || !nonEmptyString(part.title) || !isWorkProductStatus(part.status)) return null\n return {\n type: 'work_product',\n ref: { id: ref.id as string, version: ref.version },\n kind: part.kind,\n title: part.title as string,\n status: part.status,\n }\n}\n"],"mappings":"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","names":[]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  unresolvedBlockingExceptions
3
- } from "./chunk-F2CBC4DY.js";
3
+ } from "./chunk-V3HO43PG.js";
4
4
 
5
5
  // src/work-product/queue.ts
6
6
  var OPEN_STATUSES = /* @__PURE__ */ new Set(["draft", "blocked", "ready", "changes_requested"]);
@@ -138,4 +138,4 @@ export {
138
138
  projectReviewQueue,
139
139
  parseReviewQueueItem
140
140
  };
141
- //# sourceMappingURL=chunk-UP33Z633.js.map
141
+ //# sourceMappingURL=chunk-ZRSUCTST.js.map
@@ -1,6 +1,6 @@
1
1
  import { Part } from '@tangle-network/agent-interface';
2
2
  import { a as ChatInteractionField, C as ChatInteractionStatus, I as InteractionAnswers, N as NoticeKind } from './contract-CEewO6DI.js';
3
- import { c as WorkProductPersistedPart } from './types-XIZqIdX1.js';
3
+ import { c as WorkProductPersistedPart } from './types-DeEOhQbv.js';
4
4
  import { ChatPlanPersistedPart } from './plans/index.js';
5
5
 
6
6
  /**
@@ -1,4 +1,4 @@
1
- import { W as WorkProductRef, a as WorkProductProvenance, b as WorkProductRecord } from './types-XIZqIdX1.js';
1
+ import { W as WorkProductRef, a as WorkProductProvenance, b as WorkProductRecord } from './types-DeEOhQbv.js';
2
2
 
3
3
  /**
4
4
  * The review queue is a PROJECTION, not a store — a client-safe pure fold of
@@ -67,6 +67,55 @@ type TurnHealthReason =
67
67
  | {
68
68
  kind: 'turn_failed';
69
69
  reason: string;
70
+ }
71
+ /** A tool call the harness REJECTED, settled as `completed`.
72
+ *
73
+ * Found live in legal-agent production, and missed by every other rule here:
74
+ * the model called `submit_proposal`, the harness answered "Model tried to
75
+ * call unavailable tool 'submit_proposal'", and the part persisted as
76
+ * `{"tool":"invalid","state":{"status":"completed","input":{"error":"…"}}}`.
77
+ *
78
+ * Status is `completed`, the arguments parse cleanly, and the turn has text —
79
+ * so the blank-completion, malformed-argument and no-effect rules all pass it.
80
+ * Six deliverables were requested and silently discarded while the product
81
+ * reported success six times.
82
+ *
83
+ * Detected structurally, on the presence of an `error` in the settled state
84
+ * rather than on any harness's name for a rejected call — `invalid` is one
85
+ * harness's convention and must not be baked into the shell. */
86
+ | {
87
+ kind: 'tool_call_rejected';
88
+ /** The tool the model was trying to reach, when the payload names it. */
89
+ tool: string;
90
+ error: string;
91
+ }
92
+ /** The turn was answered without the model ever running — a pre-producer gate
93
+ * short-circuited and returned the product's own response.
94
+ *
95
+ * Reported as a `warning`, never `critical`: gating an unready turn is a
96
+ * legitimate design (an intake flow SHOULD answer before spending a model
97
+ * call). What is pathological is the RATE, which only the caller's own
98
+ * threshold can judge — so this variant exists to be COUNTED, and alerting
99
+ * on it is opt-in. It is the per-turn evidence behind a dead tool surface:
100
+ * a gate that answers every turn means the agent never runs at all. */
101
+ | {
102
+ kind: 'answered_without_model';
103
+ }
104
+ /** The detector could not read this turn.
105
+ *
106
+ * Every part carried a type outside the vocabulary this classifier
107
+ * understands — which is what field-level encryption at rest looks like from
108
+ * the outside (tax-agent persists `{"type":"__encrypted_parts__"}`, its own
109
+ * convention, and 32 of its 129 assistant rows are exactly that).
110
+ *
111
+ * This exists because the alternative is the bug this whole module hunts.
112
+ * An unreadable row has no text, no artifact and no tool part, so every
113
+ * other rule here would happily conclude "nothing wrong" — a detector
114
+ * reporting health from data it cannot see. Blindness is a finding, not a
115
+ * pass, so it gets its own reason and its own counter. */
116
+ | {
117
+ kind: 'unreadable_turn';
118
+ partTypes: string[];
70
119
  };
71
120
  /** A settled turn, in the narrowest shape both call sites can supply.
72
121
  *
@@ -86,6 +135,10 @@ interface TurnOutcomeInput {
86
135
  failed?: boolean;
87
136
  failureReason?: string | null;
88
137
  durationMs?: number;
138
+ /** Set when a pre-producer gate answered this turn and the model never ran.
139
+ * Supplied by `/chat-routes`' lifecycle seam, which stamps `gated` on the
140
+ * completion it now fires for a `contextGate` short-circuit. */
141
+ gated?: boolean;
89
142
  }
90
143
  /** The verdict for one turn. `healthy` is exactly `reasons.length === 0`, kept
91
144
  * as a field so callers read intent rather than an array length. */
@@ -93,6 +146,37 @@ interface TurnHealthVerdict {
93
146
  healthy: boolean;
94
147
  severity: TurnHealthSeverity | null;
95
148
  reasons: TurnHealthReason[];
149
+ /** How many tool parts this turn carried.
150
+ *
151
+ * Zero is NOT a per-turn defect — plenty of good turns answer from context
152
+ * without touching a tool, and paging on each one would be pure noise. It is
153
+ * reported so a WINDOW can be judged: a product whose deliverable is tool
154
+ * output and which produced zero tool calls across every turn in the
155
+ * lookback has a dead tool surface, and that is the shape no per-turn rule
156
+ * can see. (Measured: tax-agent, 129 of 129 assistant rows all-time.) */
157
+ toolCalls: number;
158
+ /** True when nothing about this turn could be judged — no interpretable part
159
+ * AND no visible text. Callers MUST exclude these from any healthy/unhealthy
160
+ * ratio, because counting an unreadable turn as healthy is how a detector
161
+ * reports green on data it never read. */
162
+ unreadable: boolean;
163
+ /** False when this turn carried parts that could not be interpreted.
164
+ *
165
+ * Separate from {@link unreadable} because the two blindnesses have
166
+ * different consequences, and production has a row that is one but not the
167
+ * other: tax-agent persists CIPHERTEXT as `content` alongside an encrypted
168
+ * `parts` blob, so the turn plainly delivered something (there is text) while
169
+ * its tool calls are completely invisible.
170
+ *
171
+ * Any conclusion ABOUT TOOLS — above all the dead-tool-surface verdict — may
172
+ * only be drawn over turns where this is true. Reading "no tool parts" off an
173
+ * encrypted blob and declaring the tool surface dead would be the same
174
+ * crime as declaring it healthy: a finding asserted from data never read. */
175
+ partsReadable: boolean;
176
+ /** The part types that could not be interpreted. Empty when
177
+ * {@link partsReadable}. Named in the alert so a reader can see EXACTLY what
178
+ * the detector was blind to instead of taking "unreadable" on faith. */
179
+ opaquePartTypes: string[];
96
180
  }
97
181
  /**
98
182
  * Judge one settled turn.
@@ -172,6 +256,27 @@ declare function createWebhookAlertSink(options: {
172
256
  webhookUrl: string;
173
257
  fetchImpl?: FetchLike;
174
258
  }): AlertSink;
259
+ /**
260
+ * POST to Slack `chat.postMessage` with a bot token.
261
+ *
262
+ * This is the transport the org actually has. A survey of the four product
263
+ * repos found NO ops alert path of any kind — no incoming webhook, no pager, no
264
+ * notifier — which is the mechanical reason a 17-day outage never reached a
265
+ * human. What does exist is a Slack bot token in the shared secrets store, and
266
+ * `@tangle-network/agent-integrations` already speaks this exact API, so this
267
+ * routes alerts through the channel the org runs rather than standing up a new
268
+ * one.
269
+ *
270
+ * Slack answers `200 OK` with `{"ok": false, "error": "..."}` for an invalid
271
+ * token or channel, so the body is checked and not just the status — a
272
+ * transport that reports success on a rejected post would make the alerter
273
+ * itself a silent failure.
274
+ */
275
+ declare function createSlackBotAlertSink(options: {
276
+ botToken: string;
277
+ channel: string;
278
+ fetchImpl?: FetchLike;
279
+ }): AlertSink;
175
280
  /** stderr sink. The zero-config fallback so a product that has not yet been
176
281
  * given a webhook still emits something a log search can find. */
177
282
  declare function createConsoleAlertSink(log?: (message: string) => void): AlertSink;
@@ -231,6 +336,9 @@ interface TurnHealthCompleteInfo {
231
336
  threadId?: string;
232
337
  turnStreamId?: string;
233
338
  executionId?: string;
339
+ /** `/chat-routes` sets this when a `contextGate` short-circuited the turn and
340
+ * the producer never ran. */
341
+ gated?: boolean;
234
342
  }
235
343
  /** Structural mirror of the lifecycle error payload. */
236
344
  interface TurnHealthErrorInfo {
@@ -258,6 +366,15 @@ interface TurnHealthLifecycleOptions {
258
366
  kinds: string[];
259
367
  durationMs: number;
260
368
  }): void;
369
+ /** Page when a turn was answered by a gate instead of the model.
370
+ *
371
+ * Default `false`, and the default is the honest one: gating is a legitimate
372
+ * design and a product that gates its intake would otherwise page on every
373
+ * healthy turn. Whether the rate is pathological is domain knowledge, so it
374
+ * stays a product decision — the verdict is ALWAYS reported through
375
+ * {@link TurnHealthLifecycleOptions.onVerdict} so a counter can watch the
376
+ * rate even when nobody is paged. */
377
+ alertOnGatedTurn?: boolean;
261
378
  }
262
379
  /**
263
380
  * Build the lifecycle hooks that page on a turn which succeeded at nothing.
@@ -353,6 +470,22 @@ interface SweepOptions {
353
470
  emptyRateThreshold?: number;
354
471
  /** Absolute floor: never page on a rate computed from fewer turns than this. */
355
472
  minTurnsForRate?: number;
473
+ /** Declare that this product's deliverable comes from TOOL calls, which
474
+ * switches on the dead-tool-surface detector.
475
+ *
476
+ * Opt-in because only the product knows: a copilot that answers from context
477
+ * is perfectly healthy with zero tool calls, while an agent whose entire job
478
+ * is to file, draft, or submit something is broken the moment its tool
479
+ * surface goes quiet — and broken INVISIBLY, because every turn still
480
+ * returns fluent prose and HTTP 200.
481
+ *
482
+ * This is the fourth failure shape, and the only one no per-turn rule can
483
+ * see. Measured on production: tax-agent has 129 assistant turns across 64
484
+ * threads, all-time, with zero tool parts — while every other detector in
485
+ * this module reports it healthy. */
486
+ expectsToolCalls?: boolean;
487
+ /** Turns needed before a dead tool surface is called. Default 10. */
488
+ minTurnsForToolSurface?: number;
356
489
  now?: number;
357
490
  }
358
491
  /** What the sweep found. Returned as well as alerted, so a cron can log it and
@@ -367,20 +500,23 @@ interface SweepResult {
367
500
  emptyCompletions: number;
368
501
  malformedToolCalls: number;
369
502
  toolCallsWithoutEffect: number;
503
+ /** Tool calls the harness rejected while settling them as `completed`. */
504
+ rejectedToolCalls: number;
505
+ /** Turns carrying at least one tool part. */
506
+ turnsWithToolCalls: number;
507
+ /** Total tool parts across the window. */
508
+ toolCalls: number;
509
+ /** Turns the classifier could not interpret at all (encrypted at rest, or a
510
+ * part vocabulary this module does not know). These are EXCLUDED from
511
+ * `turnsJudged`-based rates — a rate computed over rows nobody could read is
512
+ * a fabricated number. */
513
+ unreadableTurns: number;
514
+ /** Turns whose PARTS could not be interpreted. A superset of
515
+ * {@link unreadableTurns} (a row can have readable text and opaque parts).
516
+ * No tool verdict is drawn over these. */
517
+ opaquePartsTurns: number;
370
518
  alerts: TurnHealthAlert[];
371
519
  }
372
- /**
373
- * Assistant-row openers agent-app writes ITSELF when a sandbox turn fails.
374
- *
375
- * Kept byte-identical to the strings `createSandboxChatProducer` composes
376
- * (`src/chat-routes/sandbox-producer.ts`). They are shell vocabulary, not
377
- * product domain, so recognising them is this package's job — a product on
378
- * the shared producer gets a correct sweep with no configuration.
379
- *
380
- * `tests/turn-health/turn-health.test.ts` pins these against the producer, so
381
- * changing the producer's wording without changing this list fails CI rather
382
- * than silently making dead threads look answered.
383
- */
384
520
  declare const SHELL_ERROR_REPLY_PREFIXES: readonly string[];
385
521
  /**
386
522
  * Run one sweep and deliver whatever it finds.
@@ -451,4 +587,4 @@ declare function createD1TurnHealthSource(db: D1LikeForHealth, options?: {
451
587
  errorReplyPrefixes?: readonly string[];
452
588
  }): TurnHealthSource;
453
589
 
454
- export { type AlertSink, type AlertThrottleStore, type D1LikeForHealth, type FetchLike, type PersistedTurnRow, SHELL_ERROR_REPLY_PREFIXES, type SweepOptions, type SweepResult, type TurnHealthAlert, type TurnHealthCompleteInfo, type TurnHealthErrorInfo, type TurnHealthLifecycle, type TurnHealthLifecycleOptions, type TurnHealthReason, type TurnHealthSeverity, type TurnHealthSource, type TurnHealthVerdict, type TurnOutcomeInput, type UnansweredThread, classifyTurnOutcome, createConsoleAlertSink, createD1TurnHealthSource, createGuardedAlertSink, createMemoryThrottleStore, createMultiAlertSink, createThrottledAlertSink, createTurnHealthLifecycle, createWebhookAlertSink, describeReason, sweepSilentFailures, turnAlert };
590
+ export { type AlertSink, type AlertThrottleStore, type D1LikeForHealth, type FetchLike, type PersistedTurnRow, SHELL_ERROR_REPLY_PREFIXES, type SweepOptions, type SweepResult, type TurnHealthAlert, type TurnHealthCompleteInfo, type TurnHealthErrorInfo, type TurnHealthLifecycle, type TurnHealthLifecycleOptions, type TurnHealthReason, type TurnHealthSeverity, type TurnHealthSource, type TurnHealthVerdict, type TurnOutcomeInput, type UnansweredThread, classifyTurnOutcome, createConsoleAlertSink, createD1TurnHealthSource, createGuardedAlertSink, createMemoryThrottleStore, createMultiAlertSink, createSlackBotAlertSink, createThrottledAlertSink, createTurnHealthLifecycle, createWebhookAlertSink, describeReason, sweepSilentFailures, turnAlert };