skillwiki 0.10.21 → 0.10.23

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.
Files changed (49) hide show
  1. package/dist/chunk-6ZDKTNLA.js +294 -0
  2. package/dist/{chunk-65Q5UGND.js → chunk-BASWDOQB.js} +2 -2
  3. package/dist/{chunk-Y6KRDGI2.js → chunk-C2DKFJFA.js} +6 -1
  4. package/dist/chunk-EQPU2BPM.js +468 -0
  5. package/dist/{chunk-GNS2ZV5P.js → chunk-PQG26AGJ.js} +1 -1
  6. package/dist/{chunk-I5JD3BQZ.js → chunk-QBZYEEBD.js} +13 -296
  7. package/dist/chunk-QNTBNNEL.js +616 -0
  8. package/dist/{chunk-UNPZDCWN.js → chunk-SYKSL3JQ.js} +860 -700
  9. package/dist/cli.js +2334 -273
  10. package/dist/{index-projection-HAXDEM2F.js → index-projection-FRWTZB5Y.js} +3 -2
  11. package/dist/{managed-write-preflight-CTXX2MHQ.js → managed-write-preflight-4LPVMP42.js} +3 -3
  12. package/dist/skillwiki-mcp.js +6 -4
  13. package/dist/sources-L2SQV63E.js +10 -0
  14. package/package.json +1 -1
  15. package/skills/.claude-plugin/plugin.json +1 -1
  16. package/skills/.codex-plugin/plugin.json +1 -1
  17. package/skills/README.md +13 -2
  18. package/skills/agents/wiki-add-task.md +2 -1
  19. package/skills/agents/wiki-archive.md +8 -9
  20. package/skills/agents/wiki-audit.md +2 -1
  21. package/skills/agents/wiki-lint.md +2 -1
  22. package/skills/agents/wiki-query.md +3 -1
  23. package/skills/agents/wiki-reingest.md +9 -8
  24. package/skills/package.json +1 -1
  25. package/skills/proj-decide/SKILL.md +35 -5
  26. package/skills/skills/proj-decide/SKILL.md +35 -5
  27. package/skills/skills/using-skillwiki/SKILL.md +19 -13
  28. package/skills/skills/wiki-add-task/SKILL.md +4 -3
  29. package/skills/skills/wiki-archive/SKILL.md +23 -18
  30. package/skills/skills/wiki-audit/SKILL.md +3 -1
  31. package/skills/skills/wiki-init/SKILL.md +12 -1
  32. package/skills/skills/wiki-lint/SKILL.md +2 -1
  33. package/skills/skills/wiki-query/SKILL.md +7 -0
  34. package/skills/skills/wiki-reingest/SKILL.md +12 -8
  35. package/skills/skills/wiki-remove/SKILL.md +22 -5
  36. package/skills/skills/wiki-sync/SKILL.md +39 -54
  37. package/skills/using-skillwiki/SKILL.md +19 -13
  38. package/skills/wiki-add-task/SKILL.md +4 -3
  39. package/skills/wiki-archive/SKILL.md +23 -18
  40. package/skills/wiki-audit/SKILL.md +3 -1
  41. package/skills/wiki-init/SKILL.md +12 -1
  42. package/skills/wiki-lint/SKILL.md +2 -1
  43. package/skills/wiki-query/SKILL.md +7 -0
  44. package/skills/wiki-reingest/SKILL.md +12 -8
  45. package/skills/wiki-remove/SKILL.md +22 -5
  46. package/skills/wiki-sync/SKILL.md +39 -54
  47. package/templates/SCHEMA.md +8 -5
  48. package/templates/web-clipper/llm-wiki-clippings.json +56 -0
  49. package/templates/web-clipper/readme.txt +16 -0
