feat: UX-4 — two-layer graph (work layer + evidence on demand) - #183
Merged
Conversation
UX-4's first half — the part that has to exist before the graph can draw two layers. Hooks capture evidence but never draw the edge that ties it to the work it supports, so every work node would render a zero badge. kg-backfill Rule 5 (`evidences`, default ON): match evidence captures to work items by exact session id — the session: tag, plus commit metadata.session_id, which is where post-commit stamps it precisely so pre-edit recall does not inject commit noise. No session match falls back to the most recent same-project work node created BEFORE the capture, so a bulk first run distributes history across the items that were current at the time instead of piling every old commit onto today's newest node. Its sources are evidence entities, not orphans, so the zero-orphan early-out now checks the rule is actually off before taking the shortcut. Server: `GET /v1/graph?layer=work` returns work-layer entities, work↔work relations and per-node incoming-evidence counts; `GET /v1/graph/evidence?node=` is the drill-down, capped at 200 with a `truncated` flag (R2's honesty rule — a full window says so). An unknown layer value is a 400, not a silent full-graph response. [Verified-By: npx vitest run tests/core/kg-backfill.test.ts tests/core/graph-work-layer.test.ts exit=0 — 'Tests 59 passed (59)'; tests/transports/http.test.ts exit=0 — 'Tests 63 passed (63)'; npx tsc -p tsconfig.check.json --noEmit exit=0. Break-tests, each restored and re-verified green: temporal fallback guard removed → 1 failed; zero-orphan early-out reverted → 1 failed; archived-evidence filter dropped from the badge count → 1 failed]
UX-4's second half. The Knowledge Graph tab now asks for
`/v1/graph?layer=work` first and draws decisions, lessons and plans with
a badge counting the evidence under each; clicking a node loads that
evidence and only then (measured: 246 evidence entities against 53 work
items on a real graph).
Three things kept honest rather than convenient:
- Work nodes rank by recency, not recall traffic. A decision made this
morning has access_count 0 and was the LAST thing the old ranking
named. Status is deliberately NOT a second key: of 53 active work
entities measured today, zero are the target of a supersedes edge —
supersession archives the loser and this view is active-only, so
ranking on it would rank on a constant.
- Too few work items falls back to the full graph and SAYS so. Silent
fallback is what R2 just removed from recall.
- A work payload with no `evidenceCounts` is a shape error, not `?? {}`.
Defaulting it would draw every badge as zero — 'nothing supports this
decision' — from a response that never answered the question.
The badge lives inside the label budget and the hit-test, not only the
draw call: a target you can see and cannot click is worse than none.
Two contracts moved with the code rather than being worked around.
`evidences` is registered in a new `DERIVED_RELATION_TYPES` (the types
kg-backfill draws, as against the ones a model states deliberately), so
relation-types-documented still fails on a genuinely undocumented
behavioural type while not demanding a --evidences flag for an edge no
model is asked to write; a companion test pins the two lists disjoint.
The i18n scan now imports that constant instead of regexing the union it
used to read as text — the rename alone had made its match null, and a
null match made the check vacuous.
[Verified-By: node scripts/run-tests-isolated.mjs exit=0 — 'Test Files 150 passed (150)' / 'Tests 2190 passed (2190)', no Errors line; npm run verify:release exit=0. Break-tests, each restored and re-verified green: fallback announcement suppressed → 1 failed; WORK_LAYER_MIN_NODES raised to 9999 → 2 failed; evidenceCounts guard reverted to '?? {}' → 1 failed; relation.evidences dropped from the en catalogue → 2 failed]
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
UX-4 — the graph has two layers: the work, and the evidence under it
The Knowledge Graph tab used to draw everything memesh has stored at one
altitude, ranked by recall traffic. On a real graph that means 168 commits and
78 session captures compete for attention with 16 decisions, and the decision
made this morning — access count zero — is the last thing labelled.
This splits the view. The work layer (
WORK_LAYER_TYPES, the one whitelist insrc/core/work-topology.ts) is what loads; each node carries a badge countingthe mechanical capture that supports it; clicking a node loads that evidence and
only then.
The edge had to exist first
Hooks capture evidence but have never drawn an edge to the work it supports, so
every badge would have read zero.
kg backfillgains Rule 5 (evidences,on by default,
--no-evidence-linksto disable):session:*tag, ormetadata.session_idfor commit entities, which carry no session tag by design (UX-3 put it in
metadata precisely so pre-edit recall would not inject commit noise).
before the capture. A first run over months of history therefore
distributes it across the items that were current at the time, instead of
piling every old commit onto whatever node is newest today.
to something else is still evidence — so the zero-orphan early-out now checks
the rule is actually off before taking the shortcut.
Server
GET /v1/graph?layer=work→ work entities, work↔work relations, per-nodeevidenceCounts.GET /v1/graph/evidence?node=NAME→ the drill-down, capped at200 with a
truncatedflag. An unknownlayervalue is a 400, not a silentfull-graph response; a node that does not exist is a 404, distinct from a node
with no evidence (200, empty arrays).
Three places this chose honest over convenient
deliberately not a key: measured on the live graph today, 0 of 53 active
work entities are the target of a
supersedesedge — supersession archivesthe loser and this view is active-only, so a status axis would rank on a
constant. The verdict asked for "status/recency"; the measurement says the
status half would have been decoration.
empty work layer is the normal state — the tab shows the full graph and says
it did. A silent fallback is exactly what R2 removed from recall.
?? {}onevidenceCounts. A work payload missing that field is ashape error. Defaulting it would render every badge as zero — "nothing
supports this decision" — from a response that never answered the question.
The badge participates in the label budget and the hit-test, not just the draw
call: a target you can see and cannot click is worse than no target.
Two contracts moved with the code
evidencesis registered in a newDERIVED_RELATION_TYPES— the typeskg-backfilldraws, as against the ones a model states deliberately(
supersedesarchives;contradictssurfaces a conflict). Without thatdistinction
relation-types-documentedwould have demanded amemesh remember --evidencesflag and a line in every model's tool schema foran edge no model is asked to write. A companion test pins the two lists
disjoint, so the derived list cannot become a way to retire a behavioural type
from the schema the model reads.
The i18n scan that derives backfill's relation vocabulary now imports that
constant instead of regexing the inline union it used to read as source text —
naming the union (a refactor that changed no behaviour) had made its match null,
and a null match made the check vacuous.
Verification (run in this session, verdicts by exit code)
Break-tests — each mutation applied to the committed code, then reverted and
re-verified green:
ORDER BY created_at DESC→ASCin the drill-downsetFellBack(true)→false)WORK_LAYER_MIN_NODESraised to 9999evidenceCountsguard reverted to?? {}relation.evidencesdropped from the en catalogueOne of those mutations initially survived and the fix is in the diff: the
first version of
graph-two-layer.test.tsxbuilt its fixture asWORK_LAYER_MIN_NODES + 1, so raising the threshold moved the fixture with itand the test could not fail. It now uses a fixed count and pins the constant
separately.
The full suite also caught four dashboard fixtures that answered every URL with
one payload shape; they now answer
?layer=workwith the shape a real serversends. One of them was sharing a single
Responseobject across calls, whichonly surfaced because this tab legitimately fetches twice on the fallback path.
Follow-ups (not in this PR)
memesh kg backfillhas to be run once for the badges to populate on anexisting graph; the docs and the empty-state copy both say so.
5) still need the recall event stream.