Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions doc/2026-11-01_design_id2259.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# Design: local friendly-name reference resolution (#2259)

- Date: 2026-11-01
- Status: **design recommendation** — ready to implement as an isolated local-store PoC; no implementation is included in this record.
- Issue: [#2259](https://github.com/developing-today/code/issues/2259), from the [#2047 investigation](https://github.com/developing-today/code/issues/2047), under epic #2056.
- Prior spec: [friendly-name reference PoC spec](2026-10-30T13-25-00Z_design_friendly_name_references.md), merged as PR #2742. This note makes its trade-offs and implementation decisions explicit; it does not supersede the spec.

## Decision to be made

Approve the local PoC contract: references bind to a stable target ID, while lookup uses an authorized local view and returns provenance-bearing candidates in explicit rank tiers. The author, not the ranking function, chooses among contested or tied candidates. Keep the PoC out of the production path and leave cross-store fan-out for later.

Two decisions still need Drew's ruling: whether a document reference names one immutable `document.put` revision or a logical note across revisions (Q1), and how to handle the stalled PoC PR #2756 (Q2). Neither blocks this recommendation or this design record.

## The shape

```text
typed @/# phrase
-> viewer-authorized local snapshot
-> ranked candidates with match and claim provenance
-> author explicitly chooses a target
-> stored Reference(target ID + sigil + authored surface + locator hints)
-> render by target ID; preserve the authored surface as the label
```

## 1. Context and current state

- The #2047 investigation says the phrase is a query, not the durable reference; it proposes a stable object ID plus sigil, author-chosen surface, locator hints, and author context. It also sets viewer-relative resolution, petnames ahead of public claims, and no silent selection of contested aliases. The #2259 body narrows that work to a local-store PoC in the notes/documents surface, with duplicate-title and contested-alias cases, renamed/cleared rendering, a findings note, and no production feature.
- The later spec PR #2742 is merged. The PoC PR #2756 is still open. Earlier issue comments say its Codex findings were resolved; the latest #2259 escalation says findings remain unresolved. Following the newer issue record, treat the review disposition as open and re-check it before merging #2756.
- Current `DocumentRecord.id` is the `document.put` event ID, and its `DocumentRef.content` is a separate BLAKE3 content/blob hash ([`documents.rs:40-50`](../pkgs/id/src/documents.rs#L40-L50), [`documents.rs:153-166`](../pkgs/id/src/documents.rs#L153-L166)). The web save path publishes a new signed document revision on save ([`routes.rs:1194-1198`](../pkgs/id/src/web/routes.rs#L1194-L1198), [`routes.rs:1245-1264`](../pkgs/id/src/web/routes.rs#L1245-L1264)). Thus an event ID is stable for that revision, but does not by itself identify a logical note through later saves.
- `DocumentRecord` has no title field; the web save path attaches a `filename` tag separately ([`documents.rs:153-166`](../pkgs/id/src/documents.rs#L153-L166), [`routes.rs:1245-1252`](../pkgs/id/src/web/routes.rs#L1245-L1252)). The PoC must say which local name source it indexes; it must not assume `documents.rs` already provides a title.
- Existing `search` builds a `TagIndex` from live tag claims and rebuilds it per call; it matches whole tag values and explicitly does not provide string-prefix or full-text search ([`search.rs:1-11`](../pkgs/id/src/search.rs#L1-L11), [`claims.rs:79-86`](../pkgs/id/src/event_store/claims.rs#L79-L86)). Prefix, token, and full-text candidates therefore need resolver-owned matchers, not claims about existing `TagIndex` behavior.
- The Markdown converter represents links as structured link marks and converts a document tree to Markdown ([`markdown.rs:555-577`](../pkgs/id/src/web/markdown.rs#L555-L577), [`markdown.rs:1633-1664`](../pkgs/id/src/web/markdown.rs#L1633-L1664)). Serialize a reference through that structured path; do not interpolate the author's surface directly into Markdown syntax.
- `EventStore::tag_claims` returns the live claim set ([`claims.rs:32-39`](../pkgs/id/src/event_store/claims.rs#L32-L39)); the reference resolver must filter to the viewer-authorized view before returning candidates, as #2259's related visibility requirement (#2030) and the #2047 private-note case demand.
- Existing display rules already show a local petname first and mark multiple alias claims contested without picking one ([`aliases.rs:7-17`](../pkgs/id/src/aliases.rs#L7-L17), [`aliases.rs:146-158`](../pkgs/id/src/aliases.rs#L146-L158)). Petnames live only in the owner's local store ([`petname.rs:1-6`](../pkgs/id/src/petname.rs#L1-L6)); the current petname store is keyed by owner/account strings ([`petname.rs:34-39`](../pkgs/id/src/petname.rs#L34-L39)), while alias rows use subject hashes ([`aliases.rs:114-123`](../pkgs/id/src/aliases.rs#L114-L123)). The PoC adapter must make that identity mapping explicit.

## 2. Viable options and trade-offs

### Reference representation

| Option | Benefits | Costs / failure mode |
| --- | --- | --- |
| Store only the phrase; resolve again when rendered | Smallest stored form | Meaning can change when claims or names change; collisions make old text ambiguous. |
| Store only an opaque target ID | Durable and unambiguous | Drops the author's chosen label and context; plain text is opaque. |
| **Store a hybrid reference (recommended)** | Stable identity plus authored surface, sigil, and locator context; renames do not silently rewrite what the author wrote | More fields and explicit render states; requires a decision on whether a document target is a revision or a logical note. |

The hybrid representation follows the merged spec: `target_id`, `sigil_kind`, `surface`, `locator_hints`, and `author_context`, with plain-text fallback `[surface](id://<target-id>)` (spec §1). For a document, the PoC should use `DocumentRecord.id`, not `DocumentRef.content`, as the target ID; the latter identifies blob content, not the record/event.

### Index and resolution seam

| Option | Benefits | Costs / failure mode |
| --- | --- | --- |
| One global index | One query and one ranking surface | Conflicts with local-first ownership and risks becoming a private-object existence oracle. |
| Reuse `TagIndex` as the entire resolver | Reuses existing exact claim search | Cannot supply document names, private petnames, string-prefix, or full-text matches; current search documents those limits. |
| **Build a transient, per-viewer local candidate snapshot (recommended)** | Composes currently available local claims, names, petnames, and document text while keeping ranking and authorization in one resolver module | Each query builds/reads a local view; any future performance index can be added only after the PoC measures a need. |

For the PoC, derive candidates from one local store and the viewer's own petnames; do not persist a second index or query remote stores. Use the local `filename` tag as a document name where present. Any prefix, token, or full-text matching is explicit resolver behavior. If local document bodies cannot be read through the existing notes/documents surface, report that gap rather than returning a fabricated `FullText` match.

### Ranking and selection

| Option | Benefits | Costs / failure mode |
| --- | --- | --- |
| One blended score with automatic top-hit selection | Simple result handling | Hides why a candidate won and can silently promote a contested or weak claim. |
| **Ordered match tiers with provenance (recommended)** | Makes local naming precedence and match strength inspectable; keeps soft boosts from crossing semantic tiers | Requires candidates to carry match kind and provenance. |

Recommended local ordering: (1) viewer-local petname for `@` actors; (2) exact local alias/name/filename or tag claim; (3) prefix; (4) token; (5) full-text. Keep any recency or author-affinity boosts within a tier. Sort ties deterministically. Deduplicate the same target ID while retaining its claim/store provenance. With one local store, there is no cross-store precedence tier in the PoC.

## 3. Recommended representation and behavior

The minimum conceptual types are:

```text
Reference {
target_id: Hash, // document event ID for a document target in this PoC
sigil_kind: Actor | Topic, // '@' or '#'
surface: String, // author's label; not re-resolved as identity
locator_hints: Vec<StoreId>, // local locator only in this PoC
author_context: ActorId,
}

Candidate {
target_id: Hash,
sigil_kind: Actor | Topic,
match_kind: Petname | ExactClaim | ExactName | Prefix | Token | FullText,
provenance: { store, author, claim_state },
label: String,
}
```

Resolution returns a ranked candidate set, not a selected ID as a side effect. The caller stores a reference only after author selection. A contested alias or ambiguous top tier must remain explicit and cannot become a silent pick. An empty result leaves the phrase as ordinary text.

At render time, look up the stored target ID in the viewer's authorized local view. Keep the stored `surface` even if its current alias/name changes; show the current label separately as preview/context. A removed target renders the surface with a cleared marker. A target unavailable to this viewer renders inert surface text with an unresolved marker and no additional target metadata.

For the revision-level recommendation in Q1, a “renamed target” test means the target ID is unchanged while its display claim/name changes. Saving new document bytes creates a new event ID and is a new revision, not a rename. If the product instead wants a reference to follow a logical note across saves, it needs a stable logical ID and lifecycle rules beyond this PoC.

## 4. Invariants

1. **Identity is not presentation.** Resolve stored references by target ID, never by re-running the phrase. For documents, keep event ID distinct from blob content hash.
2. **Authored surface is historical.** Renaming a target never silently rewrites the reference surface; cleared and unresolved targets still render that surface.
3. **Viewer-relative and authorized.** Candidate construction happens only over the viewer-authorized local view. Hidden records must not affect candidate presence, rank, count, or explanatory metadata.
4. **No silent ambiguity.** Contested claims and ambiguous top candidates require an explicit author choice.
5. **Evidence travels with candidates.** Candidate match kind and provenance are available to the selection UI and tests.
6. **Local-only proof.** One local store and local petnames only; no hosted-store lookup, remote fan-out, or production resolution call site under #2259.
7. **Stable results.** Identical inputs yield identical candidate ordering, independent of event arrival order; current tag search already specifies deterministic ordering for its own hits ([`search.rs:13-14`](../pkgs/id/src/search.rs#L13-L14)).
8. **Surface is data, not markup.** Markdown output is built from a structured link node/mark so punctuation in the author-chosen surface cannot change the link target or surrounding document structure.

## 5. Acceptance criteria for implementation

- [ ] A reference serializes and round-trips with target ID, sigil, surface, locator hints, and author context; the Markdown fallback retains the target ID and treats punctuation in `surface` as text, not syntax.
- [ ] The implementation documents and tests the selected target identity: `DocumentRecord.id` event revision for this PoC, distinct from the content/blob hash.
- [ ] The local resolver returns ranked candidates with match kind and provenance for both sigils; `@` petnames are viewer-local and only apply to actors.
- [ ] The index's document-name source is explicit (`filename` tag where available); prefix/token/full-text modes are implemented by the resolver or recorded as unsupported, never attributed to existing tag search.
- [ ] Duplicate titles and contested alias claims return all visible candidates and never silently select one. Repeated target IDs are deduplicated without discarding provenance.
- [ ] Authorization is applied before matching; a private/inaccessible object cannot alter visible results or disclose its existence.
- [ ] Stored-reference insertion requires a selected target. Rendering tests cover resolved, renamed-label, cleared, and unresolved-for-viewer states while preserving the stored surface.
- [ ] Findings are posted on #2259: what the local PoC established and what cross-store fan-out/locator fetch still owe.
- [ ] No production reference-resolution feature is claimed or enabled by this issue.

## 6. Independently mergeable implementation plan

These are clean PR boundaries if the PoC is restarted or split. Each lands on `main`, builds independently, and leaves the project in a testable state; do not stack branches.

1. **Reference model and codec.** Add an isolated model/codec for the hybrid reference, Markdown fallback, target identity, sigil, and render-state types. Add round-trip and identity-vs-content-hash tests. No production caller.
2. **Local candidate snapshot and ranking.** Add a local resolver module that takes a viewer-authorized snapshot, joins supported local names/claims/petnames, produces candidates with provenance, applies deterministic tiers, and refuses contested/tied auto-selection. Test duplicates, visibility, sigil separation, and each implemented match kind.
3. **Notes/documents PoC surface.** Wire explicit candidate selection into stored-reference insertion and rendering. Test resolved, renamed-label, cleared, and unresolved states, plus the Markdown round-trip. Keep the module isolated from production behavior and post the findings to #2259.

The current #2756 PR already proposes one isolated local PoC slice. If Drew chooses to resume it, these phases are not a request to rebase or split that existing branch; review and land that slice as one PR if it meets these criteria. If it is abandoned, use the phases above for new work. Cross-store fan-out and a stable logical-note identity remain separate follow-up decisions/issues.

## 7. Open questions for Drew

- **Q1 — Document identity.** Should a reference target (a) the immutable `DocumentRecord.id` / `document.put` revision, as recommended for this PoC because it is the identity the current record exposes; the citation then names that revision, and a later save is a new target; or (b) a stable logical note ID that follows the note across saves, which requires identity/lifecycle support not represented by the current `DocumentRecord`? **Recommendation: (a) for #2259; decide (b) only if “follow the note through edits” is a required product behavior.**
- **Q2 — Existing PoC PR #2756.** Should we (a) commission a worker round to re-check and disposition its Codex findings, then land it if it meets this design (**recommended: it is the named PoC deliverable and avoids duplicating work**); (b) close it as abandoned and assign the PoC afresh; or (c) leave it parked? The latest #2259 escalation names these options; older comments conflict on the findings, so the newer unresolved status should govern until the PR threads are checked.

No other Drew decision is needed to implement the local PoC against the recommended defaults. Cross-store fan-out, locator fetch, and production enablement remain out of scope.

## References

- [Issue #2259](https://github.com/developing-today/code/issues/2259), especially the scope/acceptance criteria and latest escalation comment.
- [Issue #2047](https://github.com/developing-today/code/issues/2047), the original friendly-name investigation sketch and ambiguity cases.
- [Merged local PoC spec](2026-10-30T13-25-00Z_design_friendly_name_references.md) (PR #2742).
- [Open local PoC PR #2756](https://github.com/developing-today/code/pull/2756).