From bf0e042e95528024729127ac2d0aae25363003e1 Mon Sep 17 00:00:00 2001 From: stack72 Date: Tue, 5 May 2026 13:43:22 +0100 Subject: [PATCH] feat(data): surface jobTag alongside workflowTag/stepTag in data search (swamp-club#245) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to swamp-club#237. After #237, workflow step output carries `data.tags.job` (populated via `workflowTagOverrides`), but the legacy `swamp data search` result mapping at src/libswamp/data/search.ts:305-306 exposed only `workflowTag` and `stepTag` — leaving the provenance triple incomplete on this surface compared to `swamp data query` (which already supports `jobName == "..."`). This change fills the gap with three small, additive edits: - DataSearchItem gains `jobTag?: string`, ordered between workflowTag and stepTag to mirror the production tag-write order in execution_service.ts (workflow → job → step). - The dataSearch generator populates `jobTag: data.tags.job`. Pre-#237 data has no `job` tag, so jobTag becomes `undefined` — same nullable handling already used for workflowTag/stepTag. - The Ink renderer surfaces jobTag both as a searchable picker token and as a `**Job:**` line in the preview metadata, between Workflow and Step. Two regression tests cover the wiring: a positive case asserting workflow/job/step tags are populated together (proves order parity), and a negative case locking in the `jobTag === undefined` contract when the source data has no `job` tag. No `--job` filter added — `data search` already has asymmetric filter coverage (`--workflow` exists, no `--step`); adding `--job` would introduce new asymmetry without solving a real ergonomic gap. Power users should use `swamp data query 'jobName == "..."'`. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/libswamp/data/search.ts | 2 ++ src/libswamp/data/search_test.ts | 39 ++++++++++++++++++++++ src/presentation/renderers/data_search.tsx | 3 +- 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/libswamp/data/search.ts b/src/libswamp/data/search.ts index 4f0fc53d..abe7f706 100644 --- a/src/libswamp/data/search.ts +++ b/src/libswamp/data/search.ts @@ -43,6 +43,7 @@ export interface DataSearchItem { createdAt: string; tags: Record; workflowTag?: string; + jobTag?: string; stepTag?: string; } @@ -303,6 +304,7 @@ export async function* dataSearch( createdAt: data.createdAt.toISOString(), tags: data.tags, workflowTag: data.tags.workflow, + jobTag: data.tags.job, stepTag: data.tags.step, }); } diff --git a/src/libswamp/data/search_test.ts b/src/libswamp/data/search_test.ts index cb8e82c2..720018a5 100644 --- a/src/libswamp/data/search_test.ts +++ b/src/libswamp/data/search_test.ts @@ -158,6 +158,45 @@ Deno.test("dataSearch: filters by tags with AND logic", async () => { assertEquals(completed.data.results[0].id, "d1"); }); +Deno.test("dataSearch: surfaces workflowTag/jobTag/stepTag from data tags", async () => { + const items = [ + makeDataItem({ + id: "d1", + tags: { workflow: "my-wf", job: "my-job", step: "my-step" }, + }), + ]; + const deps = makeDeps(items); + const events = await collect( + dataSearch(createLibSwampContext(), deps, {}), + ); + + const completed = events[1] as Extract< + DataSearchEvent, + { kind: "completed" } + >; + assertEquals(completed.data.results.length, 1); + assertEquals(completed.data.results[0].workflowTag, "my-wf"); + assertEquals(completed.data.results[0].jobTag, "my-job"); + assertEquals(completed.data.results[0].stepTag, "my-step"); +}); + +Deno.test("dataSearch: jobTag is undefined when data has no job tag", async () => { + const items = [ + makeDataItem({ id: "d1", tags: {} }), + ]; + const deps = makeDeps(items); + const events = await collect( + dataSearch(createLibSwampContext(), deps, {}), + ); + + const completed = events[1] as Extract< + DataSearchEvent, + { kind: "completed" } + >; + assertEquals(completed.data.results.length, 1); + assertEquals(completed.data.results[0].jobTag, undefined); +}); + Deno.test("dataSearch: yields error when model not found", async () => { const deps = makeDeps([], { findDefinitionByIdOrName: () => Promise.resolve(null), diff --git a/src/presentation/renderers/data_search.tsx b/src/presentation/renderers/data_search.tsx index 6d30bcd9..5c61fbd7 100644 --- a/src/presentation/renderers/data_search.tsx +++ b/src/presentation/renderers/data_search.tsx @@ -122,7 +122,7 @@ class InkDataSearchRenderer implements DataSearchRenderer { (item) => `${item.name} ${item.modelName} ${item.modelType} ${item.type} ${ item.workflowTag ?? "" - } ${item.stepTag ?? ""} ${ + } ${item.jobTag ?? ""} ${item.stepTag ?? ""} ${ Object.entries(item.tags).map(([k, v]) => `${k}=${v}`).join(" ") }`, renderDataResultLine, @@ -179,6 +179,7 @@ function buildMetadataMarkdown(item: DataSearchItem): string { ]; if (item.workflowTag) lines.push(`**Workflow:** ${item.workflowTag}`); + if (item.jobTag) lines.push(`**Job:** ${item.jobTag}`); if (item.stepTag) lines.push(`**Step:** ${item.stepTag}`); if (tagEntries.length > 0) {