diff --git a/ui/components/both-dataspaces.test.tsx b/ui/components/both-dataspaces.test.tsx
new file mode 100644
index 0000000..a72c23a
--- /dev/null
+++ b/ui/components/both-dataspaces.test.tsx
@@ -0,0 +1,127 @@
+// @vitest-environment happy-dom
+import { createRoot, type Root } from "react-dom/client";
+import { act } from "react";
+import { renderToStaticMarkup } from "react-dom/server";
+import { afterEach, describe, expect, it } from "vitest";
+import { EntityDetail } from "@/components/EntityDetail";
+import { RecordBrowser } from "@/components/RecordBrowser";
+import { findDataspace, findEntity, findKind, listNavigation } from "@/lib/catalog";
+import { applyTableView } from "@/lib/table-view";
+import type { ViewHistory } from "@/lib/table-view";
+
+declare global {
+ var IS_REACT_ACT_ENVIRONMENT: boolean;
+}
+
+globalThis.IS_REACT_ACT_ENVIRONMENT = true;
+
+const SPACES = ["systems-thinking", "software-architecture"] as const;
+
+let root: Root | null = null;
+
+afterEach(() => {
+ act(() => root?.unmount());
+ root = null;
+ document.body.innerHTML = "";
+});
+
+function memoryHistory(): ViewHistory {
+ let current = "";
+ return {
+ read: () => (current ? `?${current}` : ""),
+ push: (search) => {
+ current = search;
+ },
+ subscribe: () => () => undefined,
+ };
+}
+
+function names(): string[] {
+ return [...document.querySelectorAll("tbody tr td:first-child")].map((cell) => cell.textContent ?? "");
+}
+
+function buttonNamed(label: string): HTMLButtonElement {
+ const node = [...document.querySelectorAll("button")].find((item) => item.textContent === label);
+ if (!(node instanceof HTMLButtonElement)) throw new Error(`missing ${label}`);
+ return node;
+}
+
+describe("both dataspaces", () => {
+ it("browses, searches, filters, sorts, and opens a detail for each field", async () => {
+ expect(listNavigation().map((item) => item.id).sort()).toEqual([...SPACES].sort());
+
+ for (const id of SPACES) {
+ const space = findDataspace(id);
+ const kind = findKind(id, "concept");
+ expect(space?.title.length).toBeGreaterThan(0);
+ expect(kind?.rows.length).toBeGreaterThan(1);
+ expect(kind?.rows.every((row) => row.route.startsWith(`/${id}/concept/`))).toBe(true);
+
+ const sample = kind!.rows[0];
+ const tag = sample.facets.tags[0];
+ const searched = applyTableView(kind!.rows, { q: sample.name, facets: {}, sort: { key: "name", dir: 1 } });
+ expect(searched.map((row) => row.name)).toEqual([sample.name]);
+ const filtered = applyTableView(kind!.rows, { q: "", facets: { tags: [tag] }, sort: { key: "name", dir: 1 } });
+ expect(filtered.length).toBeGreaterThan(0);
+ expect(filtered.length).toBeLessThan(kind!.rows.length);
+ expect(filtered.every((row) => row.facets.tags.includes(tag))).toBe(true);
+ const descending = applyTableView(kind!.rows, { q: "", facets: {}, sort: { key: "name", dir: -1 } });
+ expect(descending.map((row) => row.name)).toEqual(
+ [...kind!.rows.map((row) => row.name)].sort((a, b) => b.localeCompare(a)),
+ );
+
+ const host = document.createElement("div");
+ document.body.innerHTML = "";
+ document.body.appendChild(host);
+ root = createRoot(host);
+ await act(async () => {
+ root?.render();
+ });
+ expect(names().length).toBe(kind!.rows.length);
+
+ const input = document.querySelector("input[aria-label='Search records']");
+ if (!(input instanceof HTMLInputElement)) throw new Error("missing search");
+ await act(async () => {
+ const setValue = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
+ setValue?.call(input, sample.name);
+ input.dispatchEvent(new Event("input", { bubbles: true }));
+ input.form?.requestSubmit();
+ });
+ expect(names()).toEqual([sample.name]);
+
+ await act(async () => {
+ const setValue = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
+ setValue?.call(input, "");
+ input.dispatchEvent(new Event("input", { bubbles: true }));
+ input.form?.requestSubmit();
+ });
+ expect(names().length).toBe(kind!.rows.length);
+ await act(async () => {
+ buttonNamed(tag).click();
+ });
+ expect(names().length).toBe(filtered.length);
+ expect(document.body.textContent).toContain(tag);
+ await act(async () => {
+ buttonNamed(tag).click();
+ });
+ expect(names().length).toBe(kind!.rows.length);
+ const sortName = [...document.querySelectorAll("button")].find((node) => node.textContent?.includes("Name"));
+ if (!(sortName instanceof HTMLButtonElement)) throw new Error("missing sort");
+ await act(async () => {
+ sortName.click();
+ });
+ expect(names()).toEqual(descending.map((row) => row.name));
+
+ const slug = sample.route.split("/").filter(Boolean).at(-1) ?? "";
+ const detail = findEntity(id, "concept", decodeURIComponent(slug));
+ expect(detail?.title).toBe(sample.name);
+ const html = renderToStaticMarkup();
+ expect(html).toContain(`>${detail!.title}<`);
+ expect(html).toContain(detail!.tldr);
+ expect(html).toContain("Sources");
+
+ act(() => root?.unmount());
+ root = null;
+ }
+ });
+});
diff --git a/workplans/CONTENT-WP-0002-ui-extraction.md b/workplans/CONTENT-WP-0002-ui-extraction.md
index 3fe4e73..b54591c 100644
--- a/workplans/CONTENT-WP-0002-ui-extraction.md
+++ b/workplans/CONTENT-WP-0002-ui-extraction.md
@@ -16,7 +16,7 @@ state_hub_workstream_id: "55c1d92f-fe1a-552a-998d-2d6905fb6b99"
# UI extraction and experience skeleton
-Deliver the strongest OnCo-style interaction patterns on generic data. CONTENT-WP-0000 and CONTENT-WP-0001 are finished, so this plan is eligible. T01 through T05 have landed under `ui/`. T05 adds keyboard search, empty, loading, and error states, and a layout that wraps on a narrow screen. T06 is next. See `workplans/evidence/CONTENT-WP-0002-ImplementationNotes.md`.
+Deliver the strongest OnCo-style interaction patterns on generic data. CONTENT-WP-0000 and CONTENT-WP-0001 are finished, so this plan is eligible. T01 through T06 have landed under `ui/`. T06 checks that browse, search, filter, sort, and detail work for both example dataspaces. T07 is next. See `workplans/evidence/CONTENT-WP-0002-ImplementationNotes.md`.
## Work packages
@@ -71,7 +71,7 @@ state_hub_task_id: "bbbc39cd-c47f-541d-b43d-df9e7ff646c3"
```task
id: CONTENT-WP-0002-T06
-status: todo
+status: done
priority: high
state_hub_task_id: "3474ef62-53ae-59f5-8a19-55e21ade728a"
```
diff --git a/workplans/evidence/CONTENT-WP-0002-ImplementationNotes.md b/workplans/evidence/CONTENT-WP-0002-ImplementationNotes.md
index 34bbad7..e153150 100644
--- a/workplans/evidence/CONTENT-WP-0002-ImplementationNotes.md
+++ b/workplans/evidence/CONTENT-WP-0002-ImplementationNotes.md
@@ -92,4 +92,18 @@ Checks on 2026-10-13, after T05:
- `npm run typecheck`, `scripts/check_ui_boundary.py`, fixture validation, and the contract and ingest tests passed.
- `next dev` on `127.0.0.1:3400` rendered `/systems-thinking/concept/` with the search hint and the narrow-layout classes. The server was stopped. No graphical browser was available, so a narrow viewport was not walked. That smoke stays with T09.
-Not done: the T06 gate that browse, search, filter, sort, and detail pass together for both dataspaces, the formal import gate, the provenance gate, or the keyboard and small-screen smoke. A facet key other than `tags` still has no values in these fixtures.
+Not done after T05: the T06 gate that browse, search, filter, sort, and detail pass together for both dataspaces, the formal import gate, the provenance gate, or the keyboard and small-screen smoke. A facet key other than `tags` still has no values in these fixtures.
+
+## T06 — both dataspaces
+
+No new screen. The gate runs the same browse, search, filter, sort, and detail path on `systems-thinking` and `software-architecture`.
+
+For each field the check loads the concept index, confirms every row route stays inside that field, searches for one record by its name, filters by one of its tags, sorts names descending, and renders that record's detail with its summary and sources. The home of each field also links to the other.
+
+Checks on 2026-10-13, after T06:
+
+- `npm test` in `ui/`: 18 tests passed, including `components/both-dataspaces.test.tsx`.
+- `npm run typecheck`, `scripts/check_ui_boundary.py`, fixture validation, and the contract and ingest tests passed.
+- `next dev` on `127.0.0.1:3400` returned HTTP 200 for `/systems-thinking/`, `/software-architecture/`, `/systems-thinking/concept/feedback/`, and `/software-architecture/concept/coupling/`. Each page contained the other field's name or the record's sources. The server was stopped. Search and sort on those pages were exercised in the UI test, because the filter runs in the browser after the HTML is sent.
+
+Not done: the formal import gate, the provenance gate, or the keyboard and small-screen smoke. A facet key other than `tags` still has no values in these fixtures.