Skip to content

feat: UX-3 — project history + memesh why file attribution - #181

Merged
kevintseng merged 5 commits into
mainfrom
feat/ux3-project-why
Aug 16, 2026
Merged

feat: UX-3 — project history + memesh why file attribution#181
kevintseng merged 5 commits into
mainfrom
feat/ux3-project-why

Conversation

@kevintseng

Copy link
Copy Markdown
Contributor

UX-3 — project history + memesh why

The work-topology arc's history slice: the Project tab learns to tell one project's story honestly, and a new memesh why <file> answers "why does this file look like this" from local git plus the graph — with every gap in the chain reported as a typed abstention, never guessed around.

memesh why <file> (CLI + POST /v1/why)

  • The chain: local git resolves which commits touched the file (git log --follow, or git blame for --line N) → the graph joins each hash to the commit entity the post-commit hook captured → metadata.session_id walks to the session's entities → file:<basename> tags collect associated memories, labelled "associated, not commit-derived".
  • The load-bearing join is prefix-based, both directions: post-commit names entities commit-<ABBREVIATED hash> (git's own [branch abc1234] output line) while blame/log emit full 40-char SHAs — an exact-name lookup finds nothing, ever. Break-tested: reverting the prefix match to equality turns 3 tests red.
  • Typed abstentions (not_a_git_repo, file_not_tracked, git_unavailable, line_out_of_range, line_uncommitted, no_commit_entity, no_session_link): core emits codes, the CLI renders sentences. A commit memesh never saw says so; a commit captured before session recording says so.
  • The HTTP route never runs git. POST /v1/why takes commit hashes from the caller; its strict schema has no repo-path field on purpose, so the server is never handed a directory to execute anything in (pinned by a test that posts a sneaky cwd key and expects 400).
  • Forward fix: post-commit now records metadata.session_id + metadata.files (cap 50) on commit entities — metadata rather than tags, so pre-edit-recall's file:* join cannot start injecting commit noise into every edit. Break-tested (removing the metadata write turns the new hook test red). captureEntity grew an INSERT-only metadata param that cannot displace the provenance stamps.

Project tab

  • Capture density by category — per-cluster histogram of created_at (the same axis the phase strip segments on), honestly named: it shows what memesh captured, not everything that happened, and the caveat renders beside the title. Bar is aria-hidden; title/note/legend counts carry the information. Quiet periods stay as real empty buckets — the gap is the data.
  • Lineage overlaysupersedes (solid neutral) and contradicts (dashed warning) arcs on the tree timeline, measured off the DOM so an arc only ever connects rows actually on screen; the visible legend counts drawn arcs only. Only the two behavioural relation types are drawn — caused exists in MCP prose but no writer emits it.
  • Superseded nodes come backsupersedes auto-archives its target on write, so the old active-only filter hid exactly the node every chain points at. selectProjectEntities() re-admits archived entities an active one points at with a lineage edge — and only those (pinned by its own test file).
  • ADR-style Decisions view — one card per decision entity with a two-state status derived from the graph (active / superseded; no invented proposed/accepted lifecycle) and its supersession chain spelled out with jump links. Expander per DESIGN.md (aria-expanded/aria-controls, lazy body, memory voice for content).
  • URL deep links — the SPA now writes ?tab= back (the read side existed since the 5-tab shell; nothing ever wrote it) and the Project tab reads/writes ?project=, so a copied URL shows the reader the view being looked at.

Docs & gates in the same PR

  • API_REFERENCE: POST /v1/why + memesh why; ARCHITECTURE: counts follow (28 commands, 33 routes) + core/why.ts module line; CHANGELOG under [Unreleased].
  • i18n: 10 new keys in all 11 locales.
  • Audit baseline: 6 entries re-keyed for pure line shifts (statement verified unchanged), 12 new hits triaged with reasons.
  • New component registered in component-contracts with a dedicated degenerate-input test file.

Verification (run in this session, verdicts by exit code)

node scripts/run-tests-isolated.mjs   exit=0   Test Files 146 passed (146) / Tests 2152 passed (2152); 0 "Errors" lines
npm run verify:release                exit=0   (lint, typecheck, version-coherence, generated-mirror, doc-claims, verification-audit, audit:prod all green)
npm run test:e2e-dashboard            exit=0   "Dashboard packaged e2e smoke passed"

Break-tests against the committed baseline (mutate → red → restore → green):

  • prefix join reverted to exact-name equality → tests/core/why.test.ts 3 failed (exit 1); restored → 12 passed.
  • post-commit metadata write removed → tests/hooks/post-commit.test.ts 1 failed (exit 1); restored → 20 passed.

…ssion metadata

The chain: local git resolves which commits touched a file (log, or blame
for --line), the graph answers what it remembers about them, and every gap
is a TYPED abstention (not_a_git_repo, file_not_tracked, line_uncommitted,
no_commit_entity, no_session_link) — never a guess.

Load-bearing join: post-commit names entities commit-<ABBREV> (git's own
"[branch abc1234]" line) while blame/log emit full 40-char SHAs, so exact
name lookup finds nothing, ever. explainCommits prefix-matches in both
directions (stored abbrev shorter than query, or an API caller's 7-char
hash shorter than a longer stored abbrev).

Surfaces:
- src/core/why.ts — resolveFileCommits (git half, CLI-only) +
  explainCommits (DB half, shared).
- POST /v1/why takes commit HASHES, not a repo path: the server never
  shells out to git; strict schema rejects any repo-path key.
- memesh why <file> [--line N] renders abstentions as sentences; file-tag
  memories are labelled "associated, not commit-derived".
- post-commit now records metadata.session_id + metadata.files (cap 50)
  on commit entities — the commit-to-session hop for future commits, as
  METADATA not tags, so pre-edit-recall's file-tag join cannot start
  injecting commit noise into edits. captureEntity grew an INSERT-only
  metadata param that cannot displace the provenance stamps.

Verification (this session):
- vitest tests/core/why.test.ts + tests/hooks/post-commit.test.ts: 32 passed, exit 0
- vitest tests/transports/http.test.ts: 58 passed, exit 0
- vitest tests/cli/why.test.ts (against dist): 2 passed, exit 0
- npm run typecheck: exit 0; npm run build: exit 0 (6 smoke tests passed)
… view, URL deep links

Four additions to the Project tab, each honest about what it measures:

- Capture density by category: a per-cluster histogram of created_at
  (the same axis the phase strip segments on — NOT last_accessed, which
  the flat fallback groups by). Named for what it shows: what memesh
  captured, not everything that happened; the caveat renders next to the
  title. Bar is aria-hidden ornament; title, note and legend counts
  carry the information. Quiet periods render as real empty buckets —
  the gap is the data. New component CaptureDensityBand, registered in
  component-contracts with its own degenerate-input test file.

- Lineage overlay: supersedes (solid neutral) and contradicts (dashed
  warning) arcs on the tree timeline, measured off the DOM so an arc is
  only drawn between rows actually on screen; the visible legend counts
  drawn arcs only. Only the two behavioural relation types are drawn —
  `caused` appears in MCP prose but no writer emits it. ProjectTab now
  re-admits the archived TARGETS of active supersedes/contradicts edges
  (supersedes auto-archives its target on write, so the plain active
  filter hid exactly the node every chain points at); general archived
  noise stays out. Rule extracted as selectProjectEntities() and pinned
  by its own test.

- Decisions view (ADR): one card per decision-type entity, newest
  first, with a two-state status derived from the graph — superseded
  when a supersedes edge points at it or it arrived archived, active
  otherwise; no invented proposed/accepted lifecycle. Supersession
  chains spelled out with jump links into the tree view. Expander per
  DESIGN.md: aria-expanded + aria-controls, lazy body, memory voice
  (serif) for titles and observations.

- URL deep links: App writes ?tab= back via history.replaceState (the
  read side existed since the 5-tab shell; nothing ever wrote it), and
  the Project tab reads/writes ?project= — a copied URL now shows the
  reader the view being looked at.

Also: CLUSTER_DOT swatch map moved from MemoriesTab to entity-display
as the single owner (the density band is its second consumer);
ARCHITECTURE counts follow (28 commands, 33 routes) plus a core/why.ts
module line; API_REFERENCE documents POST /v1/why and memesh why.

i18n: en keys added in this commit; the ten remaining locales land in
the follow-up i18n sync commit (dashboard-i18n gate is red until then,
by design of the split).

Verification (this session):
- vitest tests/dashboard/{CaptureDensityBand,ProjectRoadmap,component-contracts}: 200 passed, exit 0
- vitest tests/dashboard/ProjectTab.select.test.ts + component-contracts: 189 passed, exit 0
- npm run typecheck: exit 0
- node scripts/check-doc-claims.mjs: exit 0
Six baseline entries moved by pure line shifts (why command inserted in
cli.ts, CLUSTER_DOT block in entity-display, its removal in MemoriesTab)
— re-keyed with the statement verified unchanged, not re-triaged. Nine
new C5 hits are the optional-input defaults of the why chain and the
lineage/ADR views (each reason states why the empty default is the
honest answer, not masked failure), and three C1 hits are the new test
files, each pairing emptiness with positive assertions.

Verification: node scripts/audit/verification-audit.mjs exit 0 (every
hit triaged) after the change; exit 1 with 15 untriaged before.
roadmap.viewDecisions / densityTitle / densityNote / lineageSupersedes /
lineageConflicts and the five adr.* keys, inserted at the same relative
position (after roadmap.moreEntities) in zh-TW, zh-CN, ja, ko, pt, fr,
de, vi, es and th, keeping the blocks structurally parallel. {n}
placeholders and the literal type value "decision" stay untranslated.
ja/ko direction words follow each locale's existing relation.supersedes
vocabulary so the ADR chain labels read as prefix + link.

Verification: npx vitest run tests/dashboard-i18n.test.ts — 20 passed,
exit 0 (run in this tree after the merge, not only in the lane
worktree); npm run typecheck exit 0.
…d changes

check-generated-mirror caught the staleness: plugin-marketplace installs
run dist/ as committed and never build.

[Verified-By: npm run build exit=0 — '✅ All 6 smoke tests passed / Build is ready for use']
@kevintseng
kevintseng merged commit 0f5c761 into main Aug 16, 2026
13 checks passed
@kevintseng
kevintseng deleted the feat/ux3-project-why branch August 16, 2026 14:55
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