Skip to content

feat: UX-4 — two-layer graph (work layer + evidence on demand) - #183

Merged
kevintseng merged 2 commits into
mainfrom
feat/ux4-two-layer-graph
Aug 17, 2026
Merged

feat: UX-4 — two-layer graph (work layer + evidence on demand)#183
kevintseng merged 2 commits into
mainfrom
feat/ux4-two-layer-graph

Conversation

@kevintseng

Copy link
Copy Markdown
Contributor

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 in
src/core/work-topology.ts) is what loads; each node carries a badge counting
the 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 backfill gains Rule 5 (evidences,
on by default, --no-evidence-links to disable):

  • Matched on an exact session id — the session:* tag, or metadata.session_id
    for 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).
  • No session match falls back to the most recent same-project work item created
    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.
  • Its sources are evidence entities, not orphans — a commit that already relates
    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-node
evidenceCounts. GET /v1/graph/evidence?node=NAME → the drill-down, capped at
200 with a truncated flag. An unknown layer value is a 400, not a silent
full-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

  • Ranking. Work nodes rank by recency, then evidence count. Status is
    deliberately not a key: measured on the live graph today, 0 of 53 active
    work entities are the target of a supersedes edge — supersession archives
    the 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.
  • The fallback speaks. Under three work items — a young install, where an
    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.
  • No ?? {} on evidenceCounts. A work payload missing that field is a
    shape 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

evidences is registered in a new DERIVED_RELATION_TYPES — the types
kg-backfill draws, as against the ones a model states deliberately
(supersedes archives; contradicts surfaces a conflict). Without that
distinction relation-types-documented would have demanded a
memesh remember --evidences flag and a line in every model's tool schema for
an 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)

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   (lint, typecheck, version coherence, generated mirror, doc claims, verification audit, audit:prod)

Break-tests — each mutation applied to the committed code, then reverted and
re-verified green:

Mutation Result
Temporal fallback guard removed (link to newest regardless of time) 1 failed
Zero-orphan early-out reverted (Rule 5 skipped on a connected graph) 1 failed
Archived-evidence filter dropped from the badge count 1 failed
ORDER BY created_at DESCASC in the drill-down 1 failed
Fallback announcement suppressed (setFellBack(true)false) 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

One of those mutations initially survived and the fix is in the diff: the
first version of graph-two-layer.test.tsx built its fixture as
WORK_LAYER_MIN_NODES + 1, so raising the threshold moved the fixture with it
and 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=work with the shape a real server
sends. One of them was sharing a single Response object across calls, which
only surfaced because this tab legitimately fetches twice on the fallback path.

Follow-ups (not in this PR)

  • memesh kg backfill has to be run once for the badges to populate on an
    existing graph; the docs and the empty-state copy both say so.
  • Germination replay and the mycelium pulse (DESIGN.md's living mechanics 4 and
    5) still need the recall event stream.

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]
@kevintseng
kevintseng merged commit 7322705 into main Aug 17, 2026
13 checks passed
@kevintseng
kevintseng deleted the feat/ux4-two-layer-graph branch August 17, 2026 03:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant