Persisted formats
This inventory lists every format Deckard writes and later reads back, so that an upgrade in place keeps reading what the previous release wrote. It names each format, where it is written and read, its version marker, its migrations, and the test that pins it. It was taken at dev 1805a01, before Phase 0 of the refactor plan, and every path:line below is 1-based at that commit.
Summary
“Pinned” means a test asserts the stored shape or the literal key. A test that imports a key constant pins behavior, but it keeps passing if the constant’s value changes. Test files named without a folder are in src/test/.
| # | Format | Store | Version marker | Pinned by |
|---|---|---|---|---|
| 1 | Preferences blob, deckard.preferences |
globalState and workspaceState |
version: 1 field, not checked on read |
preferences.test.ts, preferences-prune.test.ts, pinned-notes.test.ts; partly |
| 2 | Handover flag, deckard.preferences.workspaceScoped |
globalState |
none | preferences-prune.test.ts |
| 3 | deckard.lastSeenVersion |
globalState |
none | whats-new.test.ts, by constant |
| 4 | deckard.whatsNewPending |
globalState |
none | whats-new.test.ts, by constant |
| 5 | deckard.openSampleReadme |
globalState |
none | sample-workspace.test.ts, value only |
| 6 | deckard.lastReminderDate |
globalState |
none | task-status-bar.test.ts, by constant |
| 7 | deckard.excludeHintShown |
workspaceState |
none | none yet |
| 8 | deckard.firstIndexSummaryShown |
workspaceState |
none | first-index.test.ts, by constant |
| 9 | deckard.tryNext.retired and deckard.tryNext.snoozed |
workspaceState |
none | none yet |
| 10 | deckard.capture.draft |
workspaceState |
none | capture.test.ts, shape only |
| 11 | Whether any memento key exists (keys()) |
globalState and workspaceState |
none | none yet |
| 12 | MCP server token, deckard.mcpServer.token |
SecretStorage |
none | mcp-server.test.ts, value only |
| 13 | SQLite search cache schema, deckard-search.sqlite |
storageUri |
SCHEMA_VERSION = 3 as PRAGMA user_version |
search-store.test.ts |
| 14 | Parse fingerprint, meta row parse |
SQLite cache | the fingerprint is the marker | search-store.test.ts, warm-start.test.ts; partly |
| 15 | Last scan counts, meta row lastScan |
SQLite cache | none | search-store.test.ts, warm-start.test.ts |
| 16 | Parsed note encoding, notes.parsed |
SQLite cache | FORMAT = 1 as field v |
parsed-file-codec.test.ts; partly |
| 17 | Preference snapshots, preference-snapshots/*.json |
storageUri |
the blob’s version: 1 |
preference-backups.test.ts |
| 18 | Preference export file | a file the reader picks | deckard.version: 1, not checked on read |
preference-backups.test.ts |
| 19 | Sample workspace folder, deckard-sample |
globalStorageUri |
none | sample-workspace.test.ts |
| 20 | Search page state, view type deckard.tagOverview |
webview state | none | search-page-behavior.test.ts, webview-saved-state.test.ts, searchPage.e2e.js |
| 21 | Task Board state, view type deckard.taskBoard |
webview state | none | taskBoard.e2e.js, webview-saved-state.test.ts |
| 22 | Dashboard state, view type deckard.dashboard |
webview state | none | dashboard-behavior.test.ts, webview-saved-state.test.ts |
| 23 | Notes graph state, view type deckard.notesGraph |
webview state | none | notes-graph-behavior.test.ts, parked-views.test.ts, webview-saved-state.test.ts |
| 24 | Calendar page state, view type deckard.calendarPage |
webview state | none | webview-saved-state.test.ts, calendar.test.ts |
| 24a | Help state, view type deckard.help, since Phase 6 step 4.1: { guide?: { page, anchor? }, scrollY, drawn }, read only by the drawing of the page that saved it |
webview state | drawn, a hash of the HTML’s nonce |
help-page.test.ts |
| 24b | Related Notes state, view deckard.relatedNotes, since Phase 6 step 4.6: what the reader chose in it, and its scroll |
webview state | none | webview-saved-state.test.ts |
| 24c | Stats state, view type deckard.stats, since Phase 6 step 4.2: { showAllOrphans, showUsedOnce, pairsAsTable, scrollY? } |
webview state | none | stats.test.ts |
| 25 | Entry ids, tag keys, and Find keys stored inside rows 1, 9, and 17 | inside other formats | none | preferences-prune.test.ts, preferences.test.ts; relative only |
| 26 | Settings Deckard writes | settings.json |
none | exclude-folders.test.ts, settings.test.ts; partly |
| 27 | Tag visits, since plan 37: deckard.tagVisits in workspaceState (each tag’s visit time, by tag key, at most 500, the oldest dropped first), each tag’s snapshot in a file of the workspace’s storage folder, tag-visits/<shortHash of the tag key>.json ({ tag, visit: { seenAt, entries, tasks: { open, done }, fields, answered?, ordinals? } }, written beside it and moved over it; a file whose tag is another’s reads as none; a key ending ~n is the n+1th entry, task, or question of its note with the same words, which a visit without ordinals counts as no change; no folder open, no files), and the Hubs view’s sort, deckard.hubs.sort ('new', or nothing for by name); src/core/storage/tagVisits.ts, read by readTagVisit, which reads any other shape as no visit. The snapshots an earlier build kept in workspaceState as deckard.tagVisit:<tag key> are removed at start (dropStoredSnapshots) |
workspaceState and context.storageUri |
none | since-last-looked.test.ts |
| 28 | Not a promise, since plan 41: deckard.notPromises, a list of <note URI>\u0000<hash of the line's words>, the latest last, at most 500; src/core/storage/promiseDismissals.ts, which reads anything but a list of text as none |
workspaceState |
none | promises.test.ts |
| 29 | Questions, since plan 38: deckard.questions.notIt, a list of <question's note>\u0000<hash of the question's words>\u0000<hash of the line's words> set aside with Not it, the latest last, at most 500; and deckard.questions.missedSearches, Find’s searches that found nothing, by their words, each { count, first, last }, at most 200, read into a map, so any words, __proto__ among them, are a key of their own; src/core/storage/questionMemory.ts, which reads anything else as none |
workspaceState |
none | questions-view.test.ts |
| 30 | Written before, since plan 39: deckard.writtenBefore.notSame, a list of <hash of a line's words>\u0000<hash of the older line's> set aside with Not the same, the latest last, at most 500; src/core/storage/writtenBeforeMemory.ts, which reads anything but a list of text as none |
workspaceState |
none | written-before-editor.test.ts |
Deckard calls no setKeysForSync, so no memento key travels with Settings Sync. Deckard uses no logUri. The Deckard output channel (src/extension.ts:214) writes a log that Deckard never reads back.
Out of scope: note syntax
Task metadata, front matter, block ids (^id), query blocks, and every other syntax Deckard writes into notes are the user’s own content. The parser tests cover how they are read, so this inventory leaves them out. Files Deckard writes and never reads back, such as an exported result list (src/ui/commands/exportResults.ts:164), are left out too.
1. Preferences blob
- Key:
deckard.preferences, constant atsrc/core/storage/preferences.ts:34. - Written:
persistatsrc/core/storage/preferences.ts:1177. It writes the whole blob toglobalState(:1178) and the workspace’s share toworkspaceState(:1179). The store is built atsrc/extension.ts:284. With no folder open, it gets noworkspaceStateand reads and writes the machine-wide blob alone. - Read: the constructor,
src/core/storage/preferences.ts:244(globalState) and:250(workspaceState). It lays the workspace share over the machine-wide blob with the workspace keys removed (:265). - Shape:
PersistedPreferencesatsrc/core/types.ts:377. The workspace’s share is the 18 keys inworkspacePreferenceKeys(src/core/storage/preferences.ts:60). Everything else is presentation and stays machine-wide. - Version marker:
version: 1(src/core/types.ts:378), written bynormalizePreferencesatsrc/core/storage/preferences.ts:1206. The store never reads it.readExportreads it to accept a bare snapshot (row 17). - Normalization: every read passes through
normalizePreferences(:1189). It rebuilds the object from known keys, so any key it does not name is dropped on the next write.
| Migration | Where it runs | What it converts | Pinned by |
|---|---|---|---|
| Machine-wide to per-workspace (1.19) | constructor :255, initialize :278 |
The first workspace opened with no blob of its own adopts the workspace keys of the machine-wide blob, then sets the handover flag (row 2) | preferences-prune.test.ts:172, :194, :209, :239 |
| Rendered becomes the default | normalizePreferences :1244 |
A stored renderMode: 'markdown' without renderModeChosen: true reads as 'html' |
preferences.test.ts:99 |
| Dashboard tabs removed | normalizeDashboardViewState :1538 |
Any dashboardViewState.mode other than 'browse', such as 'tasks' or 'notes', reads as 'home'. The other old view-state fields are dropped |
preferences.test.ts:234, :267 |
| Retired keys | normalizePreferences :1189 |
dashboardTaskLayout, dashboardNoteSortMode, and any other unnamed key are dropped |
preferences.test.ts:234, :267 |
| Home widgets added | normalizePreferences :1299 |
A blob with no dashboardWidgets array gets DEFAULT_DASHBOARD_WIDGETS (:153) |
preferences.test.ts:267 |
| Pins by path | normalizePinnedNotes :1569 |
A string pin reads as { filePath } |
pinned-notes.test.ts:109 |
| Ids widened (1.23) | prune :973, carryLegacyIds :1773 |
taskOrder, sectionAccessCounts, and sectionAccessTimes keyed by an old one-hash id move to the new id whose first half it is (legacyIdOf, src/core/markdown/parser.ts:1880) |
preferences-prune.test.ts:25 |
| First-seen times | prune :1000 |
A blob with no tagFirstSeen marks every indexed tag as 0, meaning known before times were kept |
preferences.test.ts:358 |
| Saved filters | normalizeSavedFilters :1647 |
Drops filters with no name, fewer than two tags and no query, or a duplicate id or tag set | preferences.test.ts:467 |
Phase 6 changed no stored shape and no migration. Since step 4.7, normalizeDashboardWidgets (src/core/storage/preferencesSchema.ts) recognizes a stored widget’s kind with isWidgetKind over WIDGET_KINDS in src/domain/dashboard/widgetCatalog.ts, the catalog the Dashboard page reads too, in place of DASHBOARD_WIDGET_KINDS; the kinds and the traits it reads are the same, and widget-catalog.test.ts pins them.
preferences-invariants.test.ts walks random operations over the store with in-memory mementos. It pins what operations keep and remove, and that one workspace never reaches another (:228). It does not pin a stored shape.
Pinned since Phase 3: src/test/preferences-roundtrip.test.ts loads a complete blob, with every field set to a non-default value, split across globalState and workspaceState, and asserts what it normalizes to and the exact bytes written back to each store. It also loads each legacy shape the migrations read (the 1.19 handover, renderMode without renderModeChosen, the removed Dashboard tabs, retired keys, string pins and headings, one-hash ids from before 1.23, a missing tagFirstSeen) and walks every mutator, recording each write’s store, key, order, and digest. The blob is made up rather than captured from a release, so a field 1.23.1 writes that the type does not declare would not be caught.
2. Handover flag
- Key:
deckard.preferences.workspaceScoped, constant atsrc/core/storage/preferences.ts:44. Boolean. - Written:
src/core/storage/preferences.ts:287, once, after the first workspace adopts the machine-wide blob. - Read:
src/core/storage/preferences.ts:257. While it is unset, a workspace with no blob of its own is seeded from the machine-wide one. - Version marker: none. Migrations: none.
- Pinned by:
preferences-prune.test.ts:239, which asserts the literal key stays unset in a window with no folder.:194pins that a second workspace is not seeded.
The plan names “its workspace-scoped twin”. The twin is the workspaceState copy of deckard.preferences (row 1). This flag is a separate key.
3. Last version seen
- Key:
deckard.lastSeenVersion,LAST_SEEN_VERSIONatsrc/ui/commands/whatsNew.ts:23. A version string such as1.23.1. - Written:
src/ui/commands/whatsNew.ts:68(new install) and:81(every activation). - Read:
src/ui/commands/whatsNew.ts:65. - Version marker: none.
- Migration: an existing user with no stored value is read as coming from
FIRST_TRACKED_FROM = '1.22.0'(:26, applied at:71). - Pinned by:
whats-new.test.ts:58asserts the stored value, and:66pins the migration. Both use the constant, so the literal key is not pinned. A pinning test would assert the literaldeckard.lastSeenVersion.
4. What’s new pending
- Key:
deckard.whatsNewPending,WHATS_NEW_PENDINGatsrc/ui/commands/whatsNew.ts:24. Shape{ from: string; to: string }(:28). - Written:
src/ui/commands/whatsNew.ts:77; cleared at:103. - Read:
src/ui/commands/whatsNew.ts:76,:86,:95,:100. - Version marker: none. Migrations: none.
- Pinned by:
whats-new.test.ts:87asserts{ from: '1.22.0', to: '1.24.0' }, by constant. A pinning test would assert the literal key.
5. Sample README to show
- Key:
deckard.openSampleReadme,SAMPLE_README_KEYatsrc/ui/commands/sampleWorkspace.ts:19. The sample folder’s URI as a string. - Written:
src/ui/commands/sampleWorkspace.ts:215; cleared at:163before the README opens. - Read:
src/ui/commands/sampleWorkspace.ts:156, compared with each open folder’s URI string intakeSampleReadme(:145). - Version marker: none. Migrations: none.
- Pinned by:
sample-workspace.test.ts:354pins the comparison. No test uses the key.
6. Last reminder date
- Key:
deckard.lastReminderDate,REMINDER_DATE_KEYatsrc/ui/views/taskStatusBar.ts:110. The local day asYYYY-MM-DD(localDate,:101). It lives inglobalState(src/extension.ts:499), so every window shares it. - Written:
src/ui/views/taskStatusBar.ts:241, before the reminder is shown. - Read:
src/ui/views/taskStatusBar.ts:237. - Version marker: none. Migrations: none.
- Pinned by:
task-status-bar.test.ts:134asserts'2026-09-19'under the constant. A pinning test would assert the literal key.
The plan’s list of seven memento keys does not include this one.
7. Exclude hint shown
- Key:
deckard.excludeHintShown,EXCLUDE_HINT_SHOWNatsrc/extension.ts:185, private to that file. Boolean. - Written:
src/extension.ts:353, and bysummarizeFirstIndexthrough itsexcludeHintShownKeyoption (src/ui/commands/firstIndex.ts:108, passed atsrc/extension.ts:343). - Read:
src/extension.ts:349. - Version marker: none. Migrations: none.
- Pinned by: none yet.
first-index.test.ts:65passes the key'hint'. A pinning test would assert that the first index of a large workspace writes the literaldeckard.excludeHintShown, and that a storedtruesuppresses the hint.
8. First index summary shown
- Key:
deckard.firstIndexSummaryShown,FIRST_INDEX_SUMMARY_SHOWNatsrc/ui/commands/firstIndex.ts:16. Boolean. - Written:
src/ui/commands/firstIndex.ts:96, before the message. - Read:
src/ui/commands/firstIndex.ts:92. - Version marker: none. Migrations: none.
- Pinned by:
first-index.test.ts:65, by constant. A pinning test would assert the literal key.
9. Try next ledger
- Keys:
deckard.tryNext.retired(TRY_NEXT_RETIRED,src/ui/commands/tryNext.ts:22), astring[]of suggestion keys;deckard.tryNext.snoozed(TRY_NEXT_SNOOZED,:23), aRecord<string, number>of epoch milliseconds until which each key is put off. Both live inworkspaceState(src/extension.ts:207). - Suggestion keys:
weeklyReview,taskBoard,pinNote, andmergeLookalike:<sourceKey>|<targetKey>(src/ui/state/tryNext.ts:58,:68,:76,:84). The last embeds two tag keys (row 25). - Written:
src/ui/commands/tryNext.ts:47and:53. Callers aresrc/extension.ts:962,:1026,:1058, andsrc/ui/webview/dashboard.ts:399,:403,:417. - Read:
src/ui/commands/tryNext.ts:33and:37. - Version marker: none. Migrations: none.
- Pinned by: none yet.
try-next.test.ts:59testschooseTryNextwith sets and records built in the test, not the ledger. A pinning test would store the literal keys with values in the 1.23.1 shape and assert thatTryNextLedgerreads a retired key as retired and a snooze as active until its time.
The plan’s list of seven memento keys does not include these two.
10. Capture draft
- Key:
deckard.capture.draft,DRAFT_KEYatsrc/ui/commands/capture.ts:52. Shape{ text: string; target: 'today' | 'heading'; literal: boolean }(:46,CaptureTargetat:18). It lives inworkspaceState(src/extension.ts:501). - Written:
src/ui/commands/capture.ts:73; cleared at:77. - Read:
src/ui/commands/capture.ts:63. A draft with blank text or another target is ignored. - Version marker: none. Migrations: none.
- Pinned by:
capture.test.ts:48round-trips the shape through a map, but does not use the key. A pinning test would store a literal draft underdeckard.capture.draftand assertreadreturns it.
The plan’s list of seven memento keys does not include this one.
11. Whether Deckard ran before
- Where:
src/extension.ts:191and:193, at the top ofactivate. - What it reads:
workspaceState.keys().length === 0means a workspace new to Deckard, which gates the first index summary.globalState.keys().length > 0 || workspaceState.keys().length > 0means an existing user, which decides whether What’s new treats a missingdeckard.lastSeenVersionas an update from 1.22. - Version marker: none. Migrations: none.
- Pinned by: none yet.
whats-new.test.tspassesexistingUserdirectly. A pinning test would activate with empty mementos and with one holding onlydeckard.preferences, and assert both flags. It would also fail if any write moved ahead of these two reads.
12. MCP server token
- Key:
deckard.mcpServer.token,TOKEN_KEYatsrc/ui/commands/mcpServer.ts:42, incontext.secrets(src/extension.ts:405). 64 hex characters. - Written:
src/ui/commands/mcpServer.ts:151(first use) and:158(reset). - Read:
src/ui/commands/mcpServer.ts:146. - Why it matters: the token is copied into each client’s setup (
getClaudeCodeSetup,:56). A new key name after an upgrade mints a new token and breaks every configured client. - Version marker: none. Migrations: none.
- Pinned by:
mcp-server.test.ts:157pins that a new token retires the old one. It does not assert the key. A pinning test would assert that a token stored under the literal key is the one the server accepts.
The plan’s list does not include this store.
13. SQLite search cache schema
- File:
deckard-search.sqliteinstorageUri(src/core/storage/searchStore.ts:111), opened withjournal_mode = WAL, so-waland-shmfiles sit beside it. With nostorageUrithe cache is:memory:and nothing persists. The store is built atsrc/extension.ts:273. The worker opens the same file (src/core/storage/searchStoreWorker.ts:52). - Tables:
notes,entries, the FTS5 tableentries_fts, the vocabulary tableentries_vocab, andmeta(src/core/storage/searchDatabase.ts:61to:90). - Version marker:
SCHEMA_VERSION = 3(src/core/storage/searchDatabase.ts:20), stored asPRAGMA user_version. - Migration: any other
user_versiondropsentries_vocab,entries_fts,entries,notes_fts, andnotes, then sets the version (src/core/storage/searchDatabase.ts:45to:53). The next scan refills it. Themetatable is not dropped. - Pinned by:
search-store.test.ts:208builds an older layout by hand and asserts it is replaced.:232pins that unchanged notes stay searchable across sessions. - Gap: no test opens a database file written at
SCHEMA_VERSION = 3by the release. A pinning test would keep such a file, or the SQL that builds it, and assert the current code opens it without dropping it.
14. Parse fingerprint
- Where: the
metarow with keyparse. Read atsrc/core/storage/searchDatabase.ts:242, written at:249. - Built from:
getParseFingerprint(src/core/workspace/scanner.ts:256) joins, per workspace folder,PARSE_FORMAT('code-and-links',src/core/markdown/parser.ts:28), the folder URI,deckard.noteBoundaries,deckard.parseInlineTags,deckard.personMarker, anddeckard.entityNamespaceAliases.cacheFingerprint(src/core/workspace/indexer.ts:507) appends the extension version and the time zone. - What invalidates the cache: a different fingerprint.
readParsedNotesthen reads nothing (src/core/storage/searchStore.ts:193), so the start is cold.replacethen rebuilds the text index and stores the new fingerprint (src/core/storage/searchStore.ts:142to:148). A drifted text index also rebuilds (hasDrifted,src/core/storage/searchDatabase.ts:259). - Consequence: the extension version is in the fingerprint, so every release starts cold once and never reads a parsed note written by another version. A warm start happens only in
ExtensionMode.Production(src/extension.ts:278). - Pinned by:
warm-start.test.ts:158pins that a new version starts cold.search-store.test.ts:109pins that a changed fingerprint rebuilds, using strings made up in the test. - Gap: no test pins what goes into the fingerprint. A pinning test would assert that changing each of the four settings,
PARSE_FORMAT, or the time zone changescacheFingerprint.
15. Last scan counts
- Where: the
metarow with keylastScan, JSON{ found, templates, excluded, read }. Written atsrc/core/storage/searchStore.ts:239(called fromsrc/core/workspace/indexer.ts:466), read at:221, and only after a warm start matched the fingerprint (src/core/workspace/indexer.ts:205). - Version marker: none. Missing fields read as
0. - Pinned by:
search-store.test.ts:358round-trips it.warm-start.test.ts:94reads it back across a session.
16. Parsed note encoding
- Where: the
notes.parsedcolumn. Encoded byencodeParsedFile(src/core/storage/parsedFileCodec.ts:45), called fromcreateNoteToWrite(src/core/storage/searchDatabase.ts:386). Decoded bydecodeParsedFile(src/core/storage/parsedFileCodec.ts:100), called fromreadParsedNotes(src/core/storage/searchStore.ts:206). - Shape:
{ v, f, fm, s, sm, t, tm }(src/core/storage/parsedFileCodec.ts:31). A section’s text is stored as a line range when the lines give it back exactly, afilePathequal to the note’s is left out, and keys whose value isundefinedare listed so they come back. - Version marker:
FORMAT = 1(src/core/storage/parsedFileCodec.ts:19), written asvand checked at:103. The plan says the codec has no version constant. It has this one. - Guards: the
vcheck and a shape check throw (:102to:112).readParsedNotesskips a row that throws or whosefilePathdoes not match (src/core/storage/searchStore.ts:204to:213). Above that, the fingerprint (row 14) keeps any row written by another version from being read. - Pinned by:
parsed-file-codec.test.ts:20pins the round trip, and:53pins that{"v":99}is refused. - Gap: no test holds encoded text written by the release. A pinning test would keep one encoded note literal and assert it decodes to the same
ParsedFile.
17. Preference snapshots
- Where:
storageUri/preference-snapshots/(src/core/storage/preferenceSnapshots.ts:44), created atsrc/extension.ts:293. Nothing is written with nostorageUri. - File name: the write time as
2026-09-22T19-43-14-277Z.json(nameFromDate,:131;dateFromName,:135); a second copy written in the same millisecond is2026-09-22T19-43-14-277Z-1.json, then-2(freeName, since the fix after Phase 7). A name that does not parse is ignored when listing (:68). The newestSNAPSHOTS_KEPT = 20(:18) are kept. - Body: the bare preferences blob, pretty-printed (
:113). - Written:
src/core/storage/preferenceSnapshots.ts:105, two seconds after each change. - Read:
list(:52) andread(:73), thenreadExport(src/ui/commands/preferenceBackups.ts:164), which accepts a bare blob only whenversion === 1andfavoriteTagsis an array (:78).importPreferencesnormalizes it (src/core/storage/preferences.ts:1169). - Version marker: the blob’s
version: 1. It is the only place that field is checked. - Pinned by:
preference-backups.test.ts:47pins the name format,:54and:72the writing and rotation,:95the bare-blob read and{ version: 2 }being refused.
18. Preference export file
- Where: a JSON file the reader chooses,
deckard-preferences.jsonby default. - Shape:
{ deckard: { kind: 'preferences', version: 1, exportedAt }, preferences }(PreferenceExport,src/ui/commands/preferenceBackups.ts:30). - Written:
createExport(:44) andexportPreferences(:95). - Read:
readExport(:58) fromimportPreferences(:111). It requiresdeckard.kind === 'preferences'and apreferencesrecord. - Version marker:
deckard.version: 1(:33,:49).readExportdoes not check it. - Migrations: none of its own. The blob is normalized on import (row 1).
- Pinned by:
preference-backups.test.ts:95round-trips an export and refuses junk.:116pins that an import is normalized. - Gap: the export is built by today’s code in the test. A pinning test would hold an export literal written by the release.
19. Sample workspace
- Where:
globalStorageUri/deckard-sample(SAMPLE_FOLDER_NAME,src/ui/commands/sampleWorkspace.ts:17).getSampleStorageUri(:45) turns avscode-userdata:URI into afile:URI. - Written:
installSample(:97) copiesresources/sample, resolving date tokens and renamingdot-vscodeto.vscode(:83). It refuses to merge onto an existing folder and replaces it only when asked (:105). - Read back: the folder is opened as a workspace. Its URI is compared at
src/extension.ts:334and:341to tell the sample apart for the first index summary, and againstdeckard.openSampleReadme(row 5). - Version marker: none. A sample made by an older release stays as it was until the reader replaces it.
- Pinned by:
sample-workspace.test.ts:346(replace, never merge),:354(README only in its folder, using the literaldeckard-sample),:364(thefile:scheme).
Webview state
This section is as of the end of Phase 6, when every page became a bundle under src/webview/<page>/; its paths name functions rather than lines.
VS Code keeps what a page passes to setState and hands it back when it restores the panel after a reload, through the serializers registerSerializers registers in src/composition/services.ts. A page restored after an upgrade receives state written by the old script. Seven panel view types are serialized: deckard.dashboard, deckard.stats, deckard.help, deckard.notesGraph, deckard.calendarPage, deckard.taskBoard, and deckard.tagOverview; only the last two hand the state to the host’s restore. The sidebar views deckard.relatedNotes and deckard.calendar are webview views (registerViews, the same file); VS Code hands a view’s state back to the page it loads again, though a view has no serializer. The Related Notes debug panel has no serializer and runs no script.
Since decision 0017, which replaced 0006, a page not kept running while hidden is loaded again each time it is shown, so what the reader chose in it must come back from setState. Each page’s controller under src/ui/webview/pages/ sets retainContextWhenHidden:
| Page | Kept running while hidden | What it keeps with setState |
Row |
|---|---|---|---|
| Dashboard | yes | its view: mode, columns, tag search, namespace filter, arranging, hint | 22 |
| Search page | yes | its search, origin, tab, and scroll | 20 |
| Task Board | yes | its search and scroll | 21 |
| Notes Graph | yes | its 23 settings and its camera | 23 |
| Stats | no | three choices and its scroll | 24c |
| Help | no | the guide page shown and its scroll | 24a |
| Calendar page | no | its layout and scroll | 24 |
| Calendar view | no | nothing | |
| Related Notes | no | the reader’s choices and its scroll | 24b |
| Related Notes debug page | no | nothing; it runs no script |
A page reads what it kept through keptState, src/webview/shared/vscode.ts, which reads anything but a record as nothing kept, and writes through keepState, which merges a change over what was kept; the search page, the Task Board, and the Notes Graph read getState() themselves, as their template scripts did, and Help keeps its own record. The shared scroll helpers rememberScroll and restoreScroll (src/webview/shared/scroll.ts) merge scrollY into a page’s state at most every 200 ms as it scrolls, and scroll back to a number on the first draw. A query editor’s getState option (src/webview/shared/queryEditor.tsx) is a host-state callback, not the VS Code API.
The page harness now seeds kept state: openWebviewPage(html, state, { savedState }) (src/test/webviewPage.ts) loads a page with it and savedState() reads back what the page kept, so the tests below pin both what a page writes and what it reads back. They load the page’s bundle, not the host; the host’s reads are pinned by the e2e suites.
20. Search page
- Shape:
{ query: string; origin: string; tab?: 'notes' | 'tasks'; scrollY?: number }. - Written:
saveStateinsrc/webview/searchPage/main.tsx, after each state the host sends, with thescrollYkept before only while the search is the same, andtabonly once the reader chose it;rememberScrollas the page scrolls. Since Phase 6 step 4.5 the page is a Preact page, and the shape is unchanged. - Read by the page:
tabwhen its script starts (savedPageStateinmain.tsx), andscrollYon its first state (restoreScroll). - Read by the host:
readSerializedSearch(src/ui/webview/searchPage.ts), called fromSearchPanels.restore. - Migration: a page saved before search pages kept one string is read from
tagKey,filterTagKeys, andrefinementorquery(readSerializedSearch). A page whose tag no longer exists is closed. - Pinned by:
search-page-behavior.test.tsasserts the written{ query, origin }.searchPage.e2e.jsrestores a page from each saved shape, current and legacy, throughreadSerializedSearch, and asserts the search it reopens on or that it closes.webview-saved-state.test.tsasserts the page’s reads oftabandscrollY, that any other value it was left with is not read, and that it keeps where it was scrolled.
21. Task Board
- Shape:
{ query: string; scrollY?: number; tasksViewMode?: true }. - Written:
receiveState,src/webview/taskBoard/main.tsx(since Phase 6 step 4.4; before,src/ui/webview/taskBoardHtml.ts), the search of each snapshot the host sends, with thescrollYkept before only while the search is the same, andtasksViewMode: truewhile the snapshot says the board edits what the Tasks view lists (since 1.24);rememberScrollas the page scrolls. - Read by the page:
scrollYon its first snapshot (restoreScroll). Nothing else it was left with is read or kept. - Read by the host:
query, when it is a string, andtasksViewMode, the Tasks view mode only when it istrue, inrestoreSearch(src/ui/webview/pages/taskBoard/taskBoardController.ts), called fromTaskBoardPanel.restore. A state without it, as every release before 1.24 wrote, opens a plain board. - Pinned by:
taskBoard.e2e.jsrestores the board with{ query: 'is:open #project/atlas' }and asserts the query it shows, and with shapes it ignores, which open onis:open, and with{ query, tasksViewMode: true }, which opens in the mode and keeps it;task-board-host.test.tsholds the mode totruealone.webview-saved-state.test.tsasserts the scroll kept only for its search, that anything else it was left with is not kept, and that it keeps where it was scrolled.
22. Dashboard
- Shape:
{ dashboardMode, tagColumns, browseQuery, tagNamespaceFilter, editingHome, homeHintDismissed }. - Written:
keepView,src/webview/dashboard/keptView.ts(since Phase 6 step 4.7; before,saveDashboardViewStatein the template script ofsrc/ui/webview/dashboardHtml.ts), the whole record each time the reader changes one of its fields. - Read by the page:
readKeptView,src/webview/dashboard/keptView.ts.dashboardModeis kept only as'browse', andtagColumnsonly as 1 to 4; a kept value that is not a record reads as nothing kept.browseQueryis written but never read back. The host’srestoreignores the state. - Pinned by:
dashboard-behavior.test.tsassertshomeHintDismissedis written.webview-saved-state.test.tsloads the page with each field set, with values it does not know, and with kept values that are not a record, and asserts the mode, columns, namespace filter, arranging state, and hint it draws, the record it writes back, andbrowseQuerywritten as the tag search is typed.
23. Notes graph
- Shape: the 23 keys of
defaults(DEFAULT_SETTINGS,src/webview/notesGraph/settings.ts), such asshowNotes,selectedTags,group,headings, andlinkDistance, pluscamera: { x, y, k }. Unchanged by Phase 6 step 4.8, which moved the page from the template script insrc/ui/webview/notesGraphHtml.ts; the page is still kept running while hidden (0017), so nothing else is kept. - Written:
persist,src/webview/notesGraph/settings.ts: every setting, in the order of the defaults, then the camera, replacing what was kept before. A key an older page kept that is not a setting is not written back. - Read by the page:
readKept,readSettings, andreadCamera,src/webview/notesGraph/settings.ts. Any saved value other thanundefinedis taken as is, with no type check, and a saved state that is not a record reads as nothing kept. The camera is taken as it was saved when its zoom reads as a finite number (isFinite, so'2'andnullpass), else the view at the origin, unzoomed. The host’srestore(NotesGraphPanel.restore,src/ui/webview/notesGraph.ts) ignores the state. - Pinned by:
notes-graph-behavior.test.tsandparked-views.test.tsassert keys the page writes.webview-saved-state.test.tsloads the page with every key and the camera, asserts each control and the zoom, and asserts every key comes back as given, of whatever type; and, since step 4.8, that a state that is not a record reads as the defaults, that empty and null values are taken as given, which cameras are taken, and which keys are written, in what order. Each was checked against the template page before it moved.
24. Calendar page
- Shape:
{ layout: 'month' | 'week' }; since Phase 6 step 4.3,{ layout?: 'month' | 'week', scrollY?: number }, as the page is no longer kept running while hidden. - Written:
setLayout(throughkeepState) andrememberScroll,src/webview/calendarPage/main.tsx;scrollYat most every 200 ms as the page scrolls. - Read by the page:
keptLayoutandrestoreScrollon the first draw,src/webview/calendarPage/main.tsx. Anything but'week'reads as'month', as before, and ascrollYthat is not a number is not read. The host’srestore(src/ui/webview/calendarPage.ts) ignores the state. - Pinned by:
webview-saved-state.test.tsloads the page with{ layout: 'week' }and asserts the week layout, and with other values, which read as the month;calendar.test.tsasserts what the page keeps.
The sidebar calendar (deckard.calendar) keeps nothing: its own bundle, src/webview/calendar/main.tsx, since Phase 6 step 4.3, never calls setState, and its month is in its HTML.
24a. Help
- Shape: since Phase 6 step 4.1,
{ guide?: { page: string; anchor?: string }; scrollY: number; drawn: string }.drawnis a hash of the script’s nonce, so state saved by an earlier drawing of the page is not read: the host draws each HTML with a new nonce, and VS Code loads the same HTML again when it shows a hidden page. - Written:
save,src/webview/help/main.ts, when a guide page is shown or closed, when the host reveals a section, and once scrolling pauses (saveSoon). - Read by the page:
readState, the same file, on load: only a record whosedrawnmatches and whosescrollYis a number, and itsguideonly with a stringpage. The page asks the host for that guide page again and scrolls back. The host reads none of it. - Pinned by:
help-page.test.tsasserts that a page shown again comes back to the guide page it showed and where it was scrolled, and that a page drawn anew, such as for a theme, opens where it is drawn, not where it was.
24b. Related Notes
- Shape: since Phase 6 step 4.6, as the view is no longer kept running while hidden:
{ noteLimit: number; noteListKey: string; showEveryActiveTag: boolean; contextOpen: boolean; linksOpen: { linked: boolean; mentions: boolean; sameType: boolean }; openLinkSections: string[]; expandedRefine: string[]; shownGroups: string[]; scrollY?: number }.noteListKeynames the listnoteLimitcounts for, asJSON.stringify([state, activeFileName, activeEntryTitle, relatedNotesSortMode, hideDailyNotes, Boolean(similar)]); a link row is namedfilePath:line. - Written:
keepChoices,src/webview/sidebarNotes/main.tsx, after each draw and each fold of the context, a Links group, or the other rows of the note’s type, merged over what was kept;scrollYbyrememberScrollat most every 200 ms as the view scrolls. - Read by the page:
readChoices,src/webview/sidebarNotes/model.ts, when its script starts, andrestoreScrollon its first draw. A value of the wrong kind reads as a new view’s choice:noteLimitonly as a finite number over 50, the flags only as booleans (linksOpen.linkedand, since plan 32,linksOpen.sameTypetrue unless kept false), the lists only their strings. The host reads none of it. - Pinned by:
webview-saved-state.test.tsloads the view with every choice, with a list it did not count for, with Refine and the calendar’s day, and with values of the wrong kind, and asserts what it draws and what it keeps.
24c. Stats
- Shape: since Phase 6 step 4.2, as the page is no longer kept running while hidden:
{ showAllOrphans: boolean; showUsedOnce: boolean; pairsAsTable: boolean; scrollY?: number }. Before, Stats kept nothing. - Written:
keepState, fromchooseinsrc/webview/stats/main.tsx, each time the reader shows the rest of the unlinked notes, the tags used once, or the pairs as a table;scrollYbyrememberScroll. Nothing is kept until the reader chooses or scrolls. - Read by the page:
keptChoices, the same file, when its script starts: each flag is on only when it was kept astrue; andrestoreScrollon its first snapshot. The host’srestoreignores the state. - Pinned by:
stats.test.ts(“Stats: what the reader chose, kept across a hide”) asserts what the page keeps as the reader chooses and scrolls, what it draws when loaded with it, and that values of another kind read as nothing chosen.
25. Keys stored inside other formats
Several formats store Deckard’s own identifiers. If a refactor changes how one is made, the stored value stops matching and is pruned or ignored without an error.
| Identifier | Made at | Stored in | Pinned by |
|---|---|---|---|
Entry ids, <prefix>-<hash>-<hash> |
createId, src/core/markdown/parser.ts:1862; inputs at :1246, :1375, :1450, :1848 |
taskOrder, sectionAccessCounts, sectionAccessTimes in row 1 |
preferences-prune.test.ts:25, relative to the parser’s own output only |
Tag keys, such as #project/atlas |
the parser | favoriteTags, tagAccessCounts, savedFilters, tagFirstSeen, and more in row 1; mergeLookalike keys in row 9 |
the parser tests |
Find choice keys, note:[filePath,heading,occurrence], task:[filePath,label], tag:<key>, view:<id> |
findChoiceKey, src/ui/state/quickFindState.ts:474; pinKey, src/core/storage/preferences.ts:1613 |
findChoices in row 1; read by findChoiceFilePath (:1341) |
preferences.test.ts:83 |
No test pins an entry id’s value. A pinning test would assert createId output for a few fixed inputs, and the id of a heading, a tagged line, and a task in a fixed note. It would catch any change to the text that goes into an id, such as the task-line regex merge of §6 changing match[4].
26. Settings Deckard writes
Deckard writes these settings through writeSetting (src/ui/commands/settings.ts:19) or configuration.update, and reads them back. The package.json inventory owns the setting names. This row is about the value shapes Deckard itself writes.
| Setting | Value Deckard writes | Written at |
|---|---|---|
deckard.exclude |
Record<string, boolean> of escaped folder globs (withExcludeKey, src/ui/commands/excludeFolders.ts:96) |
src/ui/commands/excludeFolders.ts:172, :179, :203; src/ui/commands/parking.ts:408 |
deckard.parked.folders |
the same map shape | src/ui/commands/parking.ts:417, :425, :486 |
deckard.parked.tags |
string[] |
src/ui/commands/parking.ts:547, :558, :616 |
deckard.board.statuses, deckard.board.statusNamespace |
string[], string |
src/ui/commands/taskBoardActions.ts:70 |
deckard.agenda.query |
string, or cleared |
src/extension.ts:856; src/ui/webview/taskBoard.ts:331, :337 |
deckard.agenda.groupBy, deckard.agenda.groupNamespace |
string |
src/ui/views/agendaTree.ts:704, :707, :714 |
deckard.calendar.dayPanel, .showWeekends, .showRepeats |
boolean |
src/extension.ts:829 to :845; src/ui/webview/calendarPage.ts:204, :209 |
deckard.zenMode |
boolean, per folder when set per folder |
src/ui/webview/zenMode.ts:71, :77 |
deckard.outline.followCursor |
boolean |
src/ui/views/outlineTree.ts:397 |
deckard.tagOverview.includeHubLinks |
false |
src/ui/webview/searchPage.ts:780 |
deckard.theme |
string |
src/ui/commands/chooseTheme.ts:64 |
deckard.mcpServer.enabled |
true |
src/ui/commands/mcpServer.ts:176 |
deckard.taskReminderTime |
cleared | src/ui/views/taskStatusBar.ts:332 |
- Pinned by:
exclude-folders.test.ts:24,:33,:46pin the exclude map and its keys.settings.test.ts:24pins that a write goes through. The other shapes are none yet. A pinning test would read each setting as the release writes it and assert Deckard reads the same choice back.
VS Code also stores state under Deckard’s identifiers: open panels by view type, view placement by view id, keybindings by command id, and walkthrough progress by deckard.gettingStarted. The package.json inventory of §1.8 covers them.
Differences from the plan’s list
- The plan’s seven memento keys miss five:
deckard.lastReminderDate,deckard.tryNext.retired,deckard.tryNext.snoozed,deckard.capture.draft, and the handover flagdeckard.preferences.workspaceScopedif “twin” means theworkspaceStatecopy. The full set is 11 key names in 12 key and scope pairs. - The plan misses the
SecretStoragetoken, themetarowsparseandlastScan, thekeys()checks at activation, the identifiers stored inside the preferences blob, and the settings Deckard writes. parsedFileCodechas a version constant,FORMAT = 1(src/core/storage/parsedFileCodec.ts:19).- The four settings in the fingerprint are
noteBoundaries,parseInlineTags,personMarker, andentityNamespaceAliases. The folder URI is in it too. src/test/fixtures/legacyWorkspaceIndex.tspins the in-memory index build against the old one (index-equivalence.test.ts). It pins no persisted format.
Existing compatibility risks
- A parse setting outside the fingerprint.
deckard.tasks.assigneeFromPersonTagis a parse option (src/core/workspace/scanner.ts:450) that decides a task’s assignee (src/core/markdown/parser.ts:923). It is not ingetParseFingerprint(src/core/workspace/scanner.ts:256) and does not trigger a reindex (src/core/workspace/indexer.ts:545to:588). After it changes, unchanged notes keep the old assignee in the index and in the cache until each note is edited or the workspace is reindexed. This is current behavior, so the refactor keeps it; fixing it is a behavior change for a later plan. - The
metatable survives a schema bump. The drop list atsrc/core/storage/searchDatabase.ts:46leavesmetaout, andCREATE TABLE IF NOT EXISTS metadoes not change an existing table. A change tometa’s columns needs its own migration. - Same-version builds read the release’s cache. The fingerprint uses the version string from package.json. A VSIX built from a refactor branch without a version bump and installed over the release reads the release’s parsed notes on a warm start. Only
PARSE_FORMATand the codec’svguard that case, so a phase that changes a parsed note’s shape bumpsPARSE_FORMAT. - The notes graph trusts its saved state. It copies every saved key without a type check (
readSettings,src/webview/notesGraph/settings.ts), as its template script did. Renaming a key or changing a value’s type lets an old value through to the new script. - Two version fields nobody checks. The store never reads the blob’s
version(row 1), andreadExportnever reads the export’sdeckard.version(row 18). The only check isversion === 1for a bare snapshot (src/ui/commands/preferenceBackups.ts:78), so changing or removing the blob’sversionmakes new snapshots unrestorable. - New-install detection depends on write order.
src/extension.ts:191and:193must run before any memento write. A service that moves ahead of them and writes a key at construction turns every new install into an “existing user”. - Unpinned literals. Only
deckard.preferencesanddeckard.preferences.workspaceScopedare asserted as literal strings in tests. Every other key is referenced by constant or not at all, so renaming a constant’s value passes the suite and orphans the stored data.
How to regenerate
Run these from the repository root and check each hit against the rows above.
- Memento access:
grep -rn "globalState\|workspaceState\|Memento\|setKeysForSync" src --include='*.ts' | grep -v '^src/test' - Reads and writes through any memento-like object:
grep -rnE "(state|memory|memento|globalState|workspaceState)\s*\.\s*(get|update|keys)" src --include='*.ts' | grep -v '^src/test' - Key constants:
grep -rn "= 'deckard\.[a-zA-Z.]*'" src --include='*.ts' | grep -v '^src/test' - Secret storage:
grep -rn "secrets" src --include='*.ts' | grep -v '^src/test' - Storage locations:
grep -rnE "globalStorage|storageUri|logUri|StoragePath|storagePath" src | grep -v '^src/test' - Files written:
grep -rn "fs.writeFile\|writeFileSync\|fs.createDirectory\|appendFile" src --include='*.ts' | grep -v '^src/test' - Cache schema and fingerprint:
grep -rn "SCHEMA_VERSION\|user_version\|PARSE_FORMAT\|ParseFingerprint\|cacheFingerprint\|readMeta\|writeMeta" src --include='*.ts' - Codec:
grep -n "FORMAT\|v:" src/core/storage/parsedFileCodec.ts - Parse options against the fingerprint:
grep -n "getParseOptions" -A 35 src/core/workspace/scanner.ts - Preference migrations:
grep -n "legacy\|Legacy\|version\|normalize" src/core/storage/preferences.ts - Webview state:
grep -rn "getState\|setState\|keepState\|keptState" src/webview,grep -rn "retainContextWhenHidden" src/ui/webview/pages src/composition, andgrep -rn "registerWebviewPanelSerializer\|registerWebviewViewProvider" src/composition/services.ts - Settings written:
grep -rn "writeSetting(\|configuration.update\|\.update('[a-zA-Z.]*'," src --include='*.ts' | grep -v '^src/test' - Tests per format:
grep -rln "<key or function>" src/testfor each key, constant, and function named above, then a search for the literal key string to tell a literal pin from a constant.