@vrtmrz/livesync-commonlib 0.1.41 → 0.1.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -1
- package/dist/API/DirectFileManipulatorV2.js +16 -1
- package/dist/API/processSetting.d.ts +1 -0
- package/dist/API/processSetting.js +6 -1
- package/dist/common/fileContentHash.d.ts +2 -0
- package/dist/common/fileContentHash.js +18 -0
- package/dist/common/models/setting.const.defaults.js +1 -0
- package/dist/common/models/setting.const.qr.js +1 -0
- package/dist/common/models/setting.policy.d.ts +4 -0
- package/dist/common/models/setting.policy.js +1 -0
- package/dist/common/models/setting.type.d.ts +8 -1
- package/dist/common/models/shared.definition.configNames.js +5 -0
- package/dist/common/models/tweak.definition.d.ts +6 -0
- package/dist/common/models/tweak.definition.js +1 -0
- package/dist/common/pathIdentity.d.ts +19 -0
- package/dist/common/pathIdentity.js +35 -0
- package/dist/common/pathNormalization.d.ts +7 -0
- package/dist/common/pathNormalization.js +13 -0
- package/dist/common/settingConstants.d.ts +1 -0
- package/dist/diagnostics/couchdb.d.ts +15 -0
- package/dist/diagnostics/couchdb.js +129 -0
- package/dist/diagnostics/index.d.ts +10 -0
- package/dist/diagnostics/index.js +26 -0
- package/dist/diagnostics/integrity.d.ts +5 -0
- package/dist/diagnostics/integrity.js +674 -0
- package/dist/diagnostics/local.d.ts +4 -0
- package/dist/diagnostics/local.js +25 -0
- package/dist/diagnostics/reports.d.ts +108 -0
- package/dist/diagnostics/reports.js +353 -0
- package/dist/diagnostics/types.d.ts +130 -0
- package/dist/diagnostics/types.js +13 -0
- package/dist/diagnostics/usage.d.ts +51 -0
- package/dist/diagnostics/usage.js +241 -0
- package/dist/hashing.d.ts +1 -0
- package/dist/hashing.js +3 -1
- package/dist/interfaces/FileReflectionProvenance.d.ts +4 -0
- package/dist/managers/EntryManager/EntryManager.d.ts +1 -0
- package/dist/managers/EntryManager/EntryManager.js +9 -0
- package/dist/pouchdb/StreamingFetch.d.ts +3 -1
- package/dist/pouchdb/StreamingFetch.js +3 -1
- package/dist/pouchdb/encryption.d.ts +3 -1
- package/dist/pouchdb/encryption.js +38 -30
- package/dist/pouchdb/pathIdentityCompatibility.d.ts +8 -0
- package/dist/pouchdb/pathIdentityCompatibility.js +64 -0
- package/dist/pouchdb/remoteFeatureCompatibility.d.ts +2 -0
- package/dist/pouchdb/remoteFeatureCompatibility.js +9 -1
- package/dist/replication/couchdb/LiveSyncReplicator.js +11 -0
- package/dist/replication/index.d.ts +1 -1
- package/dist/replication/index.js +2 -0
- package/dist/replication/journal/LiveSyncJournalReplicator.d.ts +2 -0
- package/dist/replication/journal/LiveSyncJournalReplicator.js +28 -6
- package/dist/replication/trystero/P2PRoomSessionOwner.js +18 -5
- package/dist/replication/trystero/TrysteroReplicator.d.ts +2 -0
- package/dist/replication/trystero/useP2PReplicatorFeature.js +2 -1
- package/dist/serviceFeatures/offlineScanner.d.ts +12 -3
- package/dist/serviceFeatures/offlineScanner.js +4 -2
- package/dist/serviceFeatures/targetFilter.d.ts +1 -1
- package/dist/serviceFeatures/targetFilter.js +28 -17
- package/dist/serviceModules/Rebuilder.d.ts +5 -1
- package/dist/serviceModules/Rebuilder.js +85 -3
- package/dist/serviceModules/ServiceFileHandlerBase.d.ts +5 -2
- package/dist/serviceModules/ServiceFileHandlerBase.js +110 -49
- package/dist/services/base/IService.d.ts +2 -1
- package/dist/services/base/PathService.d.ts +3 -3
- package/dist/services/base/PathService.js +6 -3
- package/dist/settings.d.ts +2 -0
- package/dist/settings.js +3 -0
- package/dist/worker/bgWorker.js +1 -1
- package/docs/conflict-resolution.md +17 -8
- package/docs/database-diagnostics.md +24 -0
- package/docs/development.md +1 -0
- package/docs/proven-in-use.md +11 -1
- package/docs/unicode-path-identity.md +98 -0
- package/package.json +9 -1
|
@@ -30,17 +30,17 @@ After the conflict is resolved, the deleted losing branch and no-longer-needed m
|
|
|
30
30
|
|
|
31
31
|
Commonlib follows these rules:
|
|
32
32
|
|
|
33
|
-
-
|
|
33
|
+
- never treat a path, size, modification time, or revision generation as proof of content identity;
|
|
34
34
|
- use the nearest `available` revision shared by both leaves as the base for three-way merge;
|
|
35
35
|
- retain a manual conflict when the common revision or a required body is missing or compacted;
|
|
36
|
-
- recognise an unchanged file
|
|
36
|
+
- recognise an unchanged file through its exact readable file-reflection revision or the saved SHA-256 hash and byte length of that snapshot;
|
|
37
37
|
- save a genuine edit as a child of that recorded revision, even when a newer database winner exists;
|
|
38
38
|
- deduplicate unknown-origin content against current non-deleted leaves, preserving differing content as a fresh independent root under the same document ID; and
|
|
39
39
|
- never select the newest modification time as a package-level default.
|
|
40
40
|
|
|
41
41
|
Ordinary saving and incoming reflection share this classification. A historical byte match alone cannot distinguish an unchanged stale file from an intentional revert, and a prefix or suffix relationship does not prove which version the user edited. Unknown-origin content must not acquire an inferred parent from the current winner or its parent.
|
|
42
42
|
|
|
43
|
-
After a resolution has propagated, an exact recorded revision can still identify unchanged content below a deleted losing leaf. The resolved winner may replace those bytes under the existing reflection policy. If the recorded body is
|
|
43
|
+
After a resolution has propagated, an exact recorded revision can still identify unchanged content below a deleted losing leaf. The resolved winner may replace those bytes under the existing reflection policy. If neither the recorded body nor valid matching content evidence is available, the file follows the conservative unknown-origin path. General history-query helpers remain available for callers with separate contracts, including existing deletion and rename recovery; ordinary save and overwrite protection do not scan all historical bodies.
|
|
44
44
|
|
|
45
45
|
## Resolution classes
|
|
46
46
|
|
|
@@ -51,10 +51,11 @@ After a resolution has propagated, an exact recorded revision can still identify
|
|
|
51
51
|
| One side deletes content which the other leaves unchanged | Preserve the deletion. |
|
|
52
52
|
| One side deletes content which the other modifies | Retain a manual conflict. |
|
|
53
53
|
| A receiving file matches its exact readable recorded revision | Apply the propagated result under the existing conflict policy. |
|
|
54
|
+
| A receiving file matches its saved content hash and byte length | Apply the propagated result under the existing conflict policy, even after its recorded revision is pruned. |
|
|
54
55
|
| A receiving file differs from its readable recorded revision | Preserve the edit as a child of that revision. |
|
|
55
56
|
| Origin is unknown and no current non-deleted leaf matches | Preserve a fresh independent root for host conflict resolution. |
|
|
56
57
|
| Origin is unknown and current non-deleted leaves already contain the bytes | Avoid duplicate storage; infer provenance only for a unique match. |
|
|
57
|
-
| A body or common ancestor is missing or compacted
|
|
58
|
+
| A required merge body or common ancestor is missing or compacted | Retain a manual conflict. |
|
|
58
59
|
| Content is binary or otherwise not semantically mergeable | Leave the selection policy to the host; Commonlib cannot infer user intent. |
|
|
59
60
|
|
|
60
61
|
## Stale and concurrent resolutions
|
|
@@ -76,12 +77,20 @@ A host must not apply a dialogue result after either compared revision has cease
|
|
|
76
77
|
The compatibility implementation accepts an injected, device-local `FileReflectionProvenance` capability owned by the database-to-storage composition rather than by a filesystem adapter. Maintained hosts persist:
|
|
77
78
|
|
|
78
79
|
```text
|
|
79
|
-
path -> { revision, observedStorageMtime? }
|
|
80
|
+
path -> { revision, observedStorageMtime?, contentHash?, contentSize? }
|
|
80
81
|
```
|
|
81
82
|
|
|
82
83
|
`revision` is the exact database revision most recently saved from or reflected in this device's storage. It identifies the base for subsequent local edits, rather than certifying that the current file still contains those bytes. `observedStorageMtime` is the raw modification time of the saved snapshot or the file observed after reflection. It is neither rounded nor compared across devices, and it is diagnostic only: revision identity never comes from a timestamp, path, size, or content hash.
|
|
83
84
|
|
|
84
|
-
The record is updated
|
|
85
|
+
The record is updated after a successful database-to-storage reflection or storage-to-database write, or after verifying an exact unchanged revision. A plain read does not change provenance. A record remains authoritative when a user edits the displayed file to bytes which happen to equal another branch; content equality must not silently change the branch being extended.
|
|
86
|
+
|
|
87
|
+
`contentHash` is an algorithm-tagged SHA-256 digest (`sha256:<lowercase hexadecimal>`) of the exact immutable content saved or reflected; `contentSize` is its byte length. Together they let the handler recognise unchanged content after PouchDB prunes the recorded revision. They do not recreate missing revision ancestry or authorise attaching an edited file to an unavailable base. Unsupported or malformed evidence follows the legacy comparison path.
|
|
88
|
+
|
|
89
|
+
Old records remain readable. An equal-time scan can add content evidence when the recorded revision is still the conflict-free current revision and its bytes match storage. An ordinary unchanged save can also upgrade its verified current record. These upgrades revalidate storage and provenance after hashing; an unrelated branch or an unreadable revision is never certified from the current file. A hashing failure leaves legacy revision evidence, so overwrite protection remains conservative.
|
|
90
|
+
|
|
91
|
+
Hashing is serialised across files to bound simultaneous whole-file buffers. The public `computeFileContentHash(Blob)` helper includes reading the Blob and Web Crypto SHA-256; the handler reuses results only for the same immutable Blob. It still allocates a whole-file buffer and Web Crypto may copy it, so serialisation does not imply constant memory use.
|
|
92
|
+
|
|
93
|
+
The [file-reflection content hash decision](file-reflection-content-hash.md) records why this evidence uses SHA-256, the measured comparison with xxHash64, and the conditions for reconsidering that choice.
|
|
85
94
|
|
|
86
95
|
File-handler saves and reflections share a lock for the same Metadata document, held from snapshot acquisition through provenance recording. Different documents can proceed concurrently. An ordinary save keeps one captured body and its recorded base through the database write; a later local edit is processed as a subsequent change, without rereading the file to certify that it remained unchanged. The handler acquires the lock before loading a file body from storage. Rename operations acquire both document locks in a consistent order, and internal calls reuse the caller's lock rather than acquiring it again. Host conflict-check hooks run after the locks are released, allowing a hook to await another file-handler operation on the same document. Host queues may count document-lock waiters against their concurrency limits, so a burst for one document can temporarily delay unrelated files.
|
|
87
96
|
|
|
@@ -91,13 +100,13 @@ User-directed reconciliation may select any exact current live revision, includi
|
|
|
91
100
|
|
|
92
101
|
`storeWithBaseRevision()` is the force-write primitive for deliberately preserving a branch below an exact revision, including a revision which is no longer a leaf. It must not be used when an operation means 'advance this revision only if it is still current'. For that compare-and-store boundary, `storeWithLiveBaseRevision()` uses PouchDB's ordinary revision check and returns `false` if the supplied base has already been advanced. Any current revision-tree leaf is eligible, including a non-winning conflict leaf or a logical-deletion leaf. The caller must refresh its state after refusal rather than silently falling back to the deterministic winner.
|
|
93
102
|
|
|
94
|
-
`storeIndependentRevision()` explicitly preserves unknown-origin content through `putDBEntryAsIndependentRoot()`. Passing an undefined base to `storeWithBaseRevision()` retains its existing latest-revision behaviour and does not create an independent root. `findLiveContentRevisions()` compares only current non-deleted leaves using locally available chunks. Additional provenance comparisons also use local-only retrieval, so they do not introduce remote chunk requests or delivery waits; a missing body leads to preservation rather than an inferred base.
|
|
103
|
+
`storeIndependentRevision()` explicitly preserves unknown-origin content through `putDBEntryAsIndependentRoot()`. Passing an undefined base to `storeWithBaseRevision()` retains its existing latest-revision behaviour and does not create an independent root. `findLiveContentRevisions()` compares only current non-deleted leaves using locally available chunks. Additional provenance comparisons also use local-only retrieval, so they do not introduce remote chunk requests or delivery waits; a missing body without matching saved content evidence leads to preservation rather than an inferred base.
|
|
95
104
|
|
|
96
105
|
A successful non-deleted reconciliation records the exact selected or newly created revision as provenance. Applying a logical deletion removes the storage item and its provenance record. Deletion provenance is not retained indefinitely: an absent storage item and a current logical-deletion winner already agree and need no further reconciliation.
|
|
97
106
|
|
|
98
107
|
Hosts may construct the namespaced store handle during service composition, before its backing database is open. Store operations begin only after the host's storage lifecycle is ready. They fail rather than wait when that lifecycle contract is violated, because an implicit readiness wait could hang after failed initialisation or wait on its own initialisation handler. Database reset is a transient unavailable boundary; the host must avoid file processing during it and reconstruct derived provenance after reopening.
|
|
99
108
|
|
|
100
|
-
For ordinary saving and incoming reflection, an absent or unreadable recorded base permits reconstruction only from exactly one matching current non-deleted leaf. Multiple matches avoid duplicate storage without guessing branch identity. No current match creates a fresh independent root, even when an ancestor has the same bytes. Deletion and rename retain their existing provenance-recovery contracts.
|
|
109
|
+
For ordinary saving and incoming reflection, an absent or unreadable recorded base without matching saved content evidence permits reconstruction only from exactly one matching current non-deleted leaf. Multiple matches avoid duplicate storage without guessing branch identity. No current match creates a fresh independent root, even when an ancestor has the same bytes. Deletion and rename retain their existing provenance-recovery contracts.
|
|
101
110
|
|
|
102
111
|
An unchanged stale file is not saved below the winner. When the database has advanced without a conflict, the existing database-to-storage path reflects it; a conflict instead remains subject to the host's reflection and resolution policy. Independent roots retain the file's modification time and use the existing Metadata/Chunks format, with fresh revision identifiers and reusable chunks. They have no shared ancestor with unrelated existing branches, including after replication. This does not disable a host's explicit newer-file option.
|
|
103
112
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
date: 2026-10-19
|
|
3
|
+
commonlib_version: '0.1.43'
|
|
4
|
+
livesync_version: '1.0.38'
|
|
5
|
+
status: unreleased
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Database diagnostics
|
|
9
|
+
|
|
10
|
+
The focused `/diagnostics` entry supplies read-only database usage analysis and integrity inspection. Hosts choose original names or temporary pseudonyms before each run, provide progress and cancellation controls, and present the resulting reports. Analysis uses the original identifiers; name projection happens afterwards.
|
|
11
|
+
|
|
12
|
+
Usage retains the existing available-revision selection and incremental chunk accounting. The first processed reference counts a chunk as unique; later references count it as shared. Sizes count JavaScript string units, as in the existing report. These columns do not measure physical database size or establish whether a chunk can be deleted.
|
|
13
|
+
|
|
14
|
+
Integrity inspection enumerates actual stored document IDs, including logically deleted normal-file Metadata and current conflict branches. It uses the same identity classifier as the existing Metadata inspector. It reads content only for groups with identity discrepancies, conflicting branches, duplicate path claims, or differences between the inspected local and remote Metadata. Content comparisons use reconstructed bytes, including the Vault file where available. Missing, unreadable, and uninspected content remain distinct outcomes. Equal content within this scope does not establish that an entry is safe to delete.
|
|
15
|
+
|
|
16
|
+
The first implementation supports the local database and CouchDB. Internal-file, customisation, and plug-in storage Metadata, older revision history, and other remote backends are explicitly outside integrity inspection. Usage continues to cover the revisions available to its local source. A remote failure retains the local findings and marks the integrity report partial.
|
|
17
|
+
|
|
18
|
+
The CouchDB reader only sends GET requests. It reads existing synchronisation parameters when decryption needs a Security Seed and never creates missing parameters, initialises a database, or invokes replication admission. Local content reads do not fetch or cache remote Chunks. Owned requests are cancelled and released when the run ends; host-native request adapters may finish an already dispatched request after cancellation, but its result is discarded.
|
|
19
|
+
|
|
20
|
+
Database update sequences and the host's configuration identity are checked again before completion. Storage readers must detect changes while reading a file. These observations can detect concurrent changes; they do not provide an atomic snapshot or prevent a file from changing later.
|
|
21
|
+
|
|
22
|
+
Reports project a whitelist of facts. Pseudonyms preserve exact path equality across sources and keep document, revision, and Chunk identifiers separate. Case and Unicode normalisation facts are computed before projection. Maps are local to one run and are not persisted. Original-name mode still excludes credentials, connection addresses, encryption keys, raw errors, and file contents. JSON and Markdown use the same projected integrity result; TSV uses the same name mapper for Usage.
|
|
23
|
+
|
|
24
|
+
Hosts may pass Commonlib, plug-in, and application versions to either report projector through its optional third argument. Only those three version fields are retained. Integrity reports also retain the five identity-related settings used for the inspection; credentials and the active connection configuration remain outside the report.
|
package/docs/development.md
CHANGED
|
@@ -8,6 +8,7 @@ The source tree is larger than the supported package surface. Consumers may impo
|
|
|
8
8
|
|
|
9
9
|
- `src/index.ts`, `src/context.ts`, `src/settings.ts`, `src/remoteConfigurations.ts`, `src/p2p/index.ts`, `src/platform/browser/index.ts`, and `src/platform/node/index.ts` define the focused entries;
|
|
10
10
|
- `src/replication/index.ts` defines the focused provider-capability and interaction contract for hosts which compose replication;
|
|
11
|
+
- `src/diagnostics/index.ts` defines read-only Usage and integrity inspection, report projection, and formatting; hosts own the run dialogue and output destination;
|
|
11
12
|
- `src/rpc/index.ts` defines a transitional entry for the existing LiveSync P2P composition, not a stable Commonlib 1.0 contract;
|
|
12
13
|
- `_tools/build-package.mjs` compiles those entries and creates the publishable manifest under `.package`;
|
|
13
14
|
- `docs/migration/downstream-imports.json` is the reviewed inventory from which explicit `compat/*` exports are generated;
|
package/docs/proven-in-use.md
CHANGED
|
@@ -8,7 +8,13 @@ The code paths below identify the maintained Self-hosted LiveSync composition po
|
|
|
8
8
|
|
|
9
9
|
## Obsidian plug-in
|
|
10
10
|
|
|
11
|
-
Unreleased
|
|
11
|
+
Unreleased [database diagnostics](database-diagnostics.md) tests cover preserved Usage accounting, normal-file identity classification, current conflict branches, byte comparisons, embedded Chunks, missing and unreadable content, cancellation, concurrent changes, and consistent pseudonyms. Real in-memory PouchDB tests verify pagination and conflict revision reads while retaining the original database handle. A CouchDB 3.5.0 integration test verifies GET-only access, encrypted Metadata and Chunk reads with an existing Security Seed, missing-seed failure without initialisation, and unchanged database update sequences and Seed documents. Focused tests also reject ciphertext which remains undecoded after an ID mismatch. Self-hosted LiveSync owns the serviceFeature, run-time name choice, dialogue lifecycle, and report copy and download controls. Its real Obsidian 1.12.7 test on Linux verifies cancellation before a run, both name modes, the nine-column Usage report, copy and download payloads, local integrity findings when CouchDB is unavailable, and unchanged settings, database update sequences, and Vault file names. Mobile emulation verifies the choice dialogue's viewport, safe-area, and touch-target layout; native iOS and Android behaviour remains unverified.
|
|
12
|
+
|
|
13
|
+
Unreleased content-evidence tests replicate 99, 100, and 599 updates between real in-memory PouchDB databases, including a receiver with `revs_limit: 100`. They verify unchanged reflection after the recorded revision is pruned, including a recreated handler, and preserve same-size edits, intentional reverts, and records without valid content evidence. Separate tests verify legacy upgrades and changes during hashing. These are package-owned tests; they do not establish Windows, mobile, or remote CouchDB behaviour.
|
|
14
|
+
|
|
15
|
+
The exact local packed candidate also passes Self-hosted LiveSync 1.0.37's checks, unit tests, application builds, and CLI E2E. Its focused real-Obsidian `stale-file-restart` scenario prunes the recorded revision before restarting, verifies unchanged reflection and legacy-record upgrades, and preserves an unknown-origin file. The Review Harness exercises the exported hash helper in desktop Obsidian. Maintainer-supplied Android and iOS reports additionally verify the helper and its benchmark on those devices; these timing measurements do not establish complete mobile synchronisation behaviour or peak memory use.
|
|
16
|
+
|
|
17
|
+
Unreleased deletion tests exercise `deleteMetadataOfDeletedFiles` enabled and disabled with two in-memory PouchDB replicas and injected file storage. They verify deletion and rename reflection, preservation of genuine local edits, and restoration before or during a deleted-winner lookup, including the final revision check before deletion. They also verify that missing provenance or a compacted base without content evidence preserves local storage, and that continuously advancing deleted revisions do not return stale Metadata. These tests cover the Commonlib file handler and revision lookup, including the latest-revision check before deletion; they do not exercise Obsidian file watchers or a remote CouchDB server.
|
|
12
18
|
|
|
13
19
|
Unreleased configuration-encryption tests cover changing the wrapper key without changing connection, E2EE, profile, or independent ID values. They also cover custom-key input before loading, failed writes, concurrent saves, cancellation, and unreadable-payload retention. Commonlib owns these service results. LiveSync owns the Apply controls, setting-tab registration, and database startup order; actual Apply and restart behaviour requires its focused real-Obsidian scenario.
|
|
14
20
|
|
|
@@ -88,6 +94,10 @@ The CLI extends the same neutral context with its database path and injected `St
|
|
|
88
94
|
|
|
89
95
|
The CLI filesystem composition receives a configured base directory and constructs the rooted Node storage adapter in `src/apps/cli/adapters/NodeFileSystemAdapter.ts`. Commonlib's shared storage result table checks the focused root contract; the consumer adds its file cache, Vault-shaped operations, case handling, and command policy.
|
|
90
96
|
|
|
97
|
+
The [Unicode path identity contract](unicode-path-identity.md), introduced in Commonlib `0.1.42`, provides opt-in NFC document IDs for CLI and Obsidian use with CouchDB. Commonlib tests exercise real in-memory Metadata lookup across different host normalisers. CLI temporary-filesystem tests cover resolution, updates, deletions, unchanged physical spellings, and ambiguous NFC paths. File-reflection tests preserve NFC-equivalent physical names while retaining case-only rename behaviour. With the packed candidate installed, the Linux CLI-to-real-Obsidian scenario verifies creation, remote updates, and daemon-applied deletions with and without E2EE and Path Obfuscation. A separate run migrates two legacy CLI databases with Rebuild and Fetch before the Obsidian round trip, preserving both NFD physical names and replacing the remote document ID. These checks do not establish native macOS or iOS behaviour.
|
|
98
|
+
|
|
99
|
+
The unreleased reconstruction refinement excludes unconflicted logical deletions only when replacing the local database. Rebuilder tests exercise real PouchDB enumeration before the reset boundary across Rebuild, Standard Fetch, and Fast Fetch. Real CouchDB tests cover deletion propagation, fully tombstoned documents, unresolved conflict leaves, and a stale local copy after a remote deletion. Non-deleted duplicates and unresolved conflicts still stop reconstruction, and remote-only Rebuild retains its ID check. These package-owned tests establish preflight decisions; the maintained downstream migration scenario separately owns complete CLI reconstruction and real-Obsidian Fast Fetch with the exact packed candidate.
|
|
100
|
+
|
|
91
101
|
Unreleased Node timestamp tests verify that `stat` and `trystat` retain an available birth time when change time advances, fall back when birth time is zero, and return integer milliseconds. Packed-package checks exercise the shared `getNodeFileCreationTime` export. With the exact locally packed candidate installed, downstream CLI tests verify that an in-place edit retains creation time in local Metadata. The CLI-to-real-Obsidian scenario additionally verifies the edited content and creation time in received Metadata. These checks do not establish the reporter's ZFS behaviour or physical-file creation times in Obsidian.
|
|
92
102
|
|
|
93
103
|
The maintained CLI checks include unit tests, Deno-driven CLI E2E, Compose-based compatibility runs, and the CLI-to-real-Obsidian synchronisation scenario in `test/e2e-obsidian/scripts/cli-to-obsidian-sync.ts`. The latter is the strongest current evidence that the CLI and plug-in compose compatible Commonlib behaviour across two hosts.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
---
|
|
2
|
+
date: 2026-10-19
|
|
3
|
+
commonlib-version: "0.1.42"
|
|
4
|
+
self-hosted-livesync-version: "1.0.38"
|
|
5
|
+
status: unreleased
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Unicode path identity
|
|
9
|
+
|
|
10
|
+
`pathNormalizationVersion` controls the logical path used to derive a normal-file
|
|
11
|
+
Metadata document ID. An absent value or `0` preserves legacy behaviour. Value
|
|
12
|
+
`1` applies Unicode NFC before the existing case and ID derivation rules. It
|
|
13
|
+
does not alter the Metadata path or require physical files to be renamed.
|
|
14
|
+
Unsupported values are rejected.
|
|
15
|
+
|
|
16
|
+
NFC identity shipped in Commonlib `0.1.42` and Self-hosted LiveSync `1.0.38`.
|
|
17
|
+
The deletion handling below is an unreleased refinement of reconstruction.
|
|
18
|
+
|
|
19
|
+
## Package contract
|
|
20
|
+
|
|
21
|
+
The Commonlib settings entry exposes `PathNormalizationVersion`,
|
|
22
|
+
`PathIdentitySettings`, `getPathNormalizationVersion`, and
|
|
23
|
+
`normalisePathForIdentity`. Consumers must use the shared path identity
|
|
24
|
+
implementation for Metadata IDs rather than add a host-specific normalisation
|
|
25
|
+
rule. Host storage adapters remain responsible for mapping a logical path back
|
|
26
|
+
to its existing physical entry.
|
|
27
|
+
|
|
28
|
+
## Database compatibility
|
|
29
|
+
|
|
30
|
+
Version `1` uses `nfc-path-identity-v1` in the existing generation-13
|
|
31
|
+
`used_features` list. This is separate from `idDerivationVersion`, which selects
|
|
32
|
+
the cryptographic ID key. The NFC setting is a required Tweak even when
|
|
33
|
+
encryption is disabled.
|
|
34
|
+
|
|
35
|
+
A fresh database declares the feature before its first Metadata mutation. An
|
|
36
|
+
existing database without the feature cannot acquire it through an ordinary
|
|
37
|
+
Metadata write or replication while it already contains Metadata. A client
|
|
38
|
+
configured for version `0` refuses an NFC database. These checks also apply
|
|
39
|
+
when ordinary configuration-mismatch warnings are disabled. The existing
|
|
40
|
+
version-document reception hook stops an active replication session when it
|
|
41
|
+
detects incompatible path identity. It does not revoke server credentials or
|
|
42
|
+
atomically cancel work already admitted by another device.
|
|
43
|
+
|
|
44
|
+
Fast Fetch checkpoints include the path identity version, so a checkpoint
|
|
45
|
+
created under version `0` cannot resume under version `1`, or vice versa.
|
|
46
|
+
|
|
47
|
+
## Migration contract
|
|
48
|
+
|
|
49
|
+
1. Update every participating client, stop synchronisation, and retain a
|
|
50
|
+
complete backup.
|
|
51
|
+
2. Select a complete, authoritative Vault. Resolve distinct files or Metadata
|
|
52
|
+
documents which would derive the same NFC document ID. Case-insensitive
|
|
53
|
+
handling also applies its existing case rule.
|
|
54
|
+
3. Set version `1` on the source and Rebuild both databases from that Vault.
|
|
55
|
+
4. Set version `1` on the remaining clients and Fetch the rebuilt remote before
|
|
56
|
+
resuming synchronisation.
|
|
57
|
+
|
|
58
|
+
Reconstruction checks the available storage listing and local Metadata before
|
|
59
|
+
resetting either database. When the local database is not open, the storage
|
|
60
|
+
check conservatively includes every listed file. A collision stops the
|
|
61
|
+
operation; it never chooses a file to keep or merges separate histories.
|
|
62
|
+
Ordinary synchronisation does not rename legacy document IDs in place.
|
|
63
|
+
|
|
64
|
+
Full Rebuild, Standard Fetch, and Fast Fetch replace the local database. Their
|
|
65
|
+
Metadata check requests conflict information and excludes a logically deleted
|
|
66
|
+
document (`deleted: true`) only when it has no unresolved conflict leaves.
|
|
67
|
+
A deleted winner with any unresolved conflict remains a collision candidate.
|
|
68
|
+
Remote-only Rebuild retains logically deleted Metadata and still requires its
|
|
69
|
+
stored document IDs to match the selected identity.
|
|
70
|
+
|
|
71
|
+
A fully tombstoned document is already absent from ordinary Metadata
|
|
72
|
+
enumeration. A deletion made only on the remote does not exclude a still
|
|
73
|
+
non-deleted local document until that deletion has been replicated.
|
|
74
|
+
|
|
75
|
+
The Setup URI shares `pathNormalizationVersion`; an absent value decodes as
|
|
76
|
+
legacy mode, and unsupported versions are rejected. Importing a shared value
|
|
77
|
+
does not itself perform the required reconstruction.
|
|
78
|
+
|
|
79
|
+
## Supported hosts
|
|
80
|
+
|
|
81
|
+
The initial support boundary is the maintained CLI and Obsidian with CouchDB.
|
|
82
|
+
`PathService` owns logical ID derivation. The CLI resolves NFC-equivalent names
|
|
83
|
+
against directory entries while preserving the actual entry spelling; its
|
|
84
|
+
generic Node storage contract remains exact-path access. Obsidian supplies its
|
|
85
|
+
normalised storage paths and preserves an existing physical spelling when it
|
|
86
|
+
is NFC-equivalent to the received path.
|
|
87
|
+
|
|
88
|
+
Journal and P2P refuse version `1`. Direct file access refuses a remote which
|
|
89
|
+
declares the NFC feature. Other host adapters must establish physical path
|
|
90
|
+
resolution before offering this setting. The regression tests cover ASCII,
|
|
91
|
+
NFC, NFD, and mixed paths through in-memory PouchDB Metadata lookup, with and
|
|
92
|
+
without path obfuscation, plus database admission, reconstruction ordering,
|
|
93
|
+
collisions, and physical path handling. These tests do not establish native
|
|
94
|
+
iOS or macOS filesystem behaviour.
|
|
95
|
+
|
|
96
|
+
The full host setting, Rebuild/Fetch, CLI, and Obsidian migration contract is
|
|
97
|
+
in the [Self-hosted LiveSync Unicode path identity design](https://github.com/vrtmrz/obsidian-livesync/blob/main/docs/design_docs/unicode_path_identity.md)
|
|
98
|
+
and its [decision record](https://github.com/vrtmrz/obsidian-livesync/blob/main/docs/adr/2026_10_unicode_path_identity.md).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vrtmrz/livesync-commonlib",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.43",
|
|
4
4
|
"description": "Shared data, storage, and replication primitives for Self-hosted LiveSync clients.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -47,6 +47,11 @@
|
|
|
47
47
|
"import": "./dist/context.js",
|
|
48
48
|
"default": "./dist/context.js"
|
|
49
49
|
},
|
|
50
|
+
"./diagnostics": {
|
|
51
|
+
"types": "./dist/diagnostics/index.d.ts",
|
|
52
|
+
"import": "./dist/diagnostics/index.js",
|
|
53
|
+
"default": "./dist/diagnostics/index.js"
|
|
54
|
+
},
|
|
50
55
|
"./hashing": {
|
|
51
56
|
"types": "./dist/hashing.d.ts",
|
|
52
57
|
"import": "./dist/hashing.js",
|
|
@@ -643,6 +648,9 @@
|
|
|
643
648
|
"context": [
|
|
644
649
|
"dist/context.d.ts"
|
|
645
650
|
],
|
|
651
|
+
"diagnostics": [
|
|
652
|
+
"dist/diagnostics/index.d.ts"
|
|
653
|
+
],
|
|
646
654
|
"hashing": [
|
|
647
655
|
"dist/hashing.d.ts"
|
|
648
656
|
],
|