Skip to content

Latest commit

 

History

History
328 lines (272 loc) · 18.7 KB

data-model.md

File metadata and controls

328 lines (272 loc) · 18.7 KB

Data Model

Lighthouse review content is persisted as plain JSON under a workspace's .vscode/lighthouse-reviews/ directory. There is no review database and no network state. The schema source of truth is src/reviewModel.ts (types, parse, migration), src/reviewCore.ts (pure serialization/mutations), and src/reviewPersistence.ts (coordinated transactions). src/reviewStore.ts adapts these to VS Code.

1. On-disk layout

<workspaceFolder>/.vscode/lighthouse-reviews/
├── registry.json                       # list of known reviews + activeReviewId
└── <compare>_<base>/                    # one folder per comparison; branch '/' -> '-'
    └── comments.json                    # the review: threads, comments, reviewedFiles
  • Glob (activation + scan): .vscode/lighthouse-reviews/**/comments.json (REVIEWS_GLOB in reviewStore.ts). Deliberately not the original extension's .vscode/local-reviews/ — the two coexist.
  • Folder name: <compare>_<base> where compare = SOURCE branch and base = TARGET branch, each run through sanitizeBranch (every / → -). Example: compare feature/mul, base main → feature-mul_main/.
  • Activation events: onStartupFinished and workspaceContains:.vscode/lighthouse-reviews/**/comments.json, plus implicit view/command activation. Inline comments and local usage observation start without opening the sidebar; heavy comparison initialization remains on demand.

Comparison preferences

VS Code workspace state stores the selected target per source branch and an optional full baseline commit SHA per target/source pair. These are local UI preferences, not review-schema fields. Changing the inferred baseline or a pin does not rename review folders or rewrite comments. targetBranch/targetCommit continue to describe the selected target, not the effective baseline. Legacy pins are revalidated on refresh. Choosing a Base branch clears an older pin for that pair; the Comparison UI does not expose separate Pin/Auto controls.

Set as Base on a history row uses a full SHA as the selected target. Its review folder is <compare>_<full-sha>/, with that SHA in targetBranch and targetCommit. This reuses v2 fields without a migration and remains separate from branch-named reviews. Fixed targets are validated as ancestors of the source, just like pins. Selecting a fixed target clears any older effective-baseline pin for that same pair, so Set as Base always starts from the chosen row. Unpin clears only the current source's remembered target override and the applicable old/new baseline-pin entries, then recomputes the automatic target. Review files themselves are untouched; returning to a branch target selects its existing branch-named review again.

Moving reviews from a previous extension identity

Lighthouse uses the VS Code extension identity TimothyMothra.lighthouse. It only registers lighthouse.* commands/settings and discovers .vscode/lighthouse-reviews/. Before switching, save important drafts and stop the old extension's writers. Back up the review store, then move the previous installation's entire review-store directory to .vscode/lighthouse-reviews/, preserving all JSON bytes, registries and comparison folders. If the destination already exists, stop and reconcile it explicitly; do not overwrite it.

Update explicit settings and command keybindings to the lighthouse.* namespace. VS Code's private layout/state, pinned targets and open diff/thread tabs do not migrate automatically: record wanted preferences and reapply/reopen them in Lighthouse. Do not edit VS Code databases or rewrite user-authored review prose or historical diagnostic logs. The v1/v2 review schema and the original extension's .vscode/local-reviews/ remain unchanged.

2. Sources → readers

On-disk source Read by Produces
registry.json reviewStore reviews list + activeReviewId
<compare>_<base>/comments.json parseReview (reviewModel) a Review (threads, comments, reviewedFiles, seqCounter)
historical git blobs (via ReviewDiffContentProvider) commits/files diff views left/right sides of a diff
saved review threads (via ConversationDocumentProvider) read-only conversation tabs full replies and recorded anchor context, even when the code file is gone

Conversation transcripts do not add a new on-disk review format. They read comments.json and never rewrite it just to display a discussion. anchorText, when present, is only the captured trimmed anchor line; it cannot reconstruct an entire deleted uncommitted file.

Review-wide conversations use the same v2 thread/comment schema. They may have an additive title string, but omit filePath, startLine, endLine and anchorText. They participate in the same durable seqCounter, state and reply relationships as inline threads. Older files without title remain valid. Merely opening a draft does not create comments.json; posting its first message creates the review directory/file as needed.

3. comments.json — the v2 schema

{
  "version": 2,                          // serializer ALWAYS writes 2; reader tolerates 1
  "sourceBranch": "feature/mul",         // = compare
  "targetBranch": "main",                // = base
  "sourceCommit": "…",                   // resolved shas (optional)
  "targetCommit": "…",
  "reviewedFiles": ["src/app.js"],       // emitted only when non-empty
  "seqCounter": 4,                       // durable high-water mark; emitted only when present
  "threads": [
    {
      "id": "thr-…",
      "filePath": "src/app.js",          // repo-relative, forward slashes
      "startLine": 5,
      "endLine": 5,
      "state": "unresolved",             // unresolved | resolved
      "seq": 1,                          // 1-based DISPLAY id -> shown as #01
      "tags": ["bug", "question"],
      "comments": [
        { "id": "cmt-…", "body": "why here?", "timestamp": "2026-07-04T…",
          "author": { "kind": "human", "name": "timolee" } },
        { "id": "cmt-…", "body": "Because X.  ~Written by 🤖 Copilot", "timestamp": "2026-07-04T…",
          "replyTo": "cmt-…",            // threading: parent comment id
          "author": { "kind": "agent", "name": "Copilot",
                      "model": "Claude Opus 4.8", "reasoning": "high",
                      "version": "1.0.69",
                      "sessionId": "f7dffc32-…" } }   // agent-only Copilot session UUID
      ]
    }
  ]
}

Type reference

Type Fields
Review version, sourceBranch?, targetBranch?, sourceCommit?, targetCommit?, threads[], reviewedFiles?[], reviewedFileSnapshots?, seqCounter?, sourceFile (in-memory only)
ReviewedFileSnapshot version: 1, left, right; each side is absent or carries kind, hash, mode
ReviewThread id?, filePath?, startLine?, endLine?, state? (unresolved|resolved), seq? (1-based display id), tags[], comments[]
ReviewComment id?, body, timestamp?, replyTo?, author?, tags?[], messageKind?, instruction?
ReviewAuthor kind (human|agent|unknown), name, model?, reasoning?, version?, sessionId? (agent-only)

4. Serialization rules

  • Always writes v2. serializeReview emits version: 2 regardless of what was read.
  • Diff-friendly. 2-space indent + trailing newline.
  • Sparse. reviewedFiles, reviewedFileSnapshots and seqCounter are only written when present/non-empty; author sub-fields (model/reasoning/version/sessionId) only when set.
  • No uuid dependency. Ids come from newId(prefix) = <prefix>-<Date.now(base36)>-<rand6>.

5. v1 back-compat (reading)

parseReview is tolerant so files written by the original extension (or an older Lighthouse) still load:

v1 shape Normalized to
missing version treated as version: 1
author is a bare string (e.g. "timolee") { kind: "unknown", name: "timolee" } via parseAuthor
missing tags []
missing state unresolved
missing seq / seqCounter see durable-seq migration below

6. Durable thread display ids (seq + seqCounter)

Each thread has a 1-based seq rendered as #01, #02, … seqCounter is a monotonic high-water mark stored on the Review; it is never decremented.

  • New thread: addThread does seq = ++seqCounter — it never scans Math.max(threads), so a transiently-empty in-memory review can't reset the counter and reuse a display id.
  • Legacy migration (normalizeSeq): runs once when seqCounter === undefined. It renumbers existing threads 1..N by array order and sets seqCounter = N. When seqCounter is already present it only backfills any missing seq and keeps the counter >= max(seq).

This fixed a real bug where a third thread was labeled #01 (reused) because the counter was derived from a momentarily-empty thread set.

