@erdemtuna/doc-review 0.8.0 → 0.9.0
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 +155 -19
- package/package.json +4 -2
- package/src/SKILL.md +56 -7
- package/src/atomic-write.js +53 -0
- package/src/chrome-client.js +844 -103
- package/src/chrome.css +253 -8
- package/src/chrome.html +78 -11
- package/src/cli.js +52 -130
- package/src/comment-target.js +87 -0
- package/src/comparison-view.js +337 -0
- package/src/document-execution.js +103 -0
- package/src/document-trust.js +2 -0
- package/src/edit-limits.js +23 -0
- package/src/execution-client.js +63 -0
- package/src/frame-policy.js +38 -0
- package/src/history-client.js +104 -0
- package/src/history-coordinator.js +186 -0
- package/src/history-policy.js +43 -0
- package/src/history-server.js +463 -0
- package/src/paths.js +1 -1
- package/src/poll-transport.js +222 -0
- package/src/review-mode.js +1 -1
- package/src/revision-diff.js +440 -0
- package/src/revision-schema.js +219 -0
- package/src/revision-store.js +177 -0
- package/src/sdk.js +419 -55
- package/src/semantic-snapshot.js +230 -0
- package/src/server.js +240 -53
- package/src/setup-guidance.js +128 -0
- package/src/setup.js +41 -15
- package/src/state.js +257 -35
- package/src/view-identity.js +99 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { atomicWrite } from "./atomic-write.js";
|
|
5
|
+
import { stateDir } from "./paths.js";
|
|
6
|
+
import { normalizeView } from "./view-identity.js";
|
|
7
|
+
import {
|
|
8
|
+
REVISION_SCHEMA_VERSION, REVISION_LIMITS, isRevisionId, normalizeCaptureProvenance,
|
|
9
|
+
normalizeSemanticSnapshot, normalizeRevisionLimits, revisionError, utf8Bytes,
|
|
10
|
+
} from "./revision-schema.js";
|
|
11
|
+
|
|
12
|
+
const digest = (text) => crypto.createHash("sha256").update(text).digest("hex");
|
|
13
|
+
const BLOB_ID = /^[a-f0-9]{64}$/;
|
|
14
|
+
|
|
15
|
+
function captureTime(value, now) {
|
|
16
|
+
const time = value ?? now;
|
|
17
|
+
if (!Number.isSafeInteger(time) || time < 0) throw revisionError("Invalid capture timestamp.");
|
|
18
|
+
return time;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class RevisionStore {
|
|
22
|
+
constructor({ root = path.join(stateDir(), "history"), write = atomicWrite, limits = REVISION_LIMITS } = {}) {
|
|
23
|
+
this.root = root;
|
|
24
|
+
this.write = write;
|
|
25
|
+
this.limits = normalizeRevisionLimits(limits);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
blobPath(id) {
|
|
29
|
+
if (!BLOB_ID.test(id)) throw revisionError("Invalid blob ID.");
|
|
30
|
+
return path.join(this.root, "blobs", `${id}.json`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
manifestPath(id) {
|
|
34
|
+
if (!isRevisionId(id)) throw revisionError("Invalid revision ID.");
|
|
35
|
+
return path.join(this.root, "revisions", `${id}.json`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
put({ documentId, source, semantic, reason = "observation", limitations = [] }) {
|
|
39
|
+
if (typeof documentId !== "string" || !documentId || documentId.length > 200) throw revisionError("Invalid document ID.");
|
|
40
|
+
if (typeof reason !== "string" || reason.length > 100) throw revisionError("Invalid capture reason.");
|
|
41
|
+
if (!Array.isArray(limitations) || limitations.length > 100 || limitations.some((item) => typeof item !== "string" || item.length > 500)) {
|
|
42
|
+
throw revisionError("Invalid capture limitations.");
|
|
43
|
+
}
|
|
44
|
+
if (!source && !semantic) throw revisionError("A revision must contain source or semantic content.");
|
|
45
|
+
const now = Date.now();
|
|
46
|
+
const manifest = { version: REVISION_SCHEMA_VERSION, documentId, reason, limitations: [...limitations] };
|
|
47
|
+
const blobs = [];
|
|
48
|
+
let size = 0;
|
|
49
|
+
if (source) {
|
|
50
|
+
if (typeof source.text !== "string") throw revisionError("Invalid source snapshot.");
|
|
51
|
+
const bytes = utf8Bytes(source.text);
|
|
52
|
+
if (bytes > this.limits.sourceBytes) throw revisionError("Source snapshot exceeds the size limit.", "SNAPSHOT_TOO_LARGE");
|
|
53
|
+
const mediaType = source.mediaType ?? "text/plain";
|
|
54
|
+
if (typeof mediaType !== "string" || mediaType.length > 100) throw revisionError("Invalid source media type.");
|
|
55
|
+
const serialized = JSON.stringify({ text: source.text });
|
|
56
|
+
const blobId = digest(serialized);
|
|
57
|
+
manifest.source = {
|
|
58
|
+
blobId, hash: digest(source.text), bytes, mediaType,
|
|
59
|
+
capturedAt: captureTime(source.capturedAt, now),
|
|
60
|
+
provenance: normalizeCaptureProvenance(source.provenance),
|
|
61
|
+
};
|
|
62
|
+
blobs.push([blobId, serialized]);
|
|
63
|
+
size += bytes;
|
|
64
|
+
}
|
|
65
|
+
if (semantic) {
|
|
66
|
+
const snapshot = normalizeSemanticSnapshot(semantic.snapshot, this.limits);
|
|
67
|
+
const serialized = JSON.stringify(snapshot);
|
|
68
|
+
const bytes = utf8Bytes(serialized);
|
|
69
|
+
const blobId = digest(serialized);
|
|
70
|
+
manifest.semantic = {
|
|
71
|
+
blobId, hash: blobId, bytes, schemaVersion: REVISION_SCHEMA_VERSION,
|
|
72
|
+
capturedAt: captureTime(semantic.capturedAt, now),
|
|
73
|
+
provenance: normalizeCaptureProvenance(semantic.provenance),
|
|
74
|
+
...(semantic.view !== undefined ? { view: normalizeView(semantic.view) } : {}),
|
|
75
|
+
};
|
|
76
|
+
blobs.push([blobId, serialized]);
|
|
77
|
+
size += bytes;
|
|
78
|
+
}
|
|
79
|
+
if (size > this.limits.totalBytes) throw revisionError("Combined snapshot exceeds the size limit.", "SNAPSHOT_TOO_LARGE");
|
|
80
|
+
const serialized = JSON.stringify(manifest);
|
|
81
|
+
const revisionId = `v_${digest(serialized)}`;
|
|
82
|
+
for (const dir of ["blobs", "revisions"]) fs.mkdirSync(path.join(this.root, dir), { recursive: true, mode: 0o700 });
|
|
83
|
+
for (const [blobId, content] of blobs) this.writeImmutable(this.blobPath(blobId), content);
|
|
84
|
+
this.writeImmutable(this.manifestPath(revisionId), serialized);
|
|
85
|
+
return { revisionId, ...manifest };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
writeImmutable(file, content) {
|
|
89
|
+
try {
|
|
90
|
+
const existing = fs.readFileSync(file, "utf8");
|
|
91
|
+
if (existing !== content) throw revisionError("Immutable snapshot content disagrees with its ID.", "SNAPSHOT_CORRUPT");
|
|
92
|
+
} catch (err) {
|
|
93
|
+
if (err.code !== "ENOENT") throw err;
|
|
94
|
+
this.write(file, content);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
get(revisionId) {
|
|
99
|
+
let raw;
|
|
100
|
+
try {
|
|
101
|
+
raw = fs.readFileSync(this.manifestPath(revisionId), "utf8");
|
|
102
|
+
} catch (err) {
|
|
103
|
+
if (err.code === "ENOENT") return null;
|
|
104
|
+
throw err;
|
|
105
|
+
}
|
|
106
|
+
if (`v_${digest(raw)}` !== revisionId) throw revisionError("Revision manifest is corrupt.", "SNAPSHOT_CORRUPT");
|
|
107
|
+
const manifest = JSON.parse(raw);
|
|
108
|
+
if (manifest.version !== REVISION_SCHEMA_VERSION) throw revisionError("Unsupported stored revision schema.");
|
|
109
|
+
return { revisionId, ...manifest };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
readBlob(blobId) {
|
|
113
|
+
const raw = fs.readFileSync(this.blobPath(blobId), "utf8");
|
|
114
|
+
if (digest(raw) !== blobId) throw revisionError("Revision blob is corrupt.", "SNAPSHOT_CORRUPT");
|
|
115
|
+
return JSON.parse(raw);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
readSource(revisionId) {
|
|
119
|
+
const manifest = this.get(revisionId);
|
|
120
|
+
return manifest?.source ? this.readBlob(manifest.source.blobId).text : null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
readSemantic(revisionId) {
|
|
124
|
+
const manifest = this.get(revisionId);
|
|
125
|
+
return manifest?.semantic ? this.readBlob(manifest.semantic.blobId) : null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
read(revisionId) {
|
|
129
|
+
const manifest = this.get(revisionId);
|
|
130
|
+
if (!manifest) return null;
|
|
131
|
+
return {
|
|
132
|
+
manifest,
|
|
133
|
+
source: manifest.source ? this.readBlob(manifest.source.blobId).text : null,
|
|
134
|
+
semantic: manifest.semantic ? this.readBlob(manifest.semantic.blobId) : null,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
verify(revisionId, documentId) {
|
|
139
|
+
const manifest = this.get(revisionId);
|
|
140
|
+
if (!manifest || manifest.documentId !== documentId) throw revisionError("Revision does not belong to this document.");
|
|
141
|
+
if (manifest.source) this.readBlob(manifest.source.blobId);
|
|
142
|
+
if (manifest.semantic) this.readBlob(manifest.semantic.blobId);
|
|
143
|
+
return manifest;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// A grace period protects blobs written before their state transaction commits.
|
|
147
|
+
collectGarbage(referencedIds, { olderThan = Date.now() - 60 * 60 * 1000 } = {}) {
|
|
148
|
+
const keep = new Set(referencedIds);
|
|
149
|
+
const blobs = new Set();
|
|
150
|
+
const manifestsDir = path.join(this.root, "revisions");
|
|
151
|
+
const blobsDir = path.join(this.root, "blobs");
|
|
152
|
+
const removed = { revisions: 0, blobs: 0 };
|
|
153
|
+
const files = (dir) => fs.existsSync(dir) ? fs.readdirSync(dir) : [];
|
|
154
|
+
for (const name of files(manifestsDir)) {
|
|
155
|
+
const id = name.replace(/\.json$/, "");
|
|
156
|
+
if (!isRevisionId(id)) continue;
|
|
157
|
+
const file = this.manifestPath(id);
|
|
158
|
+
if (!keep.has(id) && fs.statSync(file).mtimeMs < olderThan) {
|
|
159
|
+
fs.unlinkSync(file);
|
|
160
|
+
removed.revisions += 1;
|
|
161
|
+
} else {
|
|
162
|
+
const manifest = this.get(id);
|
|
163
|
+
if (manifest.source) blobs.add(manifest.source.blobId);
|
|
164
|
+
if (manifest.semantic) blobs.add(manifest.semantic.blobId);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
for (const name of files(blobsDir)) {
|
|
168
|
+
const id = name.replace(/\.json$/, "");
|
|
169
|
+
if (!BLOB_ID.test(id) || blobs.has(id)) continue;
|
|
170
|
+
const file = this.blobPath(id);
|
|
171
|
+
if (fs.statSync(file).mtimeMs >= olderThan) continue;
|
|
172
|
+
fs.unlinkSync(file);
|
|
173
|
+
removed.blobs += 1;
|
|
174
|
+
}
|
|
175
|
+
return removed;
|
|
176
|
+
}
|
|
177
|
+
}
|