@xagents_in/unmask 0.0.0-stage → 0.1.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.
@@ -0,0 +1,230 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ const states = ["byte-identical", "changed", "artifact-only", "not-compared"];
4
+ const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
5
+ const order = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
6
+ const linked = (entry) => entry.type === "symlink" || entry.type === "hardlink";
7
+ const count = () => ({ files: 0, bytes: 0 });
8
+ function add(counter, entry) {
9
+ counter.files++;
10
+ counter.bytes += entry.size;
11
+ if (!Number.isSafeInteger(counter.bytes))
12
+ throw new TypeError("Unsafe inventory byte count");
13
+ }
14
+ function checkInventory(inventory) {
15
+ if (
16
+ !inventory ||
17
+ !Array.isArray(inventory.entries) ||
18
+ typeof inventory.complete !== "boolean" ||
19
+ !/^[a-f0-9]{64}$/.test(inventory.digest?.sha256 || "")
20
+ )
21
+ throw new TypeError("Invalid archive inventory");
22
+ const seen = new Set();
23
+ for (const entry of inventory.entries) {
24
+ if (
25
+ !["file", "directory", "symlink", "hardlink"].includes(entry.type) ||
26
+ typeof entry.originalPath !== "string" ||
27
+ !(entry.path === null || typeof entry.path === "string") ||
28
+ !Number.isSafeInteger(entry.size) ||
29
+ entry.size < 0
30
+ )
31
+ throw new TypeError("Invalid inventory entry");
32
+ if (entry.path !== null && seen.has(entry.path))
33
+ throw new TypeError("Duplicate comparison path");
34
+ if (entry.path !== null) seen.add(entry.path);
35
+ if (linked(entry) && typeof entry.linkTarget !== "string")
36
+ throw new TypeError("Invalid inert link target");
37
+ if (
38
+ entry.type === "file" &&
39
+ (!/^[a-f0-9]{64}$/.test(entry.sha256 || "") ||
40
+ !(entry.content === null || Buffer.isBuffer(entry.content)) ||
41
+ (entry.content !== null &&
42
+ (entry.content.length !== entry.size ||
43
+ hash(entry.content) !== entry.sha256)))
44
+ )
45
+ throw new TypeError("Invalid regular file digest or retained bytes");
46
+ }
47
+ }
48
+ function evidenceFor(side, inventory, entry, range) {
49
+ const identity = [
50
+ side,
51
+ inventory.digest.sha256,
52
+ entry.path ?? entry.originalPath,
53
+ range,
54
+ ];
55
+ const evidence = {
56
+ id: `ev-${hash(JSON.stringify(identity))}`,
57
+ side,
58
+ blobDigest: inventory.digest.sha256,
59
+ path: entry.path,
60
+ originalPath: entry.originalPath,
61
+ fileSha256: entry.sha256,
62
+ size: entry.size,
63
+ range,
64
+ };
65
+ if (entry.content !== null) {
66
+ const end = Math.min(range[1], range[0] + 512);
67
+ try {
68
+ evidence.excerpt = new TextDecoder("utf-8", {
69
+ fatal: true,
70
+ ignoreBOM: true,
71
+ }).decode(entry.content.subarray(range[0], end));
72
+ evidence.excerptRange = [range[0], end];
73
+ } catch {}
74
+ }
75
+ return evidence;
76
+ }
77
+ function changedSpans(a, b) {
78
+ let start = 0;
79
+ while (start < a.length && start < b.length && a[start] === b[start]) start++;
80
+ let aEnd = a.length;
81
+ let bEnd = b.length;
82
+ while (aEnd > start && bEnd > start && a[aEnd - 1] === b[bEnd - 1]) {
83
+ aEnd--;
84
+ bEnd--;
85
+ }
86
+ return { artifact: [start, aEnd], source: [start, bEnd] };
87
+ }
88
+
89
+ export function compareInventories(artifact, sourceOrNull) {
90
+ checkInventory(artifact);
91
+ if (sourceOrNull !== null) checkInventory(sourceOrNull);
92
+ const source = sourceOrNull;
93
+ const sourceEntries = new Map(
94
+ (source?.entries || [])
95
+ .filter((e) => e.path !== null)
96
+ .map((e) => [e.path, e]),
97
+ );
98
+ function ancestorLink(target) {
99
+ if (target === null) return null;
100
+ const parts = target.split("/");
101
+ let prefix = "";
102
+ for (const part of parts) {
103
+ prefix = prefix ? `${prefix}/${part}` : part;
104
+ const entry = sourceEntries.get(prefix);
105
+ if (entry && linked(entry)) return entry;
106
+ }
107
+ return null;
108
+ }
109
+ const regular = artifact.entries
110
+ .filter((e) => e.type === "file")
111
+ .sort(
112
+ (a, b) =>
113
+ order(a.path ?? "", b.path ?? "") ||
114
+ order(a.originalPath, b.originalPath),
115
+ );
116
+ const files = [];
117
+ const evidence = [];
118
+ const coverage = {
119
+ inventory: {
120
+ ...count(),
121
+ complete: artifact.complete,
122
+ lowerBound: !artifact.complete,
123
+ },
124
+ states: Object.fromEntries(states.map((state) => [state, count()])),
125
+ compared: count(),
126
+ unresolved: count(),
127
+ syntaxInspected: count(),
128
+ modelReviewed: count(),
129
+ links: {
130
+ artifact: artifact.entries.filter(linked).length,
131
+ source: source?.entries.filter(linked).length || 0,
132
+ },
133
+ nonregular: {
134
+ artifact: artifact.entries.filter((e) => e.type !== "file").length,
135
+ source: source?.entries.filter((e) => e.type !== "file").length || 0,
136
+ },
137
+ linkEntries: [
138
+ ["artifact", artifact],
139
+ ["source", source],
140
+ ]
141
+ .flatMap(([side, inventory]) =>
142
+ (inventory?.entries || []).filter(linked).map((e) => ({
143
+ side,
144
+ path: e.path,
145
+ originalPath: e.originalPath,
146
+ type: e.type,
147
+ linkTarget: e.linkTarget,
148
+ })),
149
+ )
150
+ .sort(
151
+ (a, b) =>
152
+ order(a.side, b.side) ||
153
+ order(a.path ?? "", b.path ?? "") ||
154
+ order(a.originalPath, b.originalPath),
155
+ ),
156
+ sourceAvailable: source !== null,
157
+ sourceComplete: source?.complete ?? false,
158
+ complete: artifact.complete && (source === null || source.complete),
159
+ sourceOnly: [],
160
+ };
161
+ for (const entry of regular) {
162
+ const twin = entry.path === null ? null : sourceEntries.get(entry.path);
163
+ const link = ancestorLink(entry.path);
164
+ const file = {
165
+ path: entry.path,
166
+ originalPath: entry.originalPath,
167
+ size: entry.size,
168
+ artifactSha256: entry.sha256,
169
+ state: "not-compared",
170
+ localization: "whole-file",
171
+ evidenceIds: [],
172
+ };
173
+ if (entry.path === null) file.reason = "prefixless";
174
+ else if (!source) file.reason = "source-unavailable";
175
+ else if (link) {
176
+ file.reason = "source-link";
177
+ file.sourceLink = {
178
+ path: link.path,
179
+ type: link.type,
180
+ linkTarget: link.linkTarget,
181
+ };
182
+ } else if (twin?.type === "file") {
183
+ file.sourceSha256 = twin.sha256;
184
+ file.state =
185
+ entry.size === twin.size && entry.sha256 === twin.sha256
186
+ ? "byte-identical"
187
+ : "changed";
188
+ if (
189
+ file.state === "changed" &&
190
+ entry.content !== null &&
191
+ twin.content !== null
192
+ ) {
193
+ file.localization = "byte-spans";
194
+ file.spans = changedSpans(entry.content, twin.content);
195
+ }
196
+ } else if (!source.complete) file.reason = "source-incomplete";
197
+ else if (twin) file.reason = "source-nonregular";
198
+ else file.state = "artifact-only";
199
+ const artifactEvidence = evidenceFor(
200
+ "artifact",
201
+ artifact,
202
+ entry,
203
+ file.spans?.artifact || [0, entry.size],
204
+ );
205
+ evidence.push(artifactEvidence);
206
+ file.evidenceIds.push(artifactEvidence.id);
207
+ if (twin?.type === "file" && !link && entry.path !== null) {
208
+ const sourceEvidence = evidenceFor(
209
+ "source",
210
+ source,
211
+ twin,
212
+ file.spans?.source || [0, twin.size],
213
+ );
214
+ evidence.push(sourceEvidence);
215
+ file.evidenceIds.push(sourceEvidence.id);
216
+ }
217
+ add(coverage.inventory, entry);
218
+ add(coverage.states[file.state], entry);
219
+ if (file.state === "changed" || file.state === "byte-identical")
220
+ add(coverage.compared, entry);
221
+ if (file.state !== "byte-identical") add(coverage.unresolved, entry);
222
+ files.push(file);
223
+ }
224
+ const artifactPaths = new Set(artifact.entries.map((e) => e.path));
225
+ coverage.sourceOnly = [...sourceEntries.values()]
226
+ .filter((e) => e.type !== "directory" && !artifactPaths.has(e.path))
227
+ .map((e) => e.path)
228
+ .sort(order);
229
+ return { files, coverage, evidence };
230
+ }