diff --git a/docs/BACKLOG.md b/docs/BACKLOG.md index 2160afcaa..2c37ebde4 100644 --- a/docs/BACKLOG.md +++ b/docs/BACKLOG.md @@ -2893,6 +2893,24 @@ def route_demo_oru(msg): > ๐Ÿ”ข **Re-scored 2026-08-20 -> P2.** Value **5/10** ยท Difficulty **4/10** ยท _fill-in_. None of the step-scoped run symbols exist in ide/src, so the feature is unstarted, but the substrate it needs is shipped: the traced dry-run, the line-containment fold and the sample picker are all present. Value is capped at parity with a clean workaround, since running the whole handler in the Test Bench already works; difficulty sits at 4 because the stop condition and state dump ride an existing path while the pin mechanism, the redaction reuse and the IDE surface cross the CLI-to-extension seam. _(was 5/10 ยท 4/10.)_ > +> **PARTIAL 2026-08-25 -- THE "NOT AS AN AFTERTHOUGHT" WRINKLE IS FIXED (PR #605). THE STEP-SCOPED +> FEATURE ITSELF IS STILL UNSTARTED.** A skipped `db_lookup`/`fhir_lookup` used to render "live +> lookup - not evaluated in preview" and dead-end there; it now points at ADR 0010:62's supported +> preview path (stub the call's wrapper) instead. Also fixed: `mergeLiveValues` had dropped the +> annotation's kind, so a skipped lookup inherited the live-value tooltip "Live value (redacted by +> default)" -- it is not a value and nothing was redacted. **None of the feature this item names +> shipped:** no stop condition, no state dump, no pin mechanism, no "test up to row N" surface. +> None of the step-scoped run symbols this row's own re-score names as absent from `ide/src` exist +> yet. This PR fixes the UX dead-end the Filed paragraph called out in passing, not the item. +> **Confirmed by the owner via the Liaison: mocking-by-default is DECLINED even as a filed ADR 0010 +> amendment, and closing this item OUTRIGHT is DECLINED.** Both records agree -- the ledger stayed +> open through this PR by design, not by oversight. +> **The stop condition is a design question, not a to-do -- do not add it to this item casually.** +> `dryrun_trace.py` implements the traced dry-run as a `sys.settrace` observer; stopping a handler +> mid-execution means raising out of the trace function, which changes the execution semantics of +> the thing being observed. It is not a flag to add. The Dispatcher split it out as its own unfiled +> piece for exactly this reason. +> > **Filed 2026-07-30 โ€” not started.** Largely a stop condition + state dump on ADR 0072's traced dry-run; lookup rows must mock by default, not as an afterthought. > Verdict: build > Closing-act: code diff --git a/ide/src/liveDebug.ts b/ide/src/liveDebug.ts index 7483628d8..f36ea7146 100644 --- a/ide/src/liveDebug.ts +++ b/ide/src/liveDebug.ts @@ -350,7 +350,9 @@ export function inlineValuesFor(invocations: TraceInvocation[], reveal: boolean) out.push({ line: line - 1, after: WARNING_TEXT, - hover: `\`${call}\` is a live, read-only lookup โ€” not evaluated in this offline preview.`, + hover: + `\`${call}\` is a live, read-only lookup โ€” not evaluated in this offline preview.\n\n` + + "Preview the feed by stubbing this call's wrapper function (ADR 0010).", kind: "warning", }); } diff --git a/ide/src/stepsModel.ts b/ide/src/stepsModel.ts index 128a19f1b..5552315a7 100644 --- a/ide/src/stepsModel.ts +++ b/ide/src/stepsModel.ts @@ -232,6 +232,13 @@ export interface RowViewModel { editableParams?: string[]; code?: string; // verbatim source slice โ€” code rows only (the degradation-ladder passthrough) liveValue?: string; // redacted-by-default #92 annotation (see mergeLiveValues); undefined = none + /** + * Which kind of annotation `liveValue` holds. `mergeLiveValues` folds the inline values' TEXT into one + * string and used to drop their `kind`, so a skipped-lookup row rendered inside the live-value tooltip + * and told the author it was showing a redacted live value -- the opposite of what happened (BACKLOG + * #236). Carried so the renderer can offer the right affordance instead. + */ + liveValueKind?: "value" | "warning"; /** * The PROJECTION-TIME source text of this row's [lineStart, lineEnd] range โ€” the row exactly as the user * saw it when the lens projected the inputs (engine newline model: split on `\r\n`|`\r`|`\n`, joined by @@ -654,6 +661,25 @@ export interface LiveInlineValue { kind: "value" | "warning"; } +/** + * The tooltip on a row whose annotation is a SKIPPED LIVE LOOKUP (BACKLOG #236). + * + * A `db_lookup`/`fhir_lookup` does not do I/O in a dry-run -- it RAISES, and the tracer records a + * `live_lookup_skipped` annotation and re-raises so the disposition is byte-identical + * (`dryrun_trace.classify_live_lookup`). The row's old tooltip said "Live value (redacted by default)", + * which is wrong twice over: it is not a value and nothing was redacted. + * + * So the tooltip names the mechanism ADR 0010 already specifies instead of restating the failure -- + * `ADR 0010`: "a feed that uses it is previewed by stubbing its wrapper". That is the supported preview + * path, and pointing at it is the whole of this item: the engine is unchanged and nothing is mocked. + */ +export const LIVE_LOOKUP_TIP = + "Live read-only lookup: a dry-run cannot run it, so this row was not evaluated. " + + "Preview the feed by stubbing this call's wrapper function (ADR 0010)."; + +/** The tooltip on a row showing a real captured value. Unchanged wording (BACKLOG #92). */ +export const LIVE_VALUE_TIP = "Live value (redacted by default โ€” synthetic samples only)"; + /** The redacted annotation shown by default (matches liveDebug's VALUE_MARKER + REVEAL_PLACEHOLDER). */ export const REDACTED_LIVE_VALUE = "โ–ธ โ‹ฏ"; @@ -672,6 +698,9 @@ export function mergeLiveValues(rows: RowViewModel[], inline: LiveInlineValue[]) ); if (hits.length > 0) { row.liveValue = hits.map((h) => h.after).join(" ยท "); + // A warning WINS over a value. `traceRowValues` already suppresses values on a warned line, so a + // warning hit means this row's annotation is the skipped-lookup notice, not a reading. + row.liveValueKind = hits.some((h) => h.kind === "warning") ? "warning" : "value"; } } return rows; @@ -2624,8 +2653,10 @@ export function renderRowHtml( const indent = `style="margin-left:${row.nesting * INDENT_PX}px"`; const badge = row.badge ? `${escapeHtml(row.badge)}` : ""; const subtitle = row.subtitle ? `${escapeHtml(row.subtitle)}` : ""; + const liveTip = row.liveValueKind === "warning" ? LIVE_LOOKUP_TIP : LIVE_VALUE_TIP; + const liveClass = row.liveValueKind === "warning" ? "live warn" : "live"; const live = row.liveValue - ? `${escapeHtml(row.liveValue)}` + ? `${escapeHtml(row.liveValue)}` : ""; // A field is editable only when the handler name is known (the write path); read-only callers pass "". const editable = new Set(handlerName ? (row.editableParams ?? []) : []); diff --git a/ide/src/test/suite/steps.test.ts b/ide/src/test/suite/steps.test.ts index fd9a56ecb..e4101d665 100644 --- a/ide/src/test/suite/steps.test.ts +++ b/ide/src/test/suite/steps.test.ts @@ -5,6 +5,8 @@ import * as fs from "fs"; import * as path from "path"; import { + LIVE_LOOKUP_TIP, + LIVE_VALUE_TIP, REDACTED_LIVE_VALUE, buildHandlerViewModels, buildLensTraceArgs, @@ -318,6 +320,10 @@ suite("stepsModel โ€” buildLensTraceArgs never requests PHI (the lens's redacted }); suite("stepsModel โ€” traceRowValues folds a traced dry-run onto rows (redacted by default)", () => { + const lookupRow = (): RowViewModel[] => [ + { index: 0, kind: "action", nesting: 0, lineStart: 4, lineEnd: 4, title: "Lookup", params: [] }, + ]; + test("redacted by default (reveal off): each executed line is the โ–ธ โ‹ฏ placeholder, no real value", () => { const off = traceRowValues([PRODUCING], false); assert.strictEqual(off.length, 2); @@ -387,6 +393,123 @@ suite("stepsModel โ€” traceRowValues folds a traced dry-run onto rows (redacted assert.ok(warn?.after.includes("live lookup"), warn?.after); }); + test("a skipped lookup carries its KIND to the row, not just its text (BACKLOG #236)", () => { + // The affordance depends on the row knowing WHICH annotation it holds. `mergeLiveValues` used to + // fold only the text, so a skipped-lookup row was indistinguishable from a value row downstream. + const skipped = traceRowValues( + [inv({ annotations: [{ line: 4, kind: "live_lookup_skipped", call: "db_lookup" }] })], + false, + ); + const rows = lookupRow(); + mergeLiveValues(rows, skipped); + assert.strictEqual(rows[0].liveValueKind, "warning", "the row must know it holds a warning"); + }); + + test("a row holding real captured values is still marked a value", () => { + const rows = lookupRow(); + mergeLiveValues(rows, [{ line: 3, after: REDACTED_LIVE_VALUE, kind: "value" }]); + assert.strictEqual(rows[0].liveValueKind, "value"); + }); + + test("a warning WINS over a value landing in the same row", () => { + // `traceRowValues` already suppresses values on a warned LINE, but a multi-line row can hold both + // a warned line and a producing one. The row's affordance must follow the warning. + const rows = lookupRow(); + mergeLiveValues(rows, [ + { line: 3, after: REDACTED_LIVE_VALUE, kind: "value" }, + { line: 3, after: "not evaluated", kind: "warning" }, + ]); + assert.strictEqual(rows[0].liveValueKind, "warning"); + }); + + test("a row with no annotation gets no kind at all", () => { + const rows = lookupRow(); + mergeLiveValues(rows, []); + assert.strictEqual(rows[0].liveValueKind, undefined); + assert.strictEqual(rows[0].liveValue, undefined); + }); +}); + +// -------------------------------------------------------------------------------------------------- +// BACKLOG #236: the lookup row's tooltip must OFFER THE SUPPORTED PREVIEW PATH rather than restate the +// failure. A db_lookup/fhir_lookup does not do I/O in a dry-run -- it RAISES, and the tracer records a +// `live_lookup_skipped` annotation and re-raises (dryrun_trace.classify_live_lookup). ADR 0010 already +// names the mechanism: "a feed that uses it is previewed by stubbing its wrapper". Nothing is mocked and +// no engine file changes; the IDE just stops pointing at a dead end. +// -------------------------------------------------------------------------------------------------- + +suite("stepsModel โ€” a skipped live lookup offers the stubbing affordance (BACKLOG #236)", () => { + const warnRow = (): RowViewModel => ({ + index: 0, + kind: "action", + nesting: 0, + lineStart: 4, + lineEnd: 4, + title: "Lookup", + params: [], + liveValue: "not evaluated", + liveValueKind: "warning", + }); + + test("the warning row's tooltip names the mechanism instead of restating the failure", () => { + const html = renderRowHtml(warnRow()); + assert.ok(html.includes("stubbing"), `expected the stubbing affordance, got: ${html}`); + assert.ok(html.includes("ADR 0010"), "the tooltip must cite where the mechanism is specified"); + }); + + test("THE MISLABEL: a skipped lookup no longer claims to be a redacted live value", () => { + // It is not a value and nothing was redacted. Telling an author the opposite of what happened is + // the defect this fixes; it was reached because mergeLiveValues dropped the kind. + const html = renderRowHtml(warnRow()); + assert.ok(!html.includes(LIVE_VALUE_TIP), "the value tooltip must not appear on a warning row"); + assert.ok(!html.includes("redacted"), `no redaction claim on a skipped lookup: ${html}`); + }); + + test("a real value row's tooltip is UNCHANGED (BACKLOG #92 wording preserved)", () => { + const row = { ...warnRow(), liveValue: REDACTED_LIVE_VALUE, liveValueKind: "value" as const }; + const html = renderRowHtml(row); + assert.ok(html.includes(LIVE_VALUE_TIP), "the existing value tooltip must survive verbatim"); + assert.ok(!html.includes(LIVE_LOOKUP_TIP), "a value row must not offer the lookup affordance"); + }); + + test("a row with no annotation renders no tooltip span at all", () => { + const row = { ...warnRow(), liveValue: undefined, liveValueKind: undefined }; + const html = renderRowHtml(row); + assert.ok(!html.includes(LIVE_LOOKUP_TIP) && !html.includes(LIVE_VALUE_TIP), html); + }); + + test("END TO END: a live_lookup_skipped annotation reaches the rendered row as the affordance", () => { + // The whole chain the author actually meets: tracer annotation -> traceRowValues -> mergeLiveValues + // -> renderRowHtml. Each step is covered above; this asserts they compose. + const skipped = traceRowValues( + [inv({ annotations: [{ line: 4, kind: "live_lookup_skipped", call: "db_lookup" }] })], + false, + ); + const rows = [ + { + index: 0, + kind: "action" as const, + nesting: 0, + lineStart: 4, + lineEnd: 4, + title: "Lookup", + params: [], + }, + ]; + mergeLiveValues(rows, skipped); + const html = renderRowHtml(rows[0]); + assert.ok(html.includes("stubbing"), `affordance missing end to end: ${html}`); + assert.ok(!html.includes("redacted"), `mislabel survived end to end: ${html}`); + }); + + test("the affordance text carries no glyph (CLAUDE.md section 11)", () => { + // The row's inline text keeps its pre-existing warning glyph, which belongs to BACKLOG #1265's + // migration. The text THIS item adds must not introduce another. + assert.ok(!/[\u2700-\u27bf\u2b00-\u2bff\u26a0\u2705\u274c\ufe0f]|[\ud800-\udbff]/.test(LIVE_LOOKUP_TIP), LIVE_LOOKUP_TIP); + }); +}); + +suite("stepsModel โ€” traceRowValues, continued", () => { test("across traced messages, the newest invocation's value wins for a shared line", () => { const first = inv({ events: [{ line: 3, event: "line", assigned: { x: "AAA" } }] }); const second = inv({ events: [{ line: 3, event: "line", assigned: { x: "BBB" } }] });