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.
<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_GLOBin reviewStore.ts). Deliberately not the original extension's.vscode/local-reviews/— the two coexist. - Folder name:
<compare>_<base>wherecompare= SOURCE branch andbase= TARGET branch, each run throughsanitizeBranch(every/→-). Example: comparefeature/mul, basemain→feature-mul_main/. - Activation events:
onStartupFinishedandworkspaceContains:.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.
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.
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.
| 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.
| 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) |
- Always writes v2.
serializeReviewemitsversion: 2regardless of what was read. - Diff-friendly. 2-space indent + trailing newline.
- Sparse.
reviewedFiles,reviewedFileSnapshotsandseqCounterare only written when present/non-empty;authorsub-fields (model/reasoning/version/sessionId) only when set. - No uuid dependency. Ids come from
newId(prefix)=<prefix>-<Date.now(base36)>-<rand6>.
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 |
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:
addThreaddoesseq = ++seqCounter— it never scansMath.max(threads), so a transiently-empty in-memory review can't reset the counter and reuse a display id. - Legacy migration (
normalizeSeq): runs once whenseqCounter === undefined. It renumbers existing threads1..Nby array order and setsseqCounter = N. WhenseqCounteris already present it only backfills any missingseqand 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.
- Core set (default):
idea question bug change todo nit praise(configurable vialighthouse.tags). - Entered as
/tagtokens in the comment input (seetagCompletion.ts); merged into the thread'stags[]on submit. - Classification only:
/approved,/todo, quoted prose, and agent replies do not create approval.
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.
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 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.
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.
| 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) |
{ "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 ] } ] }