Skip to content

feat(ctx): phase-2 supersede — correct an immutable fact via forward link#444

Merged
eris-ths merged 3 commits into
mainfrom
feat/ctx-supersede
Jun 23, 2026
Merged

feat(ctx): phase-2 supersede — correct an immutable fact via forward link#444
eris-ths merged 3 commits into
mainfrom
feat/ctx-supersede

Conversation

@eris-ths

Copy link
Copy Markdown
Owner

What

The first phase-2 ctx verb: ctx supersede <old-id> --fact "...".

ctx records are immutable on save, so a "correction" is not an in-place edit — it records a new fact whose supersedes field points back at the one it replaces. The old record is never mutated, so the ledger keeps both and the supersession is reconstructable from the forward-only link alone.

Surface

  • ctx supersede <old-id> --fact "..." [--tag ...] [--by ...] — records the correction with a supersedes link. A missing target is a recoverable not-found (a correction must point at something real); a self-supersession is rejected at the domain boundary.
  • ctx list — folds superseded facts out by default (current view); --all keeps every fact, marking the superseded ones.
  • ctx show <old-id> — stays readable, resolves the reverse link at read time as superseded_by, an array of successor ids (empty while current; more than one when two independent corrections fork the same fact — both are reported rather than silently picking one).

Invariants

  • Acyclic by construction. Every link points strictly backward to an id that already existed, so no cycle can form; the domain also rejects self-supersession outright. A chain (C supersedes B supersedes A) is allowed.
  • Always a current head. The newest fact can never be superseded (nothing is written after it to point back), so a non-empty store always keeps at least one current head; an empty default list means the store is genuinely empty.
  • Byte-stable YAML. The supersedes key is omitted from an ordinary record's YAML, so phase-1 records round-trip byte-for-byte.

Commits

  1. feat(ctx): phase-2 supersede — domain field + use case + handlers + wiring + docs + tests.
  2. refactor(ctx): supersede follow-ups — fork-aware reverse link (returns all successors), drop an unreachable empty-message branch.
  3. refactor(ctx): dedup write path, sort comparator, tag parsingappendFact() (shared write path), byNewestFirst() (shared comparator), parseTagList() (shared --tag splitter).

Verification

  • Full suite green: 1846/1846 pass, 0 fail (node tests/run.mjs, exit 0).
  • Dogfooded through the real binary: record → supersede → list fold → --all → show reverse-link array → fork (two successors) → error paths (missing target, malformed id, missing --fact) → byte-stable YAML.
  • Updated the verb-consistency / version-string / nearest-command tests that pinned the phase-1 surface.

Remaining phase-2

fork / chain / status. chain is the design test the docs flag (junk-drawer risk at the 100-record scale); supersededBy notes where a persisted reverse index would land if that materializes.

🤖 Generated with Claude Code

eris-ths added 3 commits June 23, 2026 16:00
…link

ctx records are immutable on save, so a correction is a NEW fact whose
`supersedes` points back at the one it replaces; the old record is never
mutated. `ctx list` folds superseded facts out by default and gains
`--all` to keep them (marked); `ctx show <old-id>` resolves the reverse
`superseded_by` link at read time. Chains stay acyclic (links point
strictly backward; domain rejects self-supersession); a missing target is
a recoverable not-found. `supersedes` is omitted from ordinary records'
YAML so phase-1 records round-trip byte-for-byte.

Remaining phase-2 verbs: fork / chain / status.
…ead branch

- supersededBy now returns ALL successors (newest-first), not the first:
  two independent corrections of one fact (a fork A<-B, A<-C) are both
  reported instead of silently picking one. JSON `superseded_by` is now a
  string[] (empty while current); text lists all. Cost is one substrate
  scan, inherent to the flat layout — documented as the spot a reverse
  index lands if `chain` pushes past the junk-drawer scale.
- list: drop the unreachable 'all superseded' empty-message branch. The
  newest fact can never be superseded (nothing is written after it to point
  back), so a non-empty store always keeps a current head; an empty default
  list is genuinely empty. Removes a dead branch with a misleading message.
- docs (verbs.md / AGENT.md) + changelog updated to the array contract,
  fork behavior, and the empty-store invariant. Adds a fork test.
Three duplications removed, behavior unchanged (full suite green):
- appendFact() — the shared 'allocate id -> Ctx.create -> saveNew' write
  path. record() delegates directly; supersede() validates the target then
  delegates with the link. One write path, consistent id/timestamp pairing.
- byNewestFirst() — the created_at-desc / id-desc comparator was copied in
  list() and supersededBy(); now one module helper both reuse (and a future
  verb like chain can share).
- parseTagList() — the --tag splitter was copied verbatim in record.ts and
  supersede.ts; extracted to handlers/parseTagList.js, imported by both.

Also widens the optional create/append param types to 'T | undefined' so
they satisfy exactOptionalPropertyTypes when a key is passed explicitly.
@eris-ths eris-ths merged commit d5af87b into main Jun 23, 2026
5 checks passed
@eris-ths eris-ths deleted the feat/ctx-supersede branch June 23, 2026 08:06
eris-ths added a commit that referenced this pull request Jun 23, 2026
Follow-up to #444: the supersede PR updated docs/verbs.md and the ctx
body sections, but three phase-1 references were left stale across the
repo:
- AGENT.md TOC anchor still pointed at '...alpha-phase-1'
- CLAUDE.md one-liner said 'phase 1 は record のみ'
- docs/playbook.md ctx-only patterns said 'ctx ships only record today'

All now reflect the shipped surface (record / supersede / list / show +
OKF) and the remaining phase-2 verbs (fork / chain / status).
eris-ths added a commit that referenced this pull request Jun 23, 2026
Folds three fragments into CHANGELOG.md and bumps package.json +
package-lock.json (root + packages[""]) to 0.7.1:
- Added: ctx supersede (#444) — the first phase-2 ctx verb.
- Changed: changelog-release now bumps the manifests (#441/#442) and
  refuses orphan fragments (#441).

Alpha patch bump (minor reserved while ctx is still phase-2). This also
re-aligns the released version with main: v0.7.0 predated #443/#444, so
the bin was reporting 0.7.0 while carrying unreleased verbs.
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