@@ -0,0 +1,616 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ extractFrontmatter,
4
+ readPage,
5
+ scanSensitiveContent,
6
+ splitFrontmatter
7
+ } from "./chunk-6ZDKTNLA.js";
8
+ import {
9
+ err,
10
+ ok
11
+ } from "./chunk-C2DKFJFA.js";
12
+
13
+ // src/utils/log-events.ts
14
+ import { mkdir, open, readFile, readdir } from "fs/promises";
15
+ import { join } from "path";
16
+ function isPlainObject(value) {
17
+ return typeof value === "object" && value !== null && !Array.isArray(value);
18
+ }
19
+ function canonicalize(value) {
20
+ if (Array.isArray(value)) return value.map(canonicalize);
21
+ if (!isPlainObject(value)) return value;
22
+ const out = {};
23
+ for (const key of Object.keys(value).sort()) {
24
+ out[key] = canonicalize(value[key]);
25
+ }
26
+ return out;
27
+ }
28
+ function eventPathFor(event) {
29
+ const day = event.occurred_at.slice(0, 10);
30
+ return `meta/log-events/${day}/${event.operation_id}.json`;
31
+ }
32
+ function validateLogEvent(event) {
33
+ if (event.schema !== "skillwiki-log-event/v1") {
34
+ return err("SCHEME_REJECTED", { message: "invalid event schema" });
35
+ }
36
+ if (!/^[0-9a-f]{64}$/.test(event.operation_id)) {
37
+ return err("SCHEME_REJECTED", { message: "operation_id must be 64 hex chars" });
38
+ }
39
+ if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(event.occurred_at)) {
40
+ return err("SCHEME_REJECTED", { message: "occurred_at must be UTC ISO with milliseconds" });
41
+ }
42
+ for (const field of ["host_id", "actor", "kind", "target", "note"]) {
43
+ const v = event[field];
44
+ if (typeof v !== "string" || v.trim().length === 0 || v.length > 500) {
45
+ return err("SCHEME_REJECTED", { message: `invalid ${field}` });
46
+ }
47
+ }
48
+ if (!isPlainObject(event.metadata)) {
49
+ return err("SCHEME_REJECTED", { message: "metadata must be a plain object" });
50
+ }
51
+ const sensitive = scanSensitiveContent(JSON.stringify(event));
52
+ if (sensitive.length > 0) {
53
+ return err("SENSITIVE_CONTENT_DETECTED", { findings: sensitive });
54
+ }
55
+ return ok({
56
+ schema: "skillwiki-log-event/v1",
57
+ operation_id: event.operation_id,
58
+ occurred_at: event.occurred_at,
59
+ host_id: event.host_id,
60
+ actor: event.actor,
61
+ kind: event.kind,
62
+ target: event.target,
63
+ note: event.note,
64
+ metadata: canonicalize(event.metadata)
65
+ });
66
+ }
67
+ function canonicalEventJson(event) {
68
+ const ordered = {
69
+ schema: event.schema,
70
+ operation_id: event.operation_id,
71
+ occurred_at: event.occurred_at,
72
+ host_id: event.host_id,
73
+ actor: event.actor,
74
+ kind: event.kind,
75
+ target: event.target,
76
+ note: event.note,
77
+ metadata: event.metadata
78
+ };
79
+ return `${JSON.stringify(ordered, null, 2)}
80
+ `;
81
+ }
82
+ async function writeLogEvent(vault, event) {
83
+ const validated = validateLogEvent(event);
84
+ if (!validated.ok) return validated;
85
+ const rel = eventPathFor(validated.data);
86
+ const abs = join(vault, rel);
87
+ await mkdir(join(vault, "meta", "log-events", validated.data.occurred_at.slice(0, 10)), {
88
+ recursive: true
89
+ });
90
+ const body = canonicalEventJson(validated.data);
91
+ try {
92
+ const handle = await open(abs, "wx");
93
+ try {
94
+ await handle.writeFile(body, "utf8");
95
+ } finally {
96
+ await handle.close();
97
+ }
98
+ return ok({ path: rel, created: true });
99
+ } catch (error) {
100
+ if (error.code === "EEXIST") {
101
+ let existing;
102
+ try {
103
+ existing = await readFile(abs, "utf8");
104
+ } catch (readErr) {
105
+ return err("WRITE_FAILED", { path: rel, message: String(readErr) });
106
+ }
107
+ if (existing === body) return ok({ path: rel, created: false });
108
+ return err("EVENT_IDENTITY_COLLISION", { path: rel });
109
+ }
110
+ return err("WRITE_FAILED", { path: rel, message: String(error) });
111
+ }
112
+ }
113
+ async function readLogEvents(vault) {
114
+ const root = join(vault, "meta", "log-events");
115
+ let days;
116
+ try {
117
+ days = (await readdir(root, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name).sort();
118
+ } catch {
119
+ return ok([]);
120
+ }
121
+ const events = [];
122
+ for (const day of days) {
123
+ const files = (await readdir(join(root, day))).filter((f) => f.endsWith(".json")).sort();
124
+ for (const file of files) {
125
+ const text = await readFile(join(root, day, file), "utf8");
126
+ let parsed;
127
+ try {
128
+ parsed = JSON.parse(text);
129
+ } catch {
130
+ return err("SCHEME_REJECTED", { path: `meta/log-events/${day}/${file}`, message: "invalid JSON" });
131
+ }
132
+ const validated = validateLogEvent(parsed);
133
+ if (!validated.ok) return validated;
134
+ if (eventPathFor(validated.data) !== `meta/log-events/${day}/${file}`) {
135
+ return err("SCHEME_REJECTED", {
136
+ path: `meta/log-events/${day}/${file}`,
137
+ message: "path/identity mismatch"
138
+ });
139
+ }
140
+ events.push(validated.data);
141
+ }
142
+ }
143
+ events.sort((a, b) => {
144
+ if (a.occurred_at !== b.occurred_at) return a.occurred_at < b.occurred_at ? -1 : 1;
145
+ return a.operation_id < b.operation_id ? -1 : a.operation_id > b.operation_id ? 1 : 0;
146
+ });
147
+ return ok(events);
148
+ }
149
+
150
+ // src/parsers/citations.ts
151
+ var FENCE = /```[\s\S]*?```/g;
152
+ var INLINE_CODE = /``[^`\n]+``|`[^`\n]+`/g;
153
+ var MARKER_RE = /\^\[(raw\/[^\]]+)\]/g;
154
+ var FRONTMATTER = /^---\n[\s\S]*?\n---\n?/;
155
+ function stripFences(body) {
156
+ return body.replace(FENCE, "").replace(INLINE_CODE, "");
157
+ }
158
+ var TILDE_FENCE = /~~~[\s\S]*?~~~/g;
159
+ function stripFencedBlocks(body) {
160
+ return body.replace(FENCE, "").replace(TILDE_FENCE, "");
161
+ }
162
+ function extractCitationMarkers(body) {
163
+ const stripped = stripFences(body);
164
+ const out = [];
165
+ let m;
166
+ while ((m = MARKER_RE.exec(stripped)) !== null) {
167
+ if (m[1] === "raw/...") continue;
168
+ out.push({ marker: m[0], target: m[1] });
169
+ }
170
+ return out;
171
+ }
172
+ function hasSourcesFooter(body) {
173
+ return /^## Sources\s*$/m.test(stripFencedBlocks(body));
174
+ }
175
+ function isLegacyCitationStyle(body) {
176
+ const markers = extractCitationMarkers(body);
177
+ if (markers.length === 0) return false;
178
+ if (!hasSourcesFooter(body)) return true;
179
+ const lines = stripFences(body.replace(FRONTMATTER, "")).split("\n");
180
+ let inSources = false;
181
+ let lastNonBlankWasTable = false;
182
+ for (const line of lines) {
183
+ if (/^## Sources\b/.test(line.trim())) {
184
+ inSources = true;
185
+ continue;
186
+ }
187
+ if (inSources) continue;
188
+ const matches = [...line.matchAll(MARKER_RE)];
189
+ if (matches.length === 0) {
190
+ if (line.trim().length > 0) lastNonBlankWasTable = /^\|/.test(line.trim());
191
+ continue;
192
+ }
193
+ const markerOnly = line.replace(MARKER_RE, "").trim();
194
+ if (markerOnly.length === 0 && !lastNonBlankWasTable) return true;
195
+ lastNonBlankWasTable = false;
196
+ const lastMatch = matches[matches.length - 1];
197
+ const afterLast = line.slice(lastMatch.index + lastMatch[0].length).replace(MARKER_RE, "").trim();
198
+ if (afterLast.length > 0) return true;
199
+ const beforeFirst = line.slice(0, matches[0].index).trim();
200
+ if (beforeFirst.length > 0 && !/[.!?]["'"]*\s*$/.test(beforeFirst)) return true;
201
+ }
202
+ return false;
203
+ }
204
+ function hasOrphanedCitations(body) {
205
+ const noFm = body.replace(FRONTMATTER, "");
206
+ const stripped = stripFences(noFm);
207
+ const lines = stripped.split("\n");
208
+ const rawLines = noFm.split("\n");
209
+ let inSources = false;
210
+ let sourcesEnded = false;
211
+ let sourcesStartLine = -1;
212
+ let lastNonBlankInSources = -1;
213
+ for (let i = 0; i < lines.length; i++) {
214
+ const line = lines[i];
215
+ const trimmed = line.trim();
216
+ if (/^## Sources\b/.test(trimmed)) {
217
+ inSources = true;
218
+ sourcesStartLine = i;
219
+ continue;
220
+ }
221
+ if (!inSources || sourcesEnded) continue;
222
+ if (trimmed.length === 0) {
223
+ if (lastNonBlankInSources >= 0) {
224
+ sourcesEnded = true;
225
+ }
226
+ continue;
227
+ }
228
+ const isListItem = /^\s*(?:[-*]|\d+\.)\s+/.test(line);
229
+ const hasMarker = /\^\[raw\//.test(line);
230
+ const hasBacktickRawPath = /`raw\/[^`]+`/.test(rawLines[i]);
231
+ if (isListItem && hasMarker) {
232
+ lastNonBlankInSources = i;
233
+ } else if (isListItem && hasBacktickRawPath) {
234
+ lastNonBlankInSources = i;
235
+ } else if (hasMarker && !isListItem) {
236
+ return true;
237
+ } else {
238
+ sourcesEnded = true;
239
+ }
240
+ }
241
+ if (sourcesStartLine === -1) return false;
242
+ if (sourcesEnded) {
243
+ const scanStart = Math.max(lastNonBlankInSources + 1, sourcesStartLine + 1);
244
+ for (let i = scanStart; i < lines.length; i++) {
245
+ if (/\^\[raw\//.test(lines[i])) {
246
+ return true;
247
+ }
248
+ }
249
+ }
250
+ return false;
251
+ }
252
+ function hasWikilinkCitations(body) {
253
+ const stripped = stripFences(body);
254
+ return /\[\[raw\/[^\]]+\]\]/.test(stripped);
255
+ }
256
+
257
+ // src/utils/source-relocations.ts
258
+ function parse(event) {
259
+ if (event.kind !== "source-relocation") return ok(null);
260
+ const metadata = event.metadata;
261
+ const operation = metadata.operation;
262
+ const previous = metadata.previous_path;
263
+ const current = metadata.current_path;
264
+ const hash = metadata.source_sha256;
265
+ if (!["rename", "relocate", "archive", "deduplicate"].includes(operation)) {
266
+ return err("SOURCE_RELOCATION_INVALID", { operation_id: event.operation_id, field: "operation" });
267
+ }
268
+ if (typeof previous !== "string" || typeof current !== "string" || typeof hash !== "string" || !/^[0-9a-f]{64}$/.test(hash)) {
269
+ return err("SOURCE_RELOCATION_INVALID", { operation_id: event.operation_id, field: "metadata" });
270
+ }
271
+ return ok({
272
+ operation_id: event.operation_id,
273
+ operation,
274
+ previous_path: previous,
275
+ current_path: current,
276
+ source_sha256: hash,
277
+ occurred_at: event.occurred_at
278
+ });
279
+ }
280
+ function projectSourceRelocations(events) {
281
+ const out = [];
282
+ for (const event of events) {
283
+ const relocation = parse(event);
284
+ if (!relocation.ok) return relocation;
285
+ if (relocation.data) out.push(relocation.data);
286
+ }
287
+ return ok(out);
288
+ }
289
+ async function readSourceRelocations(vault) {
290
+ const events = await readLogEvents(vault);
291
+ if (!events.ok) return events;
292
+ return projectSourceRelocations(events.data);
293
+ }
294
+ function buildSourceRelocationProjection(relocations) {
295
+ const projection = /* @__PURE__ */ new Map();
296
+ for (const relocation of relocations) {
297
+ const current = projection.get(relocation.current_path) ?? relocation.current_path;
298
+ projection.set(relocation.previous_path, current);
299
+ for (const [historical, target] of projection) {
300
+ if (target === relocation.previous_path) projection.set(historical, current);
301
+ }
302
+ }
303
+ return projection;
304
+ }
305
+ function resolveRelocatedSource(target, projection) {
306
+ let current = target;
307
+ const seen = /* @__PURE__ */ new Set();
308
+ while (projection.has(current) && !seen.has(current)) {
309
+ seen.add(current);
310
+ current = projection.get(current);
311
+ }
312
+ return current;
313
+ }
314
+
315
+ // src/utils/raw-source.ts
316
+ import { join as join2, posix, relative as relative2 } from "path";
317
+
318
+ // src/utils/vault-path-safety.ts
319
+ import { lstatSync, realpathSync } from "fs";
320
+ import { lstat, realpath } from "fs/promises";
321
+ import { dirname, relative, resolve, sep } from "path";
322
+ function vaultRelative(vaultReal, candidate) {
323
+ return relative(vaultReal, candidate).split(sep).join("/");
324
+ }
325
+ function isInside(vaultReal, candidate) {
326
+ const rel = vaultRelative(vaultReal, candidate);
327
+ return rel === "" || rel !== ".." && !rel.startsWith("../");
328
+ }
329
+ function existingRegularFileInsideVaultSync(vault, target) {
330
+ try {
331
+ const vaultReal = realpathSync(vault);
332
+ const absolutePath = resolve(vaultReal, target);
333
+ const rel = vaultRelative(vaultReal, absolutePath);
334
+ if (!isInside(vaultReal, absolutePath) || rel === "") return false;
335
+ let current = vaultReal;
336
+ for (const part of rel.split("/").filter(Boolean)) {
337
+ current = resolve(current, part);
338
+ if (lstatSync(current).isSymbolicLink()) return false;
339
+ }
340
+ const info = lstatSync(absolutePath);
341
+ return info.isFile() && !info.isSymbolicLink() && realpathSync(absolutePath) === absolutePath;
342
+ } catch {
343
+ return false;
344
+ }
345
+ }
346
+ async function canonicalVault(vault, target) {
347
+ try {
348
+ return ok(await realpath(vault));
349
+ } catch (error) {
350
+ return err("VAULT_PATH_INVALID", { target, message: "vault realpath failed", cause: String(error) });
351
+ }
352
+ }
353
+ async function rejectSymlinkedComponents(vaultReal, absolutePath, target) {
354
+ const rel = vaultRelative(vaultReal, absolutePath);
355
+ if (!isInside(vaultReal, absolutePath) || rel === "") {
356
+ return err("VAULT_PATH_INVALID", { target, message: "target escapes vault" });
357
+ }
358
+ const parts = rel.split("/").filter(Boolean);
359
+ let current = vaultReal;
360
+ for (const part of parts) {
361
+ current = resolve(current, part);
362
+ try {
363
+ const info = await lstat(current);
364
+ if (info.isSymbolicLink()) {
365
+ return err("VAULT_PATH_INVALID", { target, message: "target path may not contain symlink aliases" });
366
+ }
367
+ } catch (error) {
368
+ if (error.code === "ENOENT") break;
369
+ return err("VAULT_PATH_INVALID", { target, message: "target lstat failed", cause: String(error) });
370
+ }
371
+ }
372
+ return ok(true);
373
+ }
374
+ async function resolveExistingRegularFileInsideVault(vault, target) {
375
+ const vaultResult = await canonicalVault(vault, target);
376
+ if (!vaultResult.ok) return vaultResult;
377
+ const vaultReal = vaultResult.data;
378
+ const absolutePath = resolve(vaultReal, target);
379
+ const components = await rejectSymlinkedComponents(vaultReal, absolutePath, target);
380
+ if (!components.ok) return components;
381
+ try {
382
+ const info = await lstat(absolutePath);
383
+ if (!info.isFile() || info.isSymbolicLink()) {
384
+ return err("VAULT_PATH_INVALID", { target, message: "target must be a regular non-symlink file" });
385
+ }
386
+ const targetReal = await realpath(absolutePath);
387
+ if (!isInside(vaultReal, targetReal) || targetReal !== absolutePath) {
388
+ return err("VAULT_PATH_INVALID", { target, message: "target realpath escapes vault or uses an alias" });
389
+ }
390
+ return ok(absolutePath);
391
+ } catch (error) {
392
+ return err("FILE_NOT_FOUND", { path: target, message: String(error) });
393
+ }
394
+ }
395
+ async function resolveAbsentTargetInsideVault(vault, target) {
396
+ const vaultResult = await canonicalVault(vault, target);
397
+ if (!vaultResult.ok) return vaultResult;
398
+ const vaultReal = vaultResult.data;
399
+ const absolutePath = resolve(vaultReal, target);
400
+ const components = await rejectSymlinkedComponents(vaultReal, dirname(absolutePath), target);
401
+ if (!components.ok) return components;
402
+ try {
403
+ await lstat(absolutePath);
404
+ return err("RAW_DESTINATION_EXISTS", { path: target });
405
+ } catch (error) {
406
+ if (error.code !== "ENOENT") {
407
+ return err("VAULT_PATH_INVALID", { target, message: "destination lstat failed", cause: String(error) });
408
+ }
409
+ }
410
+ return ok(absolutePath);
411
+ }
412
+
413
+ // src/utils/raw-source.ts
414
+ function normalizeRawSourceTarget(entry) {
415
+ let target = entry.trim().replace(/^"/, "").replace(/"$/, "").replace(/^'/, "").replace(/'$/, "");
416
+ target = target.replace(/^\^\[/, "").replace(/\]$/, "");
417
+ if (!target || target.includes("\0") || target.includes("\\") || posix.isAbsolute(target)) return null;
418
+ if (posix.normalize(target) !== target || target.split("/").some((part) => part === "" || part === "." || part === "..")) return null;
419
+ if (!target.startsWith("raw/") && !target.startsWith("_archive/raw/")) return null;
420
+ return target;
421
+ }
422
+ function rawSourceTargetCandidates(vault, target) {
423
+ const normalized = normalizeRawSourceTarget(target);
424
+ if (!normalized) return [];
425
+ const candidates = [join2(vault, normalized)];
426
+ if (!normalized.endsWith(".md")) candidates.push(join2(vault, `${normalized}.md`));
427
+ if (normalized.startsWith("raw/")) {
428
+ const active = normalized.match(/^raw\/(articles|papers|transcripts)\/(.+)$/);
429
+ if (active) {
430
+ for (const lifecycle of ["archived", "duplicates"]) {
431
+ candidates.push(join2(vault, "raw", lifecycle, active[1], active[2]));
432
+ if (!normalized.endsWith(".md")) candidates.push(join2(vault, "raw", lifecycle, active[1], `${active[2]}.md`));
433
+ }
434
+ candidates.push(join2(vault, "_archive", normalized));
435
+ if (!normalized.endsWith(".md")) candidates.push(join2(vault, "_archive", `${normalized}.md`));
436
+ }
437
+ }
438
+ return [...new Set(candidates)];
439
+ }
440
+ function rawSourceTargetExistsSync(vault, target) {
441
+ return rawSourceTargetCandidates(vault, target).some(
442
+ (candidate) => existingRegularFileInsideVaultSync(vault, relative2(vault, candidate))
443
+ );
444
+ }
445
+ async function rawSourceTargetExists(vault, target) {
446
+ for (const candidate of rawSourceTargetCandidates(vault, target)) {
447
+ if ((await resolveExistingRegularFileInsideVault(vault, relative2(vault, candidate))).ok) return true;
448
+ }
449
+ const relocations = await readSourceRelocations(vault);
450
+ if (relocations.ok) {
451
+ const normalized = normalizeRawSourceTarget(target);
452
+ if (normalized) {
453
+ const resolved = resolveRelocatedSource(normalized.endsWith(".md") ? normalized : `${normalized}.md`, buildSourceRelocationProjection(relocations.data));
454
+ if (resolved !== normalized) {
455
+ if ((await resolveExistingRegularFileInsideVault(vault, resolved)).ok) return true;
456
+ }
457
+ }
458
+ }
459
+ return false;
460
+ }
461
+
462
+ // src/utils/operation-id.ts
463
+ import { createHash } from "crypto";
464
+ function operationId(namespace, parts) {
465
+ const hash = createHash("sha256").update(namespace).update("\0");
466
+ for (const part of parts) hash.update(part).update("\0");
467
+ return hash.digest("hex");
468
+ }
469
+
470
+ // src/utils/raw-operation-policy.ts
471
+ import { posix as posix2 } from "path";
472
+ var SOURCE_CATEGORIES = /* @__PURE__ */ new Set(["articles", "papers", "transcripts"]);
473
+ function classifyRawPath(value) {
474
+ const path = value.replaceAll("\\", "/");
475
+ if (path !== posix2.normalize(path) || path.startsWith("/") || path.includes("\0") || path.split("/").includes("..")) {
476
+ return err("RAW_PATH_INVALID", { path: value });
477
+ }
478
+ const parts = path.split("/");
479
+ if (parts[0] !== "raw") return err("RAW_PATH_OUTSIDE_LAYER", { path });
480
+ if (parts[1] === "assets" && parts.length > 2) {
481
+ return ok({ path, category: "assets", storage: "asset", relativeWithinCategory: parts.slice(2).join("/") });
482
+ }
483
+ if ((parts[1] === "archived" || parts[1] === "duplicates") && SOURCE_CATEGORIES.has(parts[2] ?? "") && parts.length > 3) {
484
+ return ok({
485
+ path,
486
+ category: parts[2],
487
+ storage: parts[1] === "archived" ? "archived" : "duplicate",
488
+ relativeWithinCategory: parts.slice(3).join("/")
489
+ });
490
+ }
491
+ if (SOURCE_CATEGORIES.has(parts[1] ?? "") && parts.length > 2) {
492
+ return ok({
493
+ path,
494
+ category: parts[1],
495
+ storage: "active",
496
+ relativeWithinCategory: parts.slice(2).join("/")
497
+ });
498
+ }
499
+ return err("RAW_PATH_UNSUPPORTED", { path });
500
+ }
501
+ function lifecycleDestination(source, operation) {
502
+ const parsed = classifyRawPath(source);
503
+ if (!parsed.ok) return parsed;
504
+ if (parsed.data.category === "assets") {
505
+ return err("RAW_ASSET_PATH_FROZEN", { path: source, message: "asset lifecycle is logical by default" });
506
+ }
507
+ if (parsed.data.storage !== "active") return err("RAW_SOURCE_NOT_ACTIVE", { path: source, storage: parsed.data.storage });
508
+ if (operation === "archive") return ok(`raw/archived/${parsed.data.category}/${parsed.data.relativeWithinCategory}`);
509
+ if (operation === "deduplicate") return ok(`raw/duplicates/${parsed.data.category}/${parsed.data.relativeWithinCategory}`);
510
+ return ok(source);
511
+ }
512
+ function authorizeRawOperation(input) {
513
+ if (input.operationClass === "read") return ok(true);
514
+ if (input.operationClass === "rewrite") return err("RAW_REWRITE_FORBIDDEN", { source: input.source });
515
+ if (input.operationClass === "destructive-remove") {
516
+ return input.trigger === "attended-apply" && input.explicitExactTarget === true ? ok(true) : err("RAW_DISPOSAL_REQUIRES_EXACT_TARGET_APPROVAL", { source: input.source });
517
+ }
518
+ if (input.operationClass === "preserve-move") {
519
+ if (input.trigger === "report-only") return err("RAW_STRUCTURAL_REPORT_ONLY", { source: input.source, destination: input.destination });
520
+ if (!input.source || !input.destination) return err("RAW_PATH_INVALID", { source: input.source, destination: input.destination });
521
+ const source = classifyRawPath(input.source);
522
+ if (!source.ok) return source;
523
+ const destination = classifyRawPath(input.destination);
524
+ if (!destination.ok) return destination;
525
+ return ok(true);
526
+ }
527
+ return ok(true);
528
+ }
529
+
530
+ // src/utils/source-reference-index.ts
531
+ function canonicalTarget(value) {
532
+ const normalized = normalizeRawSourceTarget(value);
533
+ if (!normalized) return null;
534
+ return normalized.endsWith(".md") ? normalized : `${normalized}.md`;
535
+ }
536
+ function referencesFromText(text) {
537
+ const references = /* @__PURE__ */ new Set();
538
+ const fm = extractFrontmatter(text);
539
+ if (fm.ok && Array.isArray(fm.data.sources)) {
540
+ for (const entry of fm.data.sources) {
541
+ const target = canonicalTarget(String(entry));
542
+ if (target) references.add(target);
543
+ }
544
+ }
545
+ const split = splitFrontmatter(text);
546
+ const body = split.ok ? split.data.body : text;
547
+ for (const marker of extractCitationMarkers(body)) {
548
+ const target = canonicalTarget(marker.target);
549
+ if (target) references.add(target);
550
+ }
551
+ return [...references];
552
+ }
553
+ async function addPages(pages, kind, available, targetMap, unresolved, relocationProjection) {
554
+ for (const page of pages) {
555
+ let text;
556
+ try {
557
+ text = await readPage(page);
558
+ } catch {
559
+ continue;
560
+ }
561
+ for (const historicalTarget of referencesFromText(text)) {
562
+ const target = available.has(historicalTarget) ? historicalTarget : relocationProjection?.get(historicalTarget) ?? historicalTarget;
563
+ if (!available.has(target)) unresolved.push({ sourcePath: page.relPath, target, kind });
564
+ const refs = targetMap.get(target) ?? /* @__PURE__ */ new Set();
565
+ refs.add(page.relPath);
566
+ targetMap.set(target, refs);
567
+ }
568
+ }
569
+ }
570
+ function freezeMap(input) {
571
+ return new Map(
572
+ [...input.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([target, refs]) => [target, [...refs].sort((a, b) => a.localeCompare(b))])
573
+ );
574
+ }
575
+ async function buildSourceReferenceIndex(input) {
576
+ const available = new Set(
577
+ [...input.availableRawPaths].map(canonicalTarget).filter((value) => value !== null)
578
+ );
579
+ const integrated = /* @__PURE__ */ new Map();
580
+ const elsewhere = /* @__PURE__ */ new Map();
581
+ const unresolved = [];
582
+ await addPages(input.typedPages, "typed", available, integrated, unresolved, input.relocationProjection);
583
+ await addPages(input.otherPages ?? [], "other", available, elsewhere, unresolved, input.relocationProjection);
584
+ unresolved.sort((a, b) => a.target.localeCompare(b.target) || a.sourcePath.localeCompare(b.sourcePath));
585
+ return {
586
+ integratedBy: freezeMap(integrated),
587
+ referencedElsewhereBy: freezeMap(elsewhere),
588
+ unresolved
589
+ };
590
+ }
591
+
592
+ export {
593
+ eventPathFor,
594
+ validateLogEvent,
595
+ canonicalEventJson,
596
+ writeLogEvent,
597
+ readLogEvents,
598
+ stripFencedBlocks,
599
+ extractCitationMarkers,
600
+ isLegacyCitationStyle,
601
+ hasOrphanedCitations,
602
+ hasWikilinkCitations,
603
+ projectSourceRelocations,
604
+ readSourceRelocations,
605
+ buildSourceRelocationProjection,
606
+ resolveExistingRegularFileInsideVault,
607
+ resolveAbsentTargetInsideVault,
608
+ normalizeRawSourceTarget,
609
+ rawSourceTargetExistsSync,
610
+ rawSourceTargetExists,
611
+ operationId,
612
+ classifyRawPath,
613
+ lifecycleDestination,
614
+ authorizeRawOperation,
615
+ buildSourceReferenceIndex
616
+ };