@vrtmrz/livesync-commonlib 0.1.42 → 0.1.44

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.
@@ -0,0 +1,4 @@
1
+ import type { EntryDoc } from "../common/types.js";
2
+ import type { DiagnosticDatabaseSource } from "./types.js";
3
+ /** Wrap an existing local handle without taking ownership or using chunk caches. */
4
+ export declare function createLocalDiagnosticSource(database: Pick<PouchDB.Database<EntryDoc>, "info" | "allDocs" | "get">): DiagnosticDatabaseSource;
@@ -0,0 +1,25 @@
1
+ function createLocalDiagnosticSource(database) {
2
+ return {
3
+ info: () => database.info(),
4
+ get: (id, options) => database.get(id, options),
5
+ async *enumerateIds() {
6
+ let previous;
7
+ while (true) {
8
+ const page = await database.allDocs({
9
+ limit: 256,
10
+ ...previous === void 0 ? {} : { startkey: previous, skip: 1 }
11
+ });
12
+ for (const row of page.rows) {
13
+ if (!row.value.deleted) yield row.id;
14
+ }
15
+ if (page.rows.length < 256) return;
16
+ const last = page.rows.at(-1).id;
17
+ if (last === previous) throw new Error("Document enumeration did not advance");
18
+ previous = last;
19
+ }
20
+ }
21
+ };
22
+ }
23
+ export {
24
+ createLocalDiagnosticSource
25
+ };
@@ -0,0 +1,16 @@
1
+ import type { DiagnosticNameMode } from "./types.js";
2
+ export interface DiagnosticNameMapper {
3
+ readonly mode: DiagnosticNameMode;
4
+ readonly salt?: string;
5
+ path(value: string): string;
6
+ id(value: string): string;
7
+ revision(value: string): string;
8
+ chunk(value: string): string;
9
+ }
10
+ /** Reads only a complete version-one path token, never a document or revision alias. */
11
+ export declare function parseDiagnosticPathToken(value: string): {
12
+ salt: string;
13
+ hash: string;
14
+ } | undefined;
15
+ /** Reuses public report salts without storing paths or a correspondence table outside this mapper. */
16
+ export declare function createDiagnosticNameMapper(mode: DiagnosticNameMode, suppliedSalt?: string): DiagnosticNameMapper;
@@ -0,0 +1,50 @@
1
+ import { Sha256 } from "@aws-crypto/sha256-js";
2
+ const SALT_PATTERN = /^[A-Za-z0-9_-]{8}$/;
3
+ function parseDiagnosticPathToken(value) {
4
+ const match = /^([A-Za-z0-9_-]{8})\$([0-9a-f]{32})$/.exec(value);
5
+ return match ? { salt: match[1], hash: match[2] } : void 0;
6
+ }
7
+ function randomSalt() {
8
+ const bytes = globalThis.crypto.getRandomValues(new Uint8Array(6));
9
+ return btoa(String.fromCharCode(...bytes)).replaceAll("+", "-").replaceAll("/", "_");
10
+ }
11
+ function nameHash(salt, domain, value) {
12
+ const hash = new Sha256();
13
+ hash.update(JSON.stringify(["livesync-diagnostic-v1", salt, domain, value]));
14
+ return Array.from(hash.digestSync().subarray(0, 16), (byte) => byte.toString(16).padStart(2, "0")).join("");
15
+ }
16
+ function createDiagnosticNameMapper(mode, suppliedSalt) {
17
+ if (mode === "original") {
18
+ const original = (value) => value;
19
+ return { mode, path: original, id: original, revision: original, chunk: original };
20
+ }
21
+ const salt = suppliedSalt ?? randomSalt();
22
+ if (!SALT_PATTERN.test(salt)) throw new Error("Invalid diagnostic salt");
23
+ const domainMapper = (domain, format) => {
24
+ const aliases = /* @__PURE__ */ new Map();
25
+ return (value) => {
26
+ const existing = aliases.get(value);
27
+ if (existing !== void 0) return existing;
28
+ const alias = format(nameHash(salt, domain, value), value);
29
+ if (aliases.size >= 4096) aliases.clear();
30
+ aliases.set(value, alias);
31
+ return alias;
32
+ };
33
+ };
34
+ return {
35
+ mode,
36
+ salt,
37
+ path: domainMapper("path", (hash) => `${salt}$${hash}`),
38
+ id: domainMapper("document", (hash) => `document-${hash}`),
39
+ revision: domainMapper("revision", (hash, value) => {
40
+ const generation = /^([1-9][0-9]*)-.+$/.exec(value)?.[1];
41
+ const prefix = generation !== void 0 && Number.isSafeInteger(Number(generation)) ? `${generation}-` : "";
42
+ return `${prefix}psu${hash}`;
43
+ }),
44
+ chunk: domainMapper("chunk", (hash) => `chunk-${hash}`)
45
+ };
46
+ }
47
+ export {
48
+ createDiagnosticNameMapper,
49
+ parseDiagnosticPathToken
50
+ };
@@ -0,0 +1,111 @@
1
+ import type { DatabaseIntegrityResult, DiagnosticCoverage, DiagnosticError, DiagnosticIdentitySettings, DiagnosticNameMode, DiagnosticObservation, DiagnosticPathEquivalenceGroup, DiagnosticSourceName, DiagnosticStatus } from "./types.js";
2
+ import type { DatabaseUsageError, DatabaseUsageResult, DatabaseUsageStatus } from "./usage.js";
3
+ import type { DiagnosticNameMapper } from "./names.js";
4
+ export { createDiagnosticNameMapper, parseDiagnosticPathToken } from "./names.js";
5
+ export type { DiagnosticNameMapper } from "./names.js";
6
+ export interface DiagnosticReportVersions {
7
+ commonlib?: string;
8
+ plugin?: string;
9
+ application?: string;
10
+ }
11
+ export interface ProjectedDiagnosticError {
12
+ source: DiagnosticError["source"];
13
+ reason: DiagnosticError["reason"];
14
+ id?: string;
15
+ revision?: string;
16
+ path?: string;
17
+ chunkId?: string;
18
+ }
19
+ export interface ProjectedDiagnosticObservation {
20
+ reference: string;
21
+ source: DiagnosticSourceName;
22
+ id: string;
23
+ revision?: string;
24
+ path: string;
25
+ expectedId?: string;
26
+ identity: DiagnosticObservation["identity"];
27
+ logicallyDeleted: boolean;
28
+ branch: DiagnosticObservation["branch"];
29
+ conflictCount: number;
30
+ pathIsNFC: boolean;
31
+ caseEquivalentPaths: boolean;
32
+ nfcEquivalentPaths: boolean;
33
+ caseEquivalentGroup?: string;
34
+ nfcEquivalentGroup?: string;
35
+ metadata?: DiagnosticObservation["metadata"];
36
+ otherSourceGroup: DiagnosticObservation["otherSourceGroup"];
37
+ vault: {
38
+ reference: string;
39
+ status: DiagnosticObservation["vault"]["status"];
40
+ reason?: DiagnosticObservation["vault"]["reason"];
41
+ size?: number;
42
+ contentSizeDifference?: number;
43
+ mtimeDifferenceMs?: number;
44
+ };
45
+ content: {
46
+ status: DiagnosticObservation["content"]["status"];
47
+ reason?: DiagnosticObservation["content"]["reason"];
48
+ chunksRead: number;
49
+ unavailableChunks: {
50
+ id: string;
51
+ status: "missing" | "unreadable";
52
+ }[];
53
+ size?: number;
54
+ recordedSizeDifference?: number;
55
+ };
56
+ comparisons: {
57
+ against: string;
58
+ relation: DiagnosticObservation["comparisons"][number]["relation"];
59
+ }[];
60
+ }
61
+ export interface ProjectedDatabaseIntegrityReport {
62
+ schemaVersion: 1;
63
+ kind: "integrity";
64
+ status: DiagnosticStatus;
65
+ nameMode: DiagnosticNameMode;
66
+ versions: DiagnosticReportVersions;
67
+ settings: DiagnosticIdentitySettings;
68
+ coverage: Record<DiagnosticSourceName, DiagnosticCoverage>;
69
+ observations: ProjectedDiagnosticObservation[];
70
+ pathEquivalenceGroups?: DiagnosticPathEquivalenceGroup[];
71
+ errors: ProjectedDiagnosticError[];
72
+ scope: DatabaseIntegrityResult["scope"];
73
+ }
74
+ export interface ProjectedDatabaseUsageRow {
75
+ title: string;
76
+ id: string;
77
+ path: string;
78
+ revision: string;
79
+ rev: string;
80
+ revHash: string;
81
+ uniqueChunkCount: number;
82
+ sharedChunkCount: number;
83
+ uniqueChunkSize: number;
84
+ sharedChunkSize: number;
85
+ }
86
+ export interface ProjectedDatabaseUsageError {
87
+ code: DatabaseUsageError["code"];
88
+ id?: string;
89
+ revision?: string;
90
+ }
91
+ export interface ProjectedDatabaseUsageReport {
92
+ version: 1;
93
+ kind: "usage";
94
+ status: DatabaseUsageStatus;
95
+ nameMode: DiagnosticNameMode;
96
+ versions: DiagnosticReportVersions;
97
+ changedDuringRun?: boolean;
98
+ rows: ProjectedDatabaseUsageRow[];
99
+ errors: ProjectedDatabaseUsageError[];
100
+ }
101
+ export type ProjectedDiagnosticReport = ProjectedDatabaseIntegrityReport | ProjectedDatabaseUsageReport;
102
+ /** Copies only the documented report fields and projects every stored name through the run mapper. */
103
+ export declare function projectIntegrityReport(result: DatabaseIntegrityResult, mapper: DiagnosticNameMapper, versions?: DiagnosticReportVersions): ProjectedDatabaseIntegrityReport;
104
+ /** Projects Usage rows and errors without carrying through extra analyser fields. */
105
+ export declare function projectUsageReport(result: DatabaseUsageResult, mapper: DiagnosticNameMapper, versions?: DiagnosticReportVersions): ProjectedDatabaseUsageReport;
106
+ /** Formats the legacy nine-column Usage export from an already projected report. */
107
+ export declare function formatUsageTSV(projected: ProjectedDatabaseUsageReport): string;
108
+ /** Serialises a projected report. Raw analyser values never enter this formatter. */
109
+ export declare function formatDiagnosticJSON(projected: ProjectedDiagnosticReport): string;
110
+ /** Renders the same projected integrity fields used by JSON, with user-controlled values in escaped code spans. */
111
+ export declare function formatIntegrityMarkdown(projected: ProjectedDatabaseIntegrityReport): string;