KG Tables — Design Spec (#4624 CRUD · #4625 filter)
Milestone: kg-tables
Design-led (Testing Constitution). The Fichero creative director owns this intent; tests enforce it; code makes them pass. One line per behavior, each cited by its pinning test. Status: APPROVED — creative director ratified; first-wave CRUD + filters shipped. Tags: [OK] today · [MISSING] not built · [PARTIAL] exists elsewhere, not in the table.
Intent (the design)
The entity table and the claim table are where a researcher browses, filters, and curates the knowledge graph — and hand-authors it, not only what AI extracted. Full CRUD lives in the tables themselves, every action is one audited, reversible backend action, and a change updates the one row in place (never a whole-table re-render). A filter narrows what’s shown without touching what exists.
Surfaces: EntitiesLibraryContent / EntitiesTableView, ClaimsLibraryContent /
ClaimsTableView, EntityStore, ClaimStore, EntityService, backend
api/routes/entity/* + api/routes/claim/* (CRUD already complete server-side).
Behaviors
A. Filter (#4625)
kg.tables.filter.text[OK] (Pinned:ClaimsFilterTests,EntitiesFilterTests.) — a per-table filter field narrows rows by text. Built:EntitiesLibraryContent.swift:~136(TextField("Filter entities", ...)feedingentityMatches),ClaimsLibraryContent.swift:~163(TextField("Filter claims", ...)feedingclaimMatches).kg.tables.filter.entity-type[OK] — an entity-type picker filters the entity table. Built:EntitiesLibraryContent.swift:~127-146(availableTypes+ typeMenu). Pinned:EntitiesFilterTests.kg.tables.filter.claim-type[PARTIAL] (#4768) — a claim type picker filters the claim table (built:ClaimsLibraryContent.swift:~151-176,availableTypes/typeMenufilteringclaimType), but there is no curation-state picker — the spec line originally promised “type / curation-state” and only type shipped.kg.tables.filter.combines-with-search[OK] (Pinned:ClaimsFilterTests,EntitiesFilterTests.) — the per-table filter AND the shared ⌘F query both apply (intersection); neither clobbers the other. Built:entityMatches(EntitiesLibraryContent.swift:~109-123) /claimMatches(ClaimsLibraryContent.swift:~136-151) both checksearchandfilterText.kg.tables.filter.empty-state[OK] (Pinned:ClaimsFilterTests,EntitiesFilterTests.) — a filter that matches nothing shows “No matches”, never a blank table or a spinner. Built:ClaimsLibraryContent.swift:~192-203(emptyMessage);EntitiesLibraryContenthas the parallel empty state.kg.tables.filter.pushdown(later) — for large sets, push the filter to the list endpoint (q/entity_type/claim_typealready exist) instead of client-side only.
B. Entity CRUD (#4624)
kg.tables.entity.create[OK] (Pinned:EntitiesTableCreateTests.) — a “New Entity” affordance creates an entity (canonical name + type), selects the new row, and enters rename. Built:EntitiesLibraryContent.swift:~77(showingCreateSheet/handleCreatedEntity), idkg.entity.new.kg.tables.entity.rename-inline[OK] — the canonical name is editable from the table. Built:EntitiesTableView.swift:~118-125(inlineTextField+commitRename). Pinned:EntitiesTableCreateTests.kg.tables.entity.retype[OK] — entity type is editable (keep). Pinned:KGInspectorCRUDUITests.kg.tables.entity.delete[OK] — delete removes the entity and its claim links, undoable. Pinned:KGInspectorCRUDUITests.kg.tables.entity.curate[PARTIAL] (implemented, unpinned; #4801, → #1765, → #1786) — bless / reject / merge stay. → #1786 asked for entity rename + “Add entity” specifically — both are now [OK] (kg.tables.entity.rename-inline/.createabove); kept open as a verify-close candidate rather than closed by this pass, since curate (bless/reject/merge) itself is still only [PARTIAL]. → #1765 is the broader origin ask (approve/reject/edit entities AND claims,curation_stateend-to-end) — its “also in WebKit” clause is moot now the KG browser has retired (#4828); what remains open is the inspector-only half this behavior tracks. Cross-ref:kg-readable-representation.md’skg.read.edit-unit-is-the-claimcovers a DIFFERENT surface reaching the sameclaim.patchaction — editing FROM a rendered sentence rather than from this table — the two should stay in sync as both land.kg.merge.repoints-subject— [PARTIAL] (#4859 still open pending close) a merge repoints a claim’ssubject_entity_idto the survivor the same way it repointsentity_ids, and records each replaced value on the merge audit (claim_subject_repoints); unmerge restores exactly those claims, and only where the field still names the survivor, so a curator’s later correction is never overwritten. A merge never rewrites the subject’s canonical name or the claim’s sentence: it says two records are one entity, not what the source said. Both callers reach this code,entity.mergeandreview.accept. After a merge the readable paragraph gives rolesubjecton the survivor’s page. Generalized to all five scalar entity-id fields, verified at HEAD (74d539720):speaker_entity_id,subject_of_inquiry_entity_id,scribe_entity_idandeditor_entity_id— the four role fields this behavior previously listed as NOT repointed — now sharemerge_entities_impl’s oneCLAIM_ENTITY_ID_FIELDSloop, the same list the entity writer’s dedup repointing already used, so the two paths cannot drift apart. Now actually tested, not only claimed (96e928783, found by this session’s own test audit — the code was right, the test file asserted only the subject):test_merge_repoints_subject.py::test_merge_repoints_every_scalar_role_fielddrives a real merge through the action layer and asserts all four role fields repoint while an unrelated subject is untouched;::test_unmerge_restores_every_scalar_role_fieldasserts undo restores all four;::test_audit_names_every_repointed_fieldasserts the merge audit names each one. PARTIAL, not OK, for the one reason that keeps #4859 open: rows merged before this fix are not rewritten, which is the maintainer’s decision (an audited, scan-first repair action, never a batch rewrite) — a data-migration gap, not a code or test gap. Pinned:fichero-server/tests/unit/api/test_merge_repoints_subject.py::TestMergeRepointsSubjectViaEntityMergeAction,::TestMergeRepointsSubjectViaReviewAccept,::test_merge_repoints_every_scalar_role_field,::test_unmerge_restores_every_scalar_role_field,::test_audit_names_every_repointed_field.kg.delete.clears-entity-links— [PARTIAL] (implemented and tested, 017995dbe; #4863 still open pending close) a non-cascadingentity.deleteclears (sets toNone) every scalar entity-id field a claim carries that names the deleted entity —subject_entity_idand its four siblings,speaker_entity_id,subject_of_inquiry_entity_id,scribe_entity_id,editor_entity_id— not only strips it fromentity_ids, and records{claim_id, field, old_entity_id}for each field cleared, the same shape a merge records. Unlike a merge’s absorbed-but-tombstoned entity, a deleted entity no longer exists at all, so the correct link value isNone, never a dangling dead id. Never touchessubject_canonicalor the claim’stext— the source still said what it said. Undo restores the full claim snapshot first (the pinned contract three existing tests require), then reapplies the recorded field clears as an additive, guarded restore; the cascade delete path removes the claim rows outright, so nothing is left dangling there either. This is the delete-side sibling ofkg.merge.repoints-subjectabove — both now cover all five scalar entity-id fields (merge’s own field-completeness gap closed 74d539720/ 96e928783, see above). Pinned:fichero-server/tests/unit/api/test_delete_clears_entity_links.py::TestEachFieldClearsOnDelete,::TestClaimCarryingSeveralFieldsAtOnce,::TestDisplayFieldsNeverTouched.kg.entity.menu.merge— [GAP] (#4828, → #1675)EntityMergeSheetmerges duplicate entities. Cross-referenceskg.tables.entity.curateabove (#4801) rather than duplicating it — the merge CAPABILITY is the same one that behavior already tracks; this entry is about the sheet’s own re-mount location specifically. → #1675 asks for more than the sheet: a reversible AUDIT TRAIL for merge/split decisions (source ids, who/why, undo/re-split), not built yet on either side.kg.entity.menu.split— [GAP] (#4828, → #1675)EntitySplitSheetsplits a conflated entity. Not named in any of the three KG specs before this pass. See #1675’s audit-trail ask above. → #1688 additionally asks for the SAME merge/unmerge/alias/edit surface reachable via the CLI, not just the UI sheets — the CLI half is unbuilt on either side.kg.claim.contradiction-triage— [GAP] (#4828, → #1677)ContradictionTriageSheettriages contradicting claims. Not named before this pass. → #1677 is the broader ask: a review UI covering EVERY workflow stage (transcript → imported entities → merge → KG → ontology), of which contradiction triage is one stage.kg.claim.review-queue— [GAP] (#4828, → #372)ClaimReviewQueueSheet, a review queue for unreviewed claims. Not named before this pass. → #372 is the original ask (queue withunreviewed → shortlisted → curated/rejectedtransitions, filter by person/topic, batch or single-item) —ClaimReviewQueueSheetexists but is unreachable since the browser retired, same as this behavior’s own [GAP] tag says.kg.entity.kind-chart— [GAP] (#4828)EntityKindChartView, entity counts by kind. Not named before this pass.kg.claim.speaker-comparison— [GAP] (#4828)SpeakerComparisonView, claims compared per speaker. Not named before this pass.
Enrichment’s two unreachable views (WikidataEnrichmentSheet, HeuristicReviewSheet) are
kg/kg-enrichment.md’s (→ #4759, already named there) — not duplicated here.
C. Claim CRUD (#4624)
kg.tables.claim.create[OK] (Pinned:ClaimsTableCreateTests.) — a “New Claim” affordance hand-authors a claim, wiring POST /api/claims. Built:NewClaimSheet.swift(view),EntityService+ ClaimEntityCRUD.swift:~117(createClaim), wired fromClaimsLibraryContent.swift(showingCreateSheet), idkg.claim.new.kg.tables.claim.create.source-optional-flagged— a hand-authored claim MAY have no source (a working hypothesis / synthesis), but it is clearly marked “no source” and treated as lower-provenance; it is never silently indistinguishable from a sourced claim.kg.tables.claim.edit[OK] (Pinned:ClaimsTableCreateTests.) — subject/verb/object + fields editable from the claim table is built:ClaimsTableView.swift:~168-172wires anonEditmenu item (kg.claim.menu.edit) that opens the existingEditClaimSheet(S·V·O + type + epistemic status, PATCH/api/claims/{id}), reused rather than re-parsed. Shipped ina166ad3ee(2026-09-08, “feat(kg-tables): edit a claim from the claims table (#4624)”). The row-levelclaimMenudoc-comment’s “edit / curate arrive in later kg-tables waves” is stale for edit (curate is the real remaining gap — seekg.tables.claim.curatebelow). Pinned:fichero/Tests/Unit/general/Views/Library/ClaimsTableCreateTests.swift::testClaimsTableOffersEditReusingTheExistingSVOEditor.kg.tables.claim.delete[PARTIAL] (#4643, → #1787) — delete from the table is built:ClaimsTableView.swift:~166-183(onDeletemenu item,kg.claim.menu.delete) →ClaimsLibraryContent.swift:~254-268(deleteClaims, one audited delete per id, reloads on failure). Left [PARTIAL] pendingkg.scale.batch-delete(sequential per-id calls, no batch endpoint yet — see Scale section below). → #1787 asks that claim delete be wired EVERYWHERE approve/reject/suppress live, single + multi, with confirm — the table’s own delete is built; approve/reject/suppress-with-confirm iskg.tables.claim.curatebelow, still [PARTIAL].kg.tables.claim.curate[PARTIAL] (#4691, → #1751) — bless / reject / merge from the table. Verified against code (2026-09-18):ClaimsTableView.swift:158-184(claimMenu) offers only Edit and Delete — no bless/reject/merge menu item. Curation is reachable only via batch MCP tools, not the table row menu (#4691, table row “claim curate … from table ✗ClaimsTableView.swift:158‘Delete only, for now’”). → #1751 is a broader EPIC (unified multi-select curate/enable/disable/group/delete/merge across files, folders, entities AND claims) — kept open here for its entities/claims slice specifically; the files/folders half issidebar-crud.md’s territory, not this spec’s.
C2. Deeper CRUD/UX asks not yet behaviors
kg.tables.entity.descendant-aggregation-provenance— [GAP] (#2020) when a folder/PDF in the sidebar is selected, the entity panel aggregates the entities of the selection AND all its children UNDIFFERENTIATED — no way to tell which document an entity came from. Wanted: convert the flat List to a Table with a clickable “Found in” column (entity-grouped, default includes-children), so descendant aggregation stays but gains provenance.kg.tables.schema-complete-field-display— [GAP] (#1768) the inspector surfaces only a few fields of the KG data model’s rich schema; wanted: show the COMPLETE field set of a claim/entity/artifact so a user can see everything that’s actually available, not a curated subset chosen ahead of time.kg.tables.entity.row-density-and-source-count— [GAP] (#1788) entity rows are full-width; lozenges should be text-width and flow/wrap into rows for information density, and the source count shown per row should be accurate (some entities have several sources and the count under-reports).kg.tables.crud.inline-not-modal— [GAP] (#1888) claim/entity editing today happens in modal sheets (EditClaimSheet,OntologyBrowser’s create sheet,EntityMergeSheet/EntitySplitSheet) — the ask is inline editing / navigation-with-Back instead of a sheet stack, matching the Finder-like direct-manipulation principle the rest of the tables follow.kg.tables.entity-resolution-registry— [GAP] (#1761) a persisted “entity checker”: human merge/alias/reclassify/split fixes on existing entities should CONSTRAIN future imports — re-importing the same folder should respect corrections already made rather than re-creating the entities a human already fixed. Ties to the standing curation-persists-and- constrains-imports ruling; this behavior is the KG-entity half specifically (the importer’s own NLP-draft half isimporter.md’simporter.nlp-never-overwrites-curated-rows).
D. Cross-cutting (both tables)
kg.tables.crud.cross-surface(creative-director ruling, 2026-09-08) — every KG CRUD capability (claim delete, entity create, claim create, edits) must exist across the WHOLE spine: backend → UX (table) → AI (MCP) + CLI → tests → export, not only the SwiftUI table. A capability that works in the table but not via MCP/CLI is NOT done. Each CRUD behavior below is delivered in all surfaces or tracked as an explicit gap.kg.tables.crud.audited— [PARTIAL] every create/edit/delete is ONE typed, audited backend action, reversible via the mutation log (one-audited-action-layer, a standing HARD rule). The table’s OWN create/edit/delete/curate paths ARE compliant (sections B/C above). The rule itself — as a cross-cutting, testable claim spanning the whole KG/entity surface, not just this table — now has its own spec and guardrail: → #4831,harness/audited-action-layer.md(audit.every-mutating-route-uses-the-registry,scripts/check_routes_use_action_layer.py). Not duplicated here.kg.tables.crud.in-place— [PARTIAL] (#4389) a create/edit/delete updates that one row in place; the table is not wholesale re-rendered (stores update one item, not the list). Basic create/edit/delete honor this (sections B/C above), but merging entities re-fetches the whole inspector list instead of updating the merged rows in place — a direct counter-example, same class of regressionharness/observable-data-layer.md’sobservable.store-mutator-updates-in-placetracks generally, filed here specifically because it’s the KG entity-merge path.kg.tables.crud.validation— a create/edit is validated at the boundary (non-empty canonical name; a claim needs at least a subject or text); an invalid input is refused with an inline reason, not silently dropped.
D2. Maintainer test findings, 2026-09-19 (B1-B4)
kg.tables.pane-kind-mismatch— [BROKEN] (#4884) seen live: a Library pane with its kind chip set to Claims still renders the ENTITIES table underneath — columns read Name/Type/Claims, the footer reads “Filter Entities,” while the head says Claims. The pane’s own displayed kind and its rendered content disagree. Cause, VERIFIED by reading: the window-scoped sidebar collection always overrides a pane’s own content kind;PaneConfig.libraryContentKindis never read by the live view. Cross-reference the pane lane’s own design-root behavior inpanes-workspaces.mdonce it exists — not restated here, that spec owns the pane-config model this defect lives in.kg.tables.folder-scope-misses-subfolders— [PARTIAL] (#4885 still open pending close) seen live: with a FOLDER selected, the Entities (and Claims) pane reports “No entities in this folder yet” while the Inspector, looking at the same folder, lists 22 people. Cause, VERIFIED by reading on both sides (not a hypothesis): the app scopes to a folder’s DIRECT child documents only; the engine already HAS recursion, but it is inconsistently opt-in across three separate routes —include_descendantson/api/claims(opt-in), always-on for the entitiesdocument_idfilter, andinclude_childrenon the document knowledge-graph route. No new endpoint is needed; the fix is choosing one consistent default (or one consistent flag) across the three, not building a fourth. The three inconsistent knobs are recorded as their own gap below, distinct from this specific symptom. App half FIXED, 2026-09-19 (6fd114270): the Entities table now scopes by the folder’s own id throughEntityStore.loadAggregatedEntities— the Inspector’s own seam, whose route already recurses the whole subtree server-side — instead of collecting direct child documents client-side and filtering against that set; the client-side direct-children filter is deleted outright, one definition of “this folder’s knowledge.” Claims needed no change (its load already asked for descendants); a new test assertsinclude_descendants=trueis actually sent. Tests executed:EntityStoreTests,EntityServiceTransportTests,SelectAllVisibleSurfaceTests,EntitiesTableCreateTests,KGTableFilterBarPlacementTests— 56 of 56 passing. Not seen working on screen, per the fixing commit’s own words. PARTIAL, not OK: the engine’s three inconsistent recursion switches this behavior originally named are still open, tracked as their own gap immediately below — this fix routes the CLIENT through the one already-recursing seam, it does not unify the engine’s own three knobs.kg.tables.folder-recursion-inconsistent-across-routes— [GAP] (#4885, same evidence as above) three separate engine routes disagree on whether folder scoping recurses into subfolders:include_descendantson/api/claimsis opt-in, the entitiesdocument_idfilter is always-on, andinclude_childrenon the document knowledge-graph route is its own third knob. Expected, stated as intent not a decided mechanism: one consistent default (or one consistent flag name/semantics) across all three, so a caller does not need to know which route silently recurses and which needs an explicit flag.kg.tables.filter-bar-and-footer-are-two-controls— [GAP, DESIGN] (#4856, this issue already tracks exactly this ask — commented, not duplicated) each table pane shows TWO bars today: its own filter bar (Filter entities, All types, New Entity) and the pane’s own footer (+, −, import, Filter). Expected, stated as intent: ONE bar, with Merge and the other curation verbs folded into it too. Open questions, not decided here: which bar’s layout wins, and whether “New Entity”/import stay visually distinct from the curation-verb group or blend into one undifferentiated control strip.kg.tables.entities-master-claims-detail— [GAP, RULED] (#4886) the maintainer’s ruling, 2026-09-19: Entities in one Library pane, Claims in the pane below, is a MASTER/DETAIL pair — selecting an entity in the Entities pane updates the Claims pane below to that entity’s claims. This is a decided design, not an open question; nothing currently wires the two panes together this way.
E. Provenance + versions (creative-director priority — likely its own cross-cutting spec)
kg.tables.provenance.author-mark— a claim/entity shows who AUTHORED it: hand-authored (human) vs AI-extracted, at a glance. (Backend recordscreated_by.) The concrete defect in today’screated_by— machine-extracted claims stored and badged as human-authored — is tracked askg.claim.provenance-kind-is-server-statedinhermeneutic-layer.md(same milestone as its two issues), not duplicated here.kg.tables.provenance.modification-chain— and who MODIFIED it, in order, each step labeled by the actor: an AI extractor (e.g. “apple-vision”, “sonnet-5.1”) or a human. “Apple Vision created it → Sonnet 5.1 changed it → hand-edited by you” is visible.kg.tables.provenance.hand-edit-recorded— a manual edit is recorded AS a human edit in the chain (never attributed to the model whose value it replaced).kg.tables.provenance.careful-versions— [GAP] (#4644) versions are kept carefully so a prior value is recoverable, not just overwritten. (Backend has aMutationLogfor undo; surface it.) #4644 is the broader ask this section’s provenance/version display answers: drag&drop reorganization, a full context menu (merge/combine/comment), select→export JSON-LD, and surfacing claim provenance/geo/time alongside versioning. The export-JSON-LD half iskg-enrichment.md’skg.jsonld.export(already [OK]) wired to a table selection, not a new export mechanism; drag&drop/context-menu/comment are UI affordances not yet built on top of the CRUD this spec already tracks.- NOTE: this is the foundational provenance + version model now specced separately at #4636 (full restorable history · reasoning capture · extensible actor roles extractor/editor/reviewer/end-user + representation types) — sibling of the anchor model #4635. The KG tables are its FIRST surface, but the display here rides that model. So Section E lands AFTER #4636’s model exists; the tables’ first waves (filters + CRUD below) do NOT block on it and ship first.
Scale, bulk operations & first-class library view (creative-director review, 2026-09-09)
The tables must behave like a first-class Mac library view at archive scale (1k–100k entities/claims), not a small demo table. What’s missing:
Bulk operations at scale
kg.scale.batch-delete[MISSING backend] (#4643) — deleting N rows is N sequential per-id DELETEs today (1000 rows = 1000 round-trips) AND aborts mid-loop on one failure, leaving the UX showing already-deleted rows (drift). Batch endpoints EXIST for transition / upsert / curation (/api/claims/batch/transition,/api/entities/batch,/api/kg/claims/batch- curation) but not delete — addPOST /api/claims/batch-delete+/api/entities/ batch-delete(one audited, atomic-where-possible action) so 1k–10k is one call.kg.scale.bulk-correctness— a bulk op prunes exactly the rows that SUCCEEDED (partial failure never drifts the UX); interim client fix = bounded-concurrent deletes collecting successes (reuse theBatchServicemaxConcurrent task-group pattern) until the endpoint lands.kg.scale.normalize-names[MISSING] (#4643) — a bulk “normalize / canonicalize names” op over a selection (or the whole library) via a provider/AI: fold “Matheo del Mazo” / “Mateo del Mazo” to a canonical form + merge. A batch curation/enrichment action, not per-row.kg.scale.progress-cancel— a long bulk op shows progress and is cancellable; bounded so it never pegs the machine (Article 4 resource-safety).
First-class library view affordances
kg.view.contiguous-selection[OK] — Set-based selection gives shift-click range + ⌘-click; keep it. Pinned:SelectionGrammarTests.kg.view.full-row-click-target— [GAP] (#4607) the entities list’s click target should be the FULL row, not just the label text, so the existing selection grammar (kg.view.contiguous-selectionabove) is easy to invoke for bulk curation — today a narrower click target makes multi-select fiddly even though the underlying grammar works.kg.view.keyboard-delete— [BROKEN] (#4794, #4851) ⌘⌫ deletes the selection; ⌘A selects all — same selection grammar as every other library mode, enforced bycheck_selection_grammar.py. Was mis-cited to the four-selection-implementations root-cause issue (long since closed — that unified only the CLICK grammar, not keyboard delete/ select-all); the actual remainder was filed as #4794, and #4851 is the SAME ⌘A gap reported again from tonight’s maintainer testing with a sharper diagnosis — not a separate defect. Retagged from [MISSING] to BROKEN: verified on disk that ⌘A is not merely unimplemented but actively routed elsewhere —LibraryView+TableView.swiftis the only file in the tree wiring a Select-All shortcut; neitherEntitiesTableView.swiftnorClaimsTableView.swifthandlesselectAllat all, so a window-level ⌘A reaches the document browser’s own selection and leaves the KG table’s selection exactly where it was (one row), matching #4851’s own diagnosis exactly.kg.view.type-icons[MISSING] (#4643) — rows use the per-type icons that ALREADY exist (KnowledgeGraphSupport: person/place/org/event/concept/date), not one flat glyph.kg.view.pagination[MISSING at 10k] (#4643) — the table loads up to 25 000 client-side; at 10k+ push filter/scope to the list endpoint (filter.pushdown) and page, so memory and first-paint stay bounded.kg.view.filter-bar-at-bottom— [PARTIAL] (implemented and tested, f47f4b60d; #4856 still open pending close) both tables’ filter bar sits at the BOTTOM, matching the Library pane’s own filter placement, so all three agree. Verified at HEAD: bothEntitiesLibraryContent.swiftandClaimsLibraryContent.swiftput their table BEFOREfilterBarin theirVStack, using the existingPaneFilterBar(placement: .bottom)mechanism the Library pane already uses — no new placement API. Pinned:KGTableFilterBarPlacementTests.testEntitiesTableFilterBarIsAfterTheTableNotBeforeIt,.testClaimsTableFilterBarIsAfterTheTableNotBeforeIt,.testBothTablesReuseThePaneFilterBarComponentAtBottomPlacement(filefichero/Tests/Unit/general/Views/Library/KGTableFilterBarPlacementTests.swift, suiteKGTableFilterBarPlacementTests).kg.tables.select-all-answers-from-the-visible-rows— [PARTIAL] (fixed ed67ec111; #4851 stays open for the maintainer to confirm on screen) ⌘A in the Entities or Claims table selects what the table actually SHOWS, from ONE owner of the command. Was BROKEN: the Entities branch answered from a second,LibraryView-owned entity pipeline that had drifted from what the table itself rendered (its own filter text and type, intersected with the shared search); Claims had no branch at all. Fixed: both tables now report their visible ids throughonVisibleIds— the same shape the dataset modes already use — andselectAllIdsanswers from those published ids, not a second computation. Pinned:SelectAllVisibleSurfaceTests.entitiesTablePublishesVisibleIds,.claimsTablePublishesVisibleIds,.selectAllIdsUsesThePublishedTableIds(filefichero/Tests/Unit/general/Views/Library/SelectAllVisibleSurfaceTests.swift, suiteSelectAllVisibleSurfaceTests).
Test coverage for scale — Swift + UX + backend + LOAD (all required)
| Behavior | Backend (pytest) | Swift (unit) | UX (XCUITest) | Load/background (#4634) |
|---|---|---|---|---|
| batch-delete | endpoint deletes N atomically; audited; partial reported | bounded/bulk delete prunes only successes | select many → delete → rows gone | delete 1k/10k bounded, no peg, timed |
| normalize-names | batch op merges canonically | pure canonicalize rule | select → normalize → merged | 10k under bounded concurrency |
| keyboard-delete | — | command maps to delete action | ⌘⌫ removes selection | — |
| type-icons | — | pure icon-for-type mapping | rows show right icons | — |
Accessibility identifiers (for the click-around leg — TEST-TEMPLATE)
Stable ids on the KG tables so an XCUITest can find + act on them:
- rows: kg.entity.row.<id> · kg.claim.row.<id>
- entity menu: kg.entity.menu.{rename,edit,bless,reject,merge,delete}
- claim menu: kg.claim.menu.{edit,delete}
- create: kg.entity.new · kg.claim.new
(More as interaction verbs land — comment/export/combine, #4644.)
First wave to pin (proposed)
- Filters:
filter.text+filter.entity-type/filter.claim-type+filter.combines-with-search+filter.empty-state(#4625) — cleanest, no backend work. - Claim
delete+ entitycreatein the table (wire existing store/service). - Claim
create(new POST /api/claims wiring: service + store + a NewClaimSheet). - Inline entity rename + claim edit-from-table.
Manual-create is intended (creative-director ruling, 2026-09-08)
A researcher can hand-author a claim/entity, not just curate AI output — a real archival
workflow. So claim.create (POST /api/claims) and entity.create in the table are in
scope, with crud.validation guarding the input.
Findings (from the CRUD-matrix scout, this session) — HISTORICAL, superseded 2026-09-17
Backend CRUD is COMPLETE (entities + claims: create/read/update/delete + curation). The gaps are all UI wiring: claim create missing (no service/store/view); claim edit/delete exist but are wired only into the Ontology cards, not the claim TABLE; the entity table lacks create + inline rename. Filters are client-side on the shared ⌘F query only — no dedicated field. (Details on #4624 / #4625.)
This is now FALSE — claim create, entity create, entity inline rename, and per-table filter fields (text + entity-type/claim-type) all shipped since this scout ran; see the retagged behaviors in sections A/B/C above for current evidence. Kept for history, not as current status.