7. Tags

  • Core set (default): idea question bug change todo nit praise (configurable via lighthouse.tags).
  • Entered as /tag tokens in the comment input (see tagCompletion.ts); merged into the thread's tags[] on submit.
  • Classification only: /approved, /todo, quoted prose, and agent replies do not create approval.

Typed messages and asynchronous work

messageKind is additive v2 metadata: discussion, question, decision or approved-change. Its absence means ordinary discussion. New Post messages use discussion for questions, comments and decisions; Approve messages use approved-change and carry an instruction. Older question/decision records remain readable, including pending legacy questions:

{
  "body": "Implement only the error-handling change described in this message.",
  "messageKind": "approved-change",
  "instruction": {
    "status": "pending", // pending | claimed | completed | blocked | cancelled
    "contentHash": "<SHA-256 of the instruction body, type and null proposal reference>"
  }
}

New approvals need no proposal ID or confirmation field: the explicit Approve action authorizes the exact saved body, not the whole thread or whatever the last reply proposed. replyTo remains a conversation link and does not expand approval scope. Discussion/decision records have no work instruction. Hash inputs are defined in reviewInstructions.ts.

Older approvals may include proposal: { commentId, digest }. That reference and its digest remain authoritative for those records; they are not silently migrated into text-only approvals. Changing approved body text invalidates execution. Unrelated discussion edits do not invalidate new approvals, but editing a legacy bound proposal still invalidates its approval. Strict writes reject malformed work metadata rather than silently discarding it; unknown nested fields survive coordinated writes.

A successful claim adds claim: { requestId, author } and sets status: "claimed". Only the matching caller-supplied claim identity/token can report a result. Reporting completion or a blocker appends a reply to the instruction and atomically sets status and resultCommentId. Retries with identical request IDs/payloads replay rather than duplicate. These records do not authenticate a human or prove source edits happened; processing still needs prior user authorization and must obey session permissions. Agent replies cannot mint approvals.

Work status is independent of discussion state. Completion is an agent report, not human acceptance or resolution. Human cancellation retains the original instruction/claim; it cannot undo edits already made. Claims have no automatic timeout/takeover. For blocked or abandoned work, cancel and post a fresh instruction, preserving the earlier conversation.

8. Reviewed-files checklist

reviewedFiles is an array of repo-relative paths the reviewer has checked off in the Changed Files view. Each checked path also has an additive v2 reviewedFileSnapshots entry:

"reviewedFileSnapshots": {
  "src/app.js": {
    "version": 1,                  // fingerprint format, not the review schema version
    "left":  { "kind": "file", "hash": "<Git blob ID>", "mode": "100644" },
    "right": { "kind": "file", "hash": "<64-character SHA-256>", "mode": "100644" }
  }
}

The left side is the file at the effective baseline, not necessarily the target tip. The right side is the saved working file when the source is checked out, or the immutable source file otherwise. kind is file or symlink; a missing endpoint is exactly { "kind": "absent" }, distinct from an empty file. Modes include meaningful executable/link changes. Working bytes are hashed in bounded chunks; immutable source hashes are cached. No file content is stored in a snapshot.

Checking certifies the complete baseline-to-saved-source change even in split mode. Staging, unstaging, and committing identical content do not invalidate it. Saved edits, external changes, or changed baseline content uncheck the affected file. A different commit with identical file endpoints does not. Once invalidated, a check is never restored automatically by undoing an edit.

Unsaved edits are not fingerprinted. They disable the entire applicable checkbox, including manual unchecking; an Unsaved label and tooltip explain how to restore availability. Saving validates the saved bytes, while discarding edits simply restores checkbox availability. Working-tree overlays for a different, non-HEAD source cannot certify that source's content.

On first use, legacy checks without valid snapshots are cleared and the user is notified; comments and thread sequence counters remain intact. Inactive reviews are not bulk migrated. Source saves schedule validation immediately. Checked-path filesystem events use a per-file 100 ms quiet window, capped at 250 ms, while Git events provide metadata hints. Later notifications cannot postpone a runnable save, and changes during validation retain a trailing recheck. Startup/review selection and explicit Refresh reconcile persisted checks, including hidden/deleted paths. Comment-only/UI refreshes do not launch file-hash scans. Changes made and restored before validation, or while inactive, are not an edit history.

Checklist and discussion mutations share a per-review queue and cooperative interprocess lock, reload the latest JSON, and reject observed external edits before atomic replacement. The MCP writer participates in the same protocol. This is not compare-and-swap against arbitrary writers that ignore the lock, including older extension versions or direct manual JSON edits. Unreadable files, failed Git queries, and write errors are reported as unverified/error states, never substituted with empty content. Checking remains a lazy first mutation that creates comments.json; mere rendering does not.

MCP read/reply contract

MCP adds no database or review schema version. Read tools create no review files; content-free diagnostic logs are stored separately in platform user data. get_review returns thread summaries with the first 64 Unicode code points of the first comment, without rendering/trimming the text or appending an ellipsis, plus a truncation flag. get_thread returns full paginated discussions. Legacy ID-less references are explicitly read-only.

reply_to_thread requires an existing uniquely identified thread, agent author metadata, a saved revision, and a caller-generated request ID. The server generates the comment ID/timestamp and preserves thread state, checklist snapshots, per-comment tags, and unrelated raw document fields. Its deterministic c-mcp-<hash> ID encodes the scope/thread/request identity; identical retries return the saved reply, while a changed payload for that request is a conflict. Check replay identity before revision conflicts so a lost successful response can be retried safely.

Mutations preserve v1 authors when upgrading to canonical v2. MCP refuses corrupt/future schemas and ambiguous IDs instead of salvaging them into a writable empty review. The UI's tolerant display parser remains available; it is not a substitute for strict MCP write validation.

Disposable branch-local Git metadata

metadata-cache.json lives beside that comparison's comments.json. It is independent of review content and does not change the v2 review schema:

{
  "version": 1,
  "repository": "<opaque SHA-256 scope key for the normalized repository root>",
  "commits": [
    {
      "commit": "<full resolved commit SHA>",
      "files": [
        { "path": "src/app.js", "entry": { "mode": "100644", "type": "blob", "oid": "<Git object ID>" } },
        { "path": "not-in-this-commit.js", "entry": null }
      ]
    }
  ]
}

The cache stores only immutable Git tree lookup results, including explicit absence at a particular commit. It contains neither source contents, live-file hashes, Git configuration nor reviewed flags. Requests first reuse memory/disk entries, then query Git for missing paths. Failed Git queries never become cached absence. New readers can reuse the disk cache, but still verify saved bytes.

Loading is lazy on the first nonempty metadata request; construction and ordinary pane rendering do not preload it. It retains at most four recently used commit trees and 4 MiB of serialized data. Writes are coalesced asynchronously and replace the cache atomically through a uniquely named temporary file in the same directory; comments.json is never replaced by cache writes.

Missing files are normal cache misses. Invalid, oversized, inaccessible or unwritable cache data is diagnosed and bypassed without altering review checkpoints or blocking live Git/memory results. Concurrent misses may repeat Git lookups; there is no lookup-deduplication layer. Deleting the cache and reloading only makes subsequent lookups cold. Ignore/tracking policy is the workspace owner's responsibility; the extension does not edit ignore files or Git state.

9. Helper functions (reviewModel.ts, vscode-free)

Function Purpose
parseReview(data, sourceFile) tolerant v1/v2 parse → Review
parseAuthor(raw) string | object → ReviewAuthor
normalizeSeq(review) one-time legacy seq/seqCounter migration
sameReviewedSnapshot(a, b) compare both saved endpoints and meaningful metadata
setFileReviewed(review, file, snapshot?) add a certified check, or remove its check and snapshot
clearLegacyReviewedFiles(review) clear uncertifiable legacy checks without changing discussions
newId(prefix) uuid-free id generator
formatTimestamp(iso) friendly local time, e.g. Jul 4, 7:12 PM (falls back to raw)
authorDisplay(author) { name, iconId } — hubot for agents, account for human/unknown (callers wrap iconId with vscode.ThemeIcon)