skillwiki 0.10.49 → 0.10.51

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,1045 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ buildSourceReferenceIndex,
4
+ buildSourceRelocationProjection,
5
+ classifyRawPath,
6
+ operationId,
7
+ projectSourceRelocations,
8
+ readLogEvents,
9
+ resolveExistingRegularFileInsideVault,
10
+ writeLogEvent
11
+ } from "./chunk-GAHMWLWU.js";
12
+ import {
13
+ ExitCode,
14
+ RawSourceSchema,
15
+ err,
16
+ extractFrontmatter,
17
+ ok,
18
+ readPage,
19
+ scanSensitiveContent,
20
+ scanVault,
21
+ splitFrontmatter
22
+ } from "./chunk-IJ7DD7QZ.js";
23
+
24
+ // src/commands/sources.ts
25
+ import { readFile as readFile4 } from "fs/promises";
26
+
27
+ // src/utils/source-lifecycle.ts
28
+ import { createHash as createHash3 } from "crypto";
29
+ import { readFile as readFile2, readdir } from "fs/promises";
30
+ import { basename, join, relative, sep } from "path";
31
+
32
+ // src/utils/source-dispositions.ts
33
+ import { createHash as createHash2 } from "crypto";
34
+ import { readFile } from "fs/promises";
35
+
36
+ // src/utils/source-action-approval.ts
37
+ import { createHash } from "crypto";
38
+ var VERSION = "swsrc1";
39
+ function sha256(value) {
40
+ return createHash("sha256").update(value, "utf8").digest("hex");
41
+ }
42
+ function encodeSourceActionApproval(payload) {
43
+ const middle = Buffer.from(JSON.stringify(payload), "utf8").toString("base64url");
44
+ return `${VERSION}.${middle}.${sha256(middle)}`;
45
+ }
46
+ function decodeSourceActionApproval(token) {
47
+ const parts = token.split(".");
48
+ if (parts.length !== 3 || parts[0] !== VERSION || !/^[0-9a-f]{64}$/.test(parts[2] ?? "")) {
49
+ return err("APPROVAL_INVALID", { message: "invalid source-action approval token" });
50
+ }
51
+ if (sha256(parts[1]) !== parts[2]) return err("APPROVAL_INVALID", { message: "source-action approval checksum mismatch" });
52
+ try {
53
+ const parsed = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
54
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return err("APPROVAL_INVALID", { message: "approval payload must be an object" });
55
+ return ok(parsed);
56
+ } catch {
57
+ return err("APPROVAL_INVALID", { message: "source-action approval payload is invalid" });
58
+ }
59
+ }
60
+
61
+ // src/utils/source-dispositions.ts
62
+ var STATUSES = /* @__PURE__ */ new Set(["reviewed-no-op", "deferred", "duplicate", "out-of-scope", "superseded", "reopened"]);
63
+ function bodySha256(text) {
64
+ const split = splitFrontmatter(text);
65
+ const body = split.ok ? split.data.body : text;
66
+ return createHash2("sha256").update(Buffer.from(body, "utf8")).digest("hex");
67
+ }
68
+ function completeSha256(text) {
69
+ return createHash2("sha256").update(Buffer.from(text, "utf8")).digest("hex");
70
+ }
71
+ function exactActiveSource(path) {
72
+ const parsed = classifyRawPath(path);
73
+ if (!parsed.ok) return parsed;
74
+ if (parsed.data.storage !== "active" || !["articles", "papers"].includes(parsed.data.category)) {
75
+ return err("SOURCE_DISPOSITION_TARGET_INVALID", { path, message: "dispositions require an exact active raw article or paper" });
76
+ }
77
+ return ok(true);
78
+ }
79
+ function validDate(value) {
80
+ return /^\d{4}-\d{2}-\d{2}$/.test(value) && (/* @__PURE__ */ new Date(`${value}T00:00:00Z`)).toISOString().slice(0, 10) === value;
81
+ }
82
+ function parseSourceDispositionEvent(event) {
83
+ if (event.kind !== "source-disposition") return ok(null);
84
+ const metadata = event.metadata;
85
+ if (!STATUSES.has(metadata.status) || typeof metadata.raw_path !== "string" || typeof metadata.complete_sha256 !== "string" && typeof metadata.body_sha256 !== "string" || typeof metadata.complete_sha256 === "string" && !/^[0-9a-f]{64}$/.test(metadata.complete_sha256) || typeof metadata.body_sha256 === "string" && !/^[0-9a-f]{64}$/.test(metadata.body_sha256) || typeof metadata.reason !== "string") {
86
+ return err("SOURCE_DISPOSITION_INVALID", { operation_id: event.operation_id });
87
+ }
88
+ return ok({
89
+ operation_id: event.operation_id,
90
+ occurred_at: event.occurred_at,
91
+ raw_path: metadata.raw_path,
92
+ ...typeof metadata.complete_sha256 === "string" ? { complete_sha256: metadata.complete_sha256 } : {},
93
+ ...typeof metadata.body_sha256 === "string" ? { body_sha256: metadata.body_sha256 } : {},
94
+ status: metadata.status,
95
+ reason: metadata.reason,
96
+ ...typeof metadata.review_after === "string" ? { review_after: metadata.review_after } : {},
97
+ ...typeof metadata.duplicate_of === "string" ? { duplicate_of: metadata.duplicate_of } : {}
98
+ });
99
+ }
100
+ function projectSourceDispositions(events) {
101
+ const dispositions = [];
102
+ for (const event of events) {
103
+ const parsed = parseSourceDispositionEvent(event);
104
+ if (!parsed.ok) return parsed;
105
+ if (parsed.data) dispositions.push(parsed.data);
106
+ }
107
+ return ok(dispositions.sort((a, b) => a.occurred_at.localeCompare(b.occurred_at) || a.operation_id.localeCompare(b.operation_id)));
108
+ }
109
+ async function readSourceDispositions(vault) {
110
+ const events = await readLogEvents(vault);
111
+ if (!events.ok) return events;
112
+ return projectSourceDispositions(events.data);
113
+ }
114
+ function effectiveSourceDisposition(input) {
115
+ const candidates = input.dispositions.filter((event) => event.raw_path === input.rawPath);
116
+ const latest = candidates.at(-1);
117
+ if (!latest) return { lifecycle: "pending", identityMismatch: false };
118
+ const identityMatches = latest.complete_sha256 ? latest.complete_sha256 === input.completeSha256 : latest.body_sha256 === input.bodySha256;
119
+ if (!identityMatches) return { lifecycle: "pending", identityMismatch: true, disposition: latest };
120
+ if (latest.status === "reopened") return { lifecycle: "pending", identityMismatch: false, disposition: latest };
121
+ if (latest.status === "deferred") {
122
+ return latest.review_after && latest.review_after > input.today ? { lifecycle: "deferred", identityMismatch: false, disposition: latest } : { lifecycle: "pending", identityMismatch: false, disposition: latest };
123
+ }
124
+ return { lifecycle: latest.status, identityMismatch: false, disposition: latest };
125
+ }
126
+ function latestDuplicateTarget(dispositions, rawPath) {
127
+ const latest = dispositions.filter((event) => event.raw_path === rawPath).at(-1);
128
+ return latest?.status === "duplicate" ? latest.duplicate_of : void 0;
129
+ }
130
+ async function planSourceDisposition(input) {
131
+ const target = exactActiveSource(input.rawPath);
132
+ if (!target.ok) return target;
133
+ if (!STATUSES.has(input.status)) return err("SOURCE_DISPOSITION_INVALID", { field: "status" });
134
+ const reason = input.reason.normalize("NFC").trim();
135
+ if (!reason) return err("SOURCE_DISPOSITION_INVALID", { field: "reason", message: "reason is required" });
136
+ const sensitive = scanSensitiveContent(reason);
137
+ if (sensitive.length > 0) return err("SENSITIVE_CONTENT_DETECTED", { findings: sensitive });
138
+ const today = input.today ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
139
+ if (input.status === "deferred" && (!input.reviewAfter || !validDate(input.reviewAfter) || input.reviewAfter <= today)) {
140
+ return err("SOURCE_DISPOSITION_INVALID", { field: "review_after", message: "deferred requires a future YYYY-MM-DD" });
141
+ }
142
+ if (input.status === "duplicate") {
143
+ if (!input.duplicateOf || input.duplicateOf === input.rawPath) return err("SOURCE_DISPOSITION_INVALID", { field: "duplicate_of" });
144
+ const duplicateTarget = exactActiveSource(input.duplicateOf);
145
+ if (!duplicateTarget.ok) return duplicateTarget;
146
+ const duplicateResolved = await resolveExistingRegularFileInsideVault(input.vault, input.duplicateOf);
147
+ if (!duplicateResolved.ok) return err("SOURCE_DISPOSITION_INVALID", { field: "duplicate_of", message: "canonical duplicate target not found", cause: duplicateResolved });
148
+ }
149
+ const sourceResolved = await resolveExistingRegularFileInsideVault(input.vault, input.rawPath);
150
+ if (!sourceResolved.ok) return err("FILE_NOT_FOUND", { path: input.rawPath, cause: sourceResolved });
151
+ const text = await readFile(sourceResolved.data, "utf8");
152
+ const bodyHash = bodySha256(text);
153
+ const completeHash = completeSha256(text);
154
+ const existing = await readSourceDispositions(input.vault);
155
+ if (!existing.ok) return existing;
156
+ if (input.status === "duplicate" && input.duplicateOf) {
157
+ const seen = /* @__PURE__ */ new Set([input.rawPath]);
158
+ let current = input.duplicateOf;
159
+ while (current) {
160
+ if (seen.has(current)) {
161
+ return err("SOURCE_DISPOSITION_INVALID", {
162
+ field: "duplicate_of",
163
+ message: "duplicate dispositions may not create a direct or transitive cycle"
164
+ });
165
+ }
166
+ seen.add(current);
167
+ current = latestDuplicateTarget(existing.data, current);
168
+ }
169
+ }
170
+ if (input.status === "reopened") {
171
+ const effective = effectiveSourceDisposition({ dispositions: existing.data, rawPath: input.rawPath, bodySha256: bodyHash, completeSha256: completeHash, today });
172
+ if (!effective.disposition || effective.lifecycle === "pending") return err("SOURCE_DISPOSITION_INVALID", { field: "status", message: "reopened requires an effective prior disposition" });
173
+ }
174
+ const occurredAt = input.now ?? (/* @__PURE__ */ new Date()).toISOString();
175
+ const operation_id = operationId("source-disposition", [input.rawPath, completeHash, input.status, reason, input.reviewAfter ?? "", input.duplicateOf ?? ""]);
176
+ const base = {
177
+ operation_id,
178
+ occurred_at: occurredAt,
179
+ raw_path: input.rawPath,
180
+ complete_sha256: completeHash,
181
+ status: input.status,
182
+ reason,
183
+ ...input.reviewAfter ? { review_after: input.reviewAfter } : {},
184
+ ...input.duplicateOf ? { duplicate_of: input.duplicateOf } : {}
185
+ };
186
+ return ok({ ...base, approval_token: encodeSourceActionApproval({ contract: "source-disposition/v1", ...base }), write: false });
187
+ }
188
+ async function applySourceDisposition(input) {
189
+ const decoded = decodeSourceActionApproval(input.approve);
190
+ if (!decoded.ok) return decoded;
191
+ const occurredAt = typeof decoded.data.occurred_at === "string" ? decoded.data.occurred_at : void 0;
192
+ const planned = await planSourceDisposition({ ...input, now: occurredAt });
193
+ if (!planned.ok) return planned;
194
+ if (planned.data.approval_token !== input.approve) return err("APPROVAL_INVALID", { message: "source disposition approval does not match live state" });
195
+ const event = await writeLogEvent(input.vault, {
196
+ schema: "skillwiki-log-event/v1",
197
+ operation_id: planned.data.operation_id,
198
+ occurred_at: planned.data.occurred_at,
199
+ host_id: input.hostId ?? "local",
200
+ actor: input.actor ?? "skillwiki-cli",
201
+ kind: "source-disposition",
202
+ target: input.rawPath,
203
+ note: input.reason,
204
+ metadata: {
205
+ status: input.status,
206
+ raw_path: input.rawPath,
207
+ complete_sha256: planned.data.complete_sha256,
208
+ reason: planned.data.reason,
209
+ ...input.reviewAfter ? { review_after: input.reviewAfter } : {},
210
+ ...input.duplicateOf ? { duplicate_of: input.duplicateOf } : {}
211
+ }
212
+ });
213
+ if (!event.ok) return event;
214
+ return ok({ event_path: event.data.path, operation_id: planned.data.operation_id, created: event.data.created });
215
+ }
216
+
217
+ // src/utils/source-lifecycle.ts
218
+ function portableDate(value) {
219
+ if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(value)) return null;
220
+ const parsed = /* @__PURE__ */ new Date(`${value}T00:00:00Z`);
221
+ return !Number.isNaN(parsed.getTime()) && parsed.toISOString().slice(0, 10) === value ? value : null;
222
+ }
223
+ function filenameDate(path) {
224
+ const match = basename(path).match(/^(\d{4}-\d{2}-\d{2})(?:-|$)/);
225
+ return match ? portableDate(match[1]) : null;
226
+ }
227
+ function captureDate(path, fm) {
228
+ const ingested = portableDate(fm.ingested);
229
+ if (ingested) return { captured: ingested, source: "ingested" };
230
+ const created = portableDate(fm.created);
231
+ if (created) return { captured: created, source: "created" };
232
+ const fromName = filenameDate(path);
233
+ if (fromName) return { captured: fromName, source: "filename" };
234
+ return { captured: null, source: "unavailable" };
235
+ }
236
+ function ageBucket(captured, today) {
237
+ if (!captured) return "unknown";
238
+ const days = Math.floor((Date.parse(`${today}T00:00:00Z`) - Date.parse(`${captured}T00:00:00Z`)) / 864e5);
239
+ if (!Number.isFinite(days)) return "unknown";
240
+ if (days <= 7) return "fresh";
241
+ if (days <= 30) return "aging";
242
+ if (days <= 90) return "stale";
243
+ return "old";
244
+ }
245
+ function storageStatus(path) {
246
+ if (path.startsWith("raw/archived/")) return "archived";
247
+ if (path.startsWith("raw/duplicates/")) return "duplicate";
248
+ if (path.startsWith("_archive/raw/")) return "legacy-archived";
249
+ return "active";
250
+ }
251
+ function supportedSource(path) {
252
+ return /^raw\/(articles|papers)\/.+\.md$/.test(path) || /^raw\/(archived|duplicates)\/(articles|papers)\/.+\.md$/.test(path) || /^_archive\/raw\/(articles|papers)\/.+\.md$/.test(path);
253
+ }
254
+ function channel(fm, schemaStatus) {
255
+ if (fm.ingested_by === "wiki-ingest") return "wiki-ingest";
256
+ if (fm.ingested_by === "proj-work") return "proj-work";
257
+ if (fm.ingested_by === "manual") return "manual";
258
+ if (schemaStatus === "legacy" && typeof fm.source === "string") return "web-clipper-legacy";
259
+ return "unknown";
260
+ }
261
+ function titleFor(path, fm) {
262
+ if (typeof fm.title === "string" && fm.title.trim()) return fm.title.trim();
263
+ return basename(path, ".md");
264
+ }
265
+ function classifySchema(fmResult) {
266
+ if (!fmResult.ok) {
267
+ return { fm: {}, status: "invalid", issues: ["frontmatter is invalid or incomplete"] };
268
+ }
269
+ const fm = fmResult.data;
270
+ const parsed = RawSourceSchema.safeParse(fm);
271
+ if (parsed.success) return { fm, status: "valid", issues: [] };
272
+ const issues = [];
273
+ if (typeof fm.source === "string" && typeof fm.source_url !== "string") {
274
+ issues.push("source_url missing; legacy source property found");
275
+ }
276
+ if (!portableDate(fm.ingested) && portableDate(fm.created)) issues.push("ingested missing");
277
+ if (Object.keys(fm).length === 0) issues.push("frontmatter missing");
278
+ for (const issue of parsed.error.issues.slice(0, 8)) {
279
+ const message = `${issue.path.join(".") || "frontmatter"}: ${issue.message}`;
280
+ if (!issues.includes(message)) issues.push(message);
281
+ }
282
+ const recognizedLegacy = typeof fm.source === "string" || portableDate(fm.created) !== null;
283
+ return { fm, status: recognizedLegacy ? "legacy" : "invalid", issues };
284
+ }
285
+ async function scanNonMarkdownSourceFiles(vault) {
286
+ const targets = [join(vault, "raw", "articles"), join(vault, "raw", "papers")];
287
+ const out = [];
288
+ async function walkDir(dir) {
289
+ let entries;
290
+ try {
291
+ entries = await readdir(dir, { withFileTypes: true });
292
+ } catch {
293
+ return;
294
+ }
295
+ for (const entry of entries) {
296
+ const fullPath = join(dir, entry.name);
297
+ if (entry.isDirectory()) {
298
+ await walkDir(fullPath);
299
+ } else if (entry.isFile() && !entry.name.endsWith(".md")) {
300
+ const rel = relative(vault, fullPath).split(sep).join("/");
301
+ out.push({ relPath: rel, absPath: fullPath });
302
+ }
303
+ }
304
+ }
305
+ for (const target of targets) {
306
+ await walkDir(target);
307
+ }
308
+ return out;
309
+ }
310
+ async function inventorySources(input) {
311
+ const scan = await scanVault(input.vault);
312
+ if (!scan.ok) return { exitCode: 9, error: scan };
313
+ const today = input.today ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
314
+ const legacyPages = scan.data.allMarkdown.filter((page) => page.relPath.startsWith("_archive/raw/"));
315
+ const pages = [...scan.data.raw, ...legacyPages].filter((page) => supportedSource(page.relPath));
316
+ const availablePaths = pages.map((page) => page.relPath);
317
+ const typedPaths = new Set(scan.data.typedKnowledge.map((page) => page.relPath));
318
+ const otherPages = scan.data.allMarkdown.filter(
319
+ (page) => !typedPaths.has(page.relPath) && !page.relPath.startsWith("raw/") && !page.relPath.startsWith("_archive/")
320
+ );
321
+ const diagnostics = [];
322
+ const logEvents = await readLogEvents(input.vault);
323
+ const relocationsResult = logEvents.ok ? projectSourceRelocations(logEvents.data) : logEvents;
324
+ const relocationProjection = buildSourceRelocationProjection(relocationsResult.ok ? relocationsResult.data : []);
325
+ const historicalByCurrent = /* @__PURE__ */ new Map();
326
+ for (const [historical, current] of relocationProjection) {
327
+ const paths = historicalByCurrent.get(current) ?? [];
328
+ paths.push(historical);
329
+ historicalByCurrent.set(current, paths);
330
+ }
331
+ for (const paths of historicalByCurrent.values()) paths.sort();
332
+ const refs = await buildSourceReferenceIndex({
333
+ typedPages: scan.data.typedKnowledge,
334
+ otherPages,
335
+ availableRawPaths: availablePaths,
336
+ relocationProjection
337
+ });
338
+ const dispositionsResult = logEvents.ok ? projectSourceDispositions(logEvents.data) : logEvents;
339
+ const dispositions = dispositionsResult.ok ? dispositionsResult.data : [];
340
+ const latestDispositionByPath = /* @__PURE__ */ new Map();
341
+ for (const disposition of dispositions) latestDispositionByPath.set(disposition.raw_path, disposition);
342
+ const dispositionReadError = dispositionsResult.ok ? null : dispositionsResult;
343
+ const items = [];
344
+ if (!relocationsResult.ok) {
345
+ diagnostics.push({ raw_path: "meta/log-events", code: "source_relocation_invalid", message: JSON.stringify(relocationsResult) });
346
+ }
347
+ for (const unresolved of refs.unresolved) {
348
+ diagnostics.push({
349
+ raw_path: unresolved.sourcePath,
350
+ code: "source_reference_unresolved",
351
+ message: `${unresolved.kind} reference does not resolve: ${unresolved.target}`
352
+ });
353
+ }
354
+ const occurredAt = input.now ?? (/* @__PURE__ */ new Date()).toISOString();
355
+ const hostId = input.hostId ?? "local";
356
+ const actor = input.actor ?? "skillwiki-cli";
357
+ for (const page of pages) {
358
+ let text;
359
+ try {
360
+ text = await readPage(page);
361
+ } catch (error) {
362
+ diagnostics.push({ raw_path: page.relPath, code: "source_unreadable", message: String(error) });
363
+ const rawPath = page.relPath.split(sep).join("/");
364
+ const reason = `source unreadable: ${String(error)}`;
365
+ const stage = "inventory";
366
+ const opId = operationId("source-skipped", [rawPath, reason, stage, ""]);
367
+ await writeLogEvent(input.vault, {
368
+ schema: "skillwiki-log-event/v1",
369
+ operation_id: opId,
370
+ occurred_at: occurredAt,
371
+ host_id: hostId,
372
+ actor,
373
+ kind: "source-skipped",
374
+ target: rawPath,
375
+ note: reason,
376
+ metadata: {
377
+ path: rawPath,
378
+ reason,
379
+ stage
380
+ }
381
+ });
382
+ continue;
383
+ }
384
+ const classification = classifySchema(extractFrontmatter(text));
385
+ const date = captureDate(page.relPath, classification.fm);
386
+ const referencedBy = refs.integratedBy.get(page.relPath) ?? [];
387
+ const elsewhere = refs.referencedElsewhereBy.get(page.relPath) ?? [];
388
+ const projectedDisposition = effectiveSourceDisposition({
389
+ dispositions: latestDispositionByPath.has(page.relPath) ? [latestDispositionByPath.get(page.relPath)] : [],
390
+ rawPath: page.relPath,
391
+ bodySha256: bodySha256(text),
392
+ completeSha256: completeSha256(text),
393
+ today
394
+ });
395
+ const sourceUrl = typeof classification.fm.source_url === "string" ? classification.fm.source_url : typeof classification.fm.source === "string" ? classification.fm.source : null;
396
+ if (classification.status === "invalid") {
397
+ diagnostics.push({ raw_path: page.relPath, code: "source_schema_invalid", message: classification.issues.join("; ") });
398
+ }
399
+ const storage = storageStatus(page.relPath);
400
+ items.push({
401
+ raw_path: page.relPath,
402
+ current_raw_path: page.relPath,
403
+ historical_raw_paths: historicalByCurrent.get(page.relPath) ?? [],
404
+ storage_status: storage,
405
+ title: titleFor(page.relPath, classification.fm),
406
+ source_url: sourceUrl,
407
+ captured: date.captured,
408
+ date_source: date.source,
409
+ age_bucket: ageBucket(date.captured, today),
410
+ capture_channel: channel(classification.fm, classification.status),
411
+ lifecycle_status: referencedBy.length > 0 ? "integrated" : storage === "duplicate" ? "duplicate" : projectedDisposition.lifecycle,
412
+ schema_status: classification.status,
413
+ schema_issues: classification.issues,
414
+ reference_count: referencedBy.length,
415
+ referenced_by: referencedBy,
416
+ referenced_elsewhere: elsewhere,
417
+ ...projectedDisposition.disposition ? { effective_disposition: projectedDisposition.disposition } : {},
418
+ ...projectedDisposition.identityMismatch ? { disposition_identity_mismatch: true } : {}
419
+ });
420
+ }
421
+ const nonMarkdownSources = await scanNonMarkdownSourceFiles(input.vault);
422
+ for (const file of nonMarkdownSources) {
423
+ let sha2562;
424
+ try {
425
+ const bytes = await readFile2(file.absPath);
426
+ sha2562 = createHash3("sha256").update(bytes).digest("hex");
427
+ } catch {
428
+ sha2562 = void 0;
429
+ }
430
+ const rawPath = file.relPath.split(sep).join("/");
431
+ const reason = "non-markdown file in source directory";
432
+ const stage = "inventory";
433
+ const opId = operationId("source-skipped", [rawPath, reason, stage, sha2562 ?? ""]);
434
+ await writeLogEvent(input.vault, {
435
+ schema: "skillwiki-log-event/v1",
436
+ operation_id: opId,
437
+ occurred_at: occurredAt,
438
+ host_id: hostId,
439
+ actor,
440
+ kind: "source-skipped",
441
+ target: rawPath,
442
+ note: reason,
443
+ metadata: {
444
+ path: rawPath,
445
+ reason,
446
+ stage,
447
+ ...sha2562 ? { sha256: sha2562 } : {}
448
+ }
449
+ });
450
+ }
451
+ if (dispositionReadError) {
452
+ diagnostics.push({ raw_path: "meta/log-events", code: "source_disposition_invalid", message: JSON.stringify(dispositionReadError) });
453
+ }
454
+ items.sort((a, b) => (b.captured ?? "").localeCompare(a.captured ?? "") || a.raw_path.localeCompare(b.raw_path));
455
+ return { exitCode: 0, output: { items, diagnostics } };
456
+ }
457
+ function sourceMatches(item, text) {
458
+ const terms = text.toLocaleLowerCase().split(/\s+/).filter(Boolean);
459
+ const haystack = `${item.title}
460
+ ${item.source_url ?? ""}
461
+ ${item.raw_path}`.toLocaleLowerCase();
462
+ return terms.every((term) => haystack.includes(term));
463
+ }
464
+
465
+ // src/utils/source-compile.ts
466
+ import { readFile as readFile3 } from "fs/promises";
467
+ var COMPILE_CLAIM_TTL_MS = 2 * 60 * 60 * 1e3;
468
+ var TYPED_PAGE_PATH = /^(entities|concepts|comparisons|queries)\/.+\.md$/;
469
+ var KINDS = /* @__PURE__ */ new Set([
470
+ "source-compile-claimed",
471
+ "source-compile-released",
472
+ "source-compile-published",
473
+ "source-review"
474
+ ]);
475
+ var REVIEW_STATUSES = /* @__PURE__ */ new Set(["open", "accepted", "needs-fix", "dismissed"]);
476
+ function shaField(value) {
477
+ return typeof value === "string" && /^[0-9a-f]{64}$/.test(value);
478
+ }
479
+ function exactActiveArticleOrPaper(path) {
480
+ const parsed = classifyRawPath(path);
481
+ if (!parsed.ok) return parsed;
482
+ if (parsed.data.storage !== "active" || parsed.data.category !== "articles" && parsed.data.category !== "papers") {
483
+ return err("SOURCE_COMPILE_TARGET_INVALID", { path, message: "compile turns require an exact active raw article or paper" });
484
+ }
485
+ return ok(true);
486
+ }
487
+ function requireInteractive(sessionKind) {
488
+ if (sessionKind !== "interactive") {
489
+ return err("SOURCE_COMPILE_SESSION_KIND", { session_kind: sessionKind ?? "unknown", message: "mutating compile/review commands require an interactive session" });
490
+ }
491
+ return ok(true);
492
+ }
493
+ function requireReason(reason) {
494
+ const trimmed = reason.normalize("NFC").trim();
495
+ if (!trimmed) return err("SOURCE_COMPILE_INVALID", { field: "reason", message: "reason is required" });
496
+ const sensitive = scanSensitiveContent(trimmed);
497
+ if (sensitive.length > 0) return err("SENSITIVE_CONTENT_DETECTED", { findings: sensitive });
498
+ return ok(trimmed);
499
+ }
500
+ function parseSourceCompileEvent(event) {
501
+ if (!KINDS.has(event.kind)) return ok(null);
502
+ const metadata = event.metadata;
503
+ if (typeof metadata.raw_path !== "string") return err("SOURCE_COMPILE_INVALID", { operation_id: event.operation_id });
504
+ if (event.kind === "source-compile-claimed") {
505
+ if (!shaField(metadata.complete_sha256) || typeof metadata.expires_at !== "string" || !shaField(metadata.turn_id) || typeof metadata.reason !== "string") {
506
+ return err("SOURCE_COMPILE_INVALID", { operation_id: event.operation_id });
507
+ }
508
+ }
509
+ if (event.kind === "source-compile-released" && (typeof metadata.reason !== "string" || metadata.complete_sha256 !== void 0 && !shaField(metadata.complete_sha256))) {
510
+ return err("SOURCE_COMPILE_INVALID", { operation_id: event.operation_id });
511
+ }
512
+ if (event.kind === "source-compile-published") {
513
+ if (!shaField(metadata.complete_sha256) || !shaField(metadata.turn_id) || !Array.isArray(metadata.typed_paths) || metadata.typed_paths.length === 0) {
514
+ return err("SOURCE_COMPILE_INVALID", { operation_id: event.operation_id });
515
+ }
516
+ }
517
+ if (event.kind === "source-review") {
518
+ if (!shaField(metadata.turn_id) || !REVIEW_STATUSES.has(metadata.status) || typeof metadata.reason !== "string") {
519
+ return err("SOURCE_COMPILE_INVALID", { operation_id: event.operation_id });
520
+ }
521
+ }
522
+ return ok({
523
+ kind: event.kind,
524
+ operation_id: event.operation_id,
525
+ occurred_at: event.occurred_at,
526
+ host_id: event.host_id,
527
+ actor: event.actor,
528
+ raw_path: metadata.raw_path,
529
+ ...typeof metadata.reason === "string" ? { reason: metadata.reason } : {},
530
+ ...shaField(metadata.complete_sha256) ? { complete_sha256: metadata.complete_sha256 } : {},
531
+ ...typeof metadata.expires_at === "string" ? { expires_at: metadata.expires_at } : {},
532
+ ...typeof metadata.session_kind === "string" ? { session_kind: metadata.session_kind } : {},
533
+ ...shaField(metadata.turn_id) ? { turn_id: metadata.turn_id } : {},
534
+ ...Array.isArray(metadata.typed_paths) ? { typed_paths: metadata.typed_paths.filter((p) => typeof p === "string") } : {},
535
+ ...REVIEW_STATUSES.has(metadata.status) ? { status: metadata.status } : {}
536
+ });
537
+ }
538
+ function projectSourceCompileEvents(events) {
539
+ const out = [];
540
+ for (const event of events) {
541
+ const parsed = parseSourceCompileEvent(event);
542
+ if (!parsed.ok) return parsed;
543
+ if (parsed.data) out.push(parsed.data);
544
+ }
545
+ return ok(out.sort((a, b) => a.occurred_at.localeCompare(b.occurred_at) || a.operation_id.localeCompare(b.operation_id)));
546
+ }
547
+ async function readSourceCompileEvents(vault) {
548
+ const events = await readLogEvents(vault);
549
+ if (!events.ok) return events;
550
+ return projectSourceCompileEvents(events.data);
551
+ }
552
+ function effectiveCompileState(input) {
553
+ const mine = input.events.filter((event) => event.raw_path === input.rawPath);
554
+ const latestWithSha = [...mine].reverse().find((event) => event.complete_sha256);
555
+ const identityMismatch = Boolean(latestWithSha?.complete_sha256 && latestWithSha.complete_sha256 !== input.completeSha256);
556
+ if (identityMismatch) return { status: "none", identityMismatch: true, claim: latestWithSha };
557
+ const latest = mine.at(-1);
558
+ if (!latest) return { status: "none", identityMismatch: false };
559
+ const claim = [...mine].reverse().find((event) => event.kind === "source-compile-claimed");
560
+ const published = [...mine].reverse().find((event) => event.kind === "source-compile-published");
561
+ const review = [...mine].reverse().find((event) => event.kind === "source-review");
562
+ if (latest.kind === "source-review") {
563
+ const status = latest.status === "accepted" || latest.status === "dismissed" ? "review-closed" : "review-open";
564
+ return { status, identityMismatch: false, claim, published, review };
565
+ }
566
+ if (latest.kind === "source-compile-published") {
567
+ return { status: "review-open", identityMismatch: false, claim, published, review };
568
+ }
569
+ if (latest.kind === "source-compile-released") {
570
+ return { status: "none", identityMismatch: false, claim, published, review };
571
+ }
572
+ if (latest.kind === "source-compile-claimed") {
573
+ const expired = latest.expires_at ? latest.expires_at < input.now : false;
574
+ return { status: expired ? "none" : "compiling", identityMismatch: false, claim: latest, published, review };
575
+ }
576
+ return { status: "none", identityMismatch: false, claim, published, review };
577
+ }
578
+ async function loadContext(input) {
579
+ const target = exactActiveArticleOrPaper(input.rawPath);
580
+ if (!target.ok) return target;
581
+ const resolved = await resolveExistingRegularFileInsideVault(input.vault, input.rawPath);
582
+ if (!resolved.ok) return err("FILE_NOT_FOUND", { path: input.rawPath, cause: resolved });
583
+ const text = await readFile3(resolved.data, "utf8");
584
+ const completeHash = completeSha256(text);
585
+ const events = await readSourceCompileEvents(input.vault);
586
+ if (!events.ok) return events;
587
+ const now = input.now ?? (/* @__PURE__ */ new Date()).toISOString();
588
+ const state = effectiveCompileState({ events: events.data, rawPath: input.rawPath, completeSha256: completeHash, now });
589
+ const inventory = await inventorySources({ vault: input.vault });
590
+ const item = inventory.output?.items.find((entry) => entry.raw_path === input.rawPath);
591
+ return ok({
592
+ text,
593
+ completeHash,
594
+ events: events.data,
595
+ state,
596
+ lifecycle: item?.lifecycle_status ?? "pending"
597
+ });
598
+ }
599
+ async function planSourceCompileClaim(input) {
600
+ const interactive = requireInteractive(input.sessionKind);
601
+ if (!interactive.ok) return interactive;
602
+ const reason = requireReason(input.reason);
603
+ if (!reason.ok) return reason;
604
+ const ctx = await loadContext(input);
605
+ if (!ctx.ok) return ctx;
606
+ if (ctx.data.lifecycle !== "pending") {
607
+ return err("SOURCE_COMPILE_NOT_PENDING", { path: input.rawPath, lifecycle: ctx.data.lifecycle });
608
+ }
609
+ const actor = input.actor ?? "skillwiki-cli";
610
+ const hostId = input.hostId ?? "local";
611
+ if (ctx.data.state.status === "compiling" && ctx.data.state.claim) {
612
+ const held = ctx.data.state.claim;
613
+ if (held.actor !== actor || held.host_id !== hostId) {
614
+ return err("SOURCE_COMPILE_CLAIM_HELD", { actor: held.actor, host_id: held.host_id, expires_at: held.expires_at });
615
+ }
616
+ }
617
+ const occurredAt = input.now ?? (/* @__PURE__ */ new Date()).toISOString();
618
+ const expiresAt = new Date(Date.parse(occurredAt) + COMPILE_CLAIM_TTL_MS).toISOString();
619
+ const turnId = operationId("source-compile-turn", [input.rawPath, ctx.data.completeHash, occurredAt]);
620
+ const operation_id = operationId("source-compile-claimed", [input.rawPath, ctx.data.completeHash, actor, hostId, reason.data, occurredAt]);
621
+ const base = {
622
+ operation_id,
623
+ occurred_at: occurredAt,
624
+ raw_path: input.rawPath,
625
+ complete_sha256: ctx.data.completeHash,
626
+ expires_at: expiresAt,
627
+ turn_id: turnId,
628
+ contract: "source-compile-claim/v1",
629
+ actor,
630
+ host_id: hostId,
631
+ reason: reason.data,
632
+ session_kind: input.sessionKind
633
+ };
634
+ return ok({
635
+ operation_id,
636
+ occurred_at: occurredAt,
637
+ raw_path: input.rawPath,
638
+ complete_sha256: ctx.data.completeHash,
639
+ expires_at: expiresAt,
640
+ turn_id: turnId,
641
+ approval_token: encodeSourceActionApproval(base),
642
+ write: false
643
+ });
644
+ }
645
+ async function applySourceCompileClaim(input) {
646
+ const decoded = decodeSourceActionApproval(input.approve);
647
+ if (!decoded.ok) return decoded;
648
+ const occurredAt = typeof decoded.data.occurred_at === "string" ? decoded.data.occurred_at : void 0;
649
+ const planned = await planSourceCompileClaim({ ...input, now: occurredAt });
650
+ if (!planned.ok) return planned;
651
+ if (planned.data.approval_token !== input.approve) return err("APPROVAL_INVALID", { message: "source compile claim approval does not match live state" });
652
+ const event = await writeLogEvent(input.vault, {
653
+ schema: "skillwiki-log-event/v1",
654
+ operation_id: planned.data.operation_id,
655
+ occurred_at: planned.data.occurred_at,
656
+ host_id: input.hostId ?? "local",
657
+ actor: input.actor ?? "skillwiki-cli",
658
+ kind: "source-compile-claimed",
659
+ target: input.rawPath,
660
+ note: input.reason,
661
+ metadata: {
662
+ raw_path: input.rawPath,
663
+ complete_sha256: planned.data.complete_sha256,
664
+ expires_at: planned.data.expires_at,
665
+ session_kind: input.sessionKind,
666
+ reason: input.reason.normalize("NFC").trim(),
667
+ turn_id: planned.data.turn_id
668
+ }
669
+ });
670
+ if (!event.ok) return event;
671
+ return ok({ event_path: event.data.path, operation_id: planned.data.operation_id, created: event.data.created });
672
+ }
673
+ async function planSourceCompileRelease(input) {
674
+ const interactive = requireInteractive(input.sessionKind);
675
+ if (!interactive.ok) return interactive;
676
+ const reason = requireReason(input.reason);
677
+ if (!reason.ok) return reason;
678
+ const ctx = await loadContext(input);
679
+ if (!ctx.ok) return ctx;
680
+ if (ctx.data.state.status !== "compiling" || !ctx.data.state.claim) {
681
+ return err("SOURCE_COMPILE_NOT_HELD", { path: input.rawPath });
682
+ }
683
+ const occurredAt = input.now ?? (/* @__PURE__ */ new Date()).toISOString();
684
+ const operation_id = operationId("source-compile-released", [input.rawPath, ctx.data.completeHash, reason.data, occurredAt]);
685
+ const base = {
686
+ operation_id,
687
+ occurred_at: occurredAt,
688
+ raw_path: input.rawPath,
689
+ complete_sha256: ctx.data.completeHash,
690
+ contract: "source-compile-release/v1",
691
+ reason: reason.data
692
+ };
693
+ return ok({
694
+ operation_id,
695
+ occurred_at: occurredAt,
696
+ raw_path: input.rawPath,
697
+ complete_sha256: ctx.data.completeHash,
698
+ approval_token: encodeSourceActionApproval(base),
699
+ write: false
700
+ });
701
+ }
702
+ async function applySourceCompileRelease(input) {
703
+ const decoded = decodeSourceActionApproval(input.approve);
704
+ if (!decoded.ok) return decoded;
705
+ const occurredAt = typeof decoded.data.occurred_at === "string" ? decoded.data.occurred_at : void 0;
706
+ const planned = await planSourceCompileRelease({ ...input, now: occurredAt });
707
+ if (!planned.ok) return planned;
708
+ if (planned.data.approval_token !== input.approve) return err("APPROVAL_INVALID", { message: "source compile release approval does not match live state" });
709
+ const event = await writeLogEvent(input.vault, {
710
+ schema: "skillwiki-log-event/v1",
711
+ operation_id: planned.data.operation_id,
712
+ occurred_at: planned.data.occurred_at,
713
+ host_id: input.hostId ?? "local",
714
+ actor: input.actor ?? "skillwiki-cli",
715
+ kind: "source-compile-released",
716
+ target: input.rawPath,
717
+ note: input.reason,
718
+ metadata: {
719
+ raw_path: input.rawPath,
720
+ complete_sha256: planned.data.complete_sha256,
721
+ reason: input.reason.normalize("NFC").trim()
722
+ }
723
+ });
724
+ if (!event.ok) return event;
725
+ return ok({ event_path: event.data.path, operation_id: planned.data.operation_id, created: event.data.created });
726
+ }
727
+ function normalizePages(pages) {
728
+ const cleaned = pages.map((page) => page.replaceAll("\\", "/").trim()).filter(Boolean);
729
+ if (cleaned.length === 0) return err("SOURCE_COMPILE_PAGES_INVALID", { message: "at least one typed page is required" });
730
+ for (const page of cleaned) {
731
+ if (!TYPED_PAGE_PATH.test(page)) return err("SOURCE_COMPILE_PAGES_INVALID", { path: page });
732
+ }
733
+ return ok([...new Set(cleaned)]);
734
+ }
735
+ async function planSourceCompilePublished(input) {
736
+ const interactive = requireInteractive(input.sessionKind);
737
+ if (!interactive.ok) return interactive;
738
+ const reason = requireReason(input.reason);
739
+ if (!reason.ok) return reason;
740
+ const pages = normalizePages(input.pages);
741
+ if (!pages.ok) return pages;
742
+ const ctx = await loadContext(input);
743
+ if (!ctx.ok) return ctx;
744
+ if (ctx.data.lifecycle !== "pending" && ctx.data.lifecycle !== "integrated") {
745
+ return err("SOURCE_COMPILE_NOT_PENDING", { path: input.rawPath, lifecycle: ctx.data.lifecycle });
746
+ }
747
+ const turnId = ctx.data.state.claim?.turn_id ?? operationId("source-compile-turn", [input.rawPath, ctx.data.completeHash, input.now ?? ""]);
748
+ const occurredAt = input.now ?? (/* @__PURE__ */ new Date()).toISOString();
749
+ const operation_id = operationId("source-compile-published", [input.rawPath, ctx.data.completeHash, pages.data.join(","), occurredAt]);
750
+ const review_operation_id = operationId("source-review-open", [input.rawPath, turnId, occurredAt]);
751
+ const base = {
752
+ operation_id,
753
+ review_operation_id,
754
+ occurred_at: occurredAt,
755
+ raw_path: input.rawPath,
756
+ complete_sha256: ctx.data.completeHash,
757
+ typed_paths: pages.data,
758
+ turn_id: turnId,
759
+ contract: "source-compile-published/v1",
760
+ reason: reason.data
761
+ };
762
+ return ok({
763
+ operation_id,
764
+ review_operation_id,
765
+ occurred_at: occurredAt,
766
+ raw_path: input.rawPath,
767
+ complete_sha256: ctx.data.completeHash,
768
+ typed_paths: pages.data,
769
+ turn_id: turnId,
770
+ approval_token: encodeSourceActionApproval(base),
771
+ write: false
772
+ });
773
+ }
774
+ async function applySourceCompilePublished(input) {
775
+ const decoded = decodeSourceActionApproval(input.approve);
776
+ if (!decoded.ok) return decoded;
777
+ const occurredAt = typeof decoded.data.occurred_at === "string" ? decoded.data.occurred_at : void 0;
778
+ const planned = await planSourceCompilePublished({ ...input, now: occurredAt });
779
+ if (!planned.ok) return planned;
780
+ if (planned.data.approval_token !== input.approve) return err("APPROVAL_INVALID", { message: "source compile published approval does not match live state" });
781
+ const hostId = input.hostId ?? "local";
782
+ const actor = input.actor ?? "skillwiki-cli";
783
+ const published = await writeLogEvent(input.vault, {
784
+ schema: "skillwiki-log-event/v1",
785
+ operation_id: planned.data.operation_id,
786
+ occurred_at: planned.data.occurred_at,
787
+ host_id: hostId,
788
+ actor,
789
+ kind: "source-compile-published",
790
+ target: input.rawPath,
791
+ note: input.reason,
792
+ metadata: {
793
+ raw_path: input.rawPath,
794
+ complete_sha256: planned.data.complete_sha256,
795
+ typed_paths: planned.data.typed_paths,
796
+ turn_id: planned.data.turn_id
797
+ }
798
+ });
799
+ if (!published.ok) return published;
800
+ const review = await writeLogEvent(input.vault, {
801
+ schema: "skillwiki-log-event/v1",
802
+ operation_id: planned.data.review_operation_id ?? operationId("source-review-open", [input.rawPath, planned.data.turn_id ?? "", planned.data.occurred_at]),
803
+ occurred_at: planned.data.occurred_at,
804
+ host_id: hostId,
805
+ actor,
806
+ kind: "source-review",
807
+ target: input.rawPath,
808
+ note: input.reason,
809
+ metadata: {
810
+ raw_path: input.rawPath,
811
+ complete_sha256: planned.data.complete_sha256,
812
+ turn_id: planned.data.turn_id,
813
+ status: "open",
814
+ typed_paths: planned.data.typed_paths,
815
+ reason: input.reason.normalize("NFC").trim()
816
+ }
817
+ });
818
+ if (!review.ok) return review;
819
+ return ok({
820
+ event_path: published.data.path,
821
+ review_event_path: review.data.path,
822
+ operation_id: planned.data.operation_id,
823
+ created: published.data.created
824
+ });
825
+ }
826
+ async function planSourceReview(input) {
827
+ const interactive = requireInteractive(input.sessionKind);
828
+ if (!interactive.ok) return interactive;
829
+ const reason = requireReason(input.reason);
830
+ if (!reason.ok) return reason;
831
+ if (!REVIEW_STATUSES.has(input.status)) return err("SOURCE_COMPILE_INVALID", { field: "status" });
832
+ const ctx = await loadContext(input);
833
+ if (!ctx.ok) return ctx;
834
+ const turnId = ctx.data.state.review?.turn_id ?? ctx.data.state.published?.turn_id ?? ctx.data.state.claim?.turn_id;
835
+ if (!turnId) return err("SOURCE_COMPILE_REVIEW_MISSING", { path: input.rawPath });
836
+ const occurredAt = input.now ?? (/* @__PURE__ */ new Date()).toISOString();
837
+ const operation_id = operationId("source-review", [input.rawPath, turnId, input.status, reason.data, occurredAt]);
838
+ const typedPaths = ctx.data.state.review?.typed_paths ?? ctx.data.state.published?.typed_paths;
839
+ const base = {
840
+ operation_id,
841
+ occurred_at: occurredAt,
842
+ raw_path: input.rawPath,
843
+ complete_sha256: ctx.data.completeHash,
844
+ turn_id: turnId,
845
+ status: input.status,
846
+ contract: "source-review/v1",
847
+ reason: reason.data
848
+ };
849
+ return ok({
850
+ operation_id,
851
+ occurred_at: occurredAt,
852
+ raw_path: input.rawPath,
853
+ complete_sha256: ctx.data.completeHash,
854
+ turn_id: turnId,
855
+ status: input.status,
856
+ typed_paths: typedPaths,
857
+ approval_token: encodeSourceActionApproval(base),
858
+ write: false
859
+ });
860
+ }
861
+ async function applySourceReview(input) {
862
+ const decoded = decodeSourceActionApproval(input.approve);
863
+ if (!decoded.ok) return decoded;
864
+ const occurredAt = typeof decoded.data.occurred_at === "string" ? decoded.data.occurred_at : void 0;
865
+ const planned = await planSourceReview({ ...input, now: occurredAt });
866
+ if (!planned.ok) return planned;
867
+ if (planned.data.approval_token !== input.approve) return err("APPROVAL_INVALID", { message: "source review approval does not match live state" });
868
+ const event = await writeLogEvent(input.vault, {
869
+ schema: "skillwiki-log-event/v1",
870
+ operation_id: planned.data.operation_id,
871
+ occurred_at: planned.data.occurred_at,
872
+ host_id: input.hostId ?? "local",
873
+ actor: input.actor ?? "skillwiki-cli",
874
+ kind: "source-review",
875
+ target: input.rawPath,
876
+ note: input.reason,
877
+ metadata: {
878
+ raw_path: input.rawPath,
879
+ complete_sha256: planned.data.complete_sha256,
880
+ turn_id: planned.data.turn_id,
881
+ status: input.status,
882
+ ...planned.data.typed_paths ? { typed_paths: planned.data.typed_paths } : {},
883
+ reason: input.reason.normalize("NFC").trim()
884
+ }
885
+ });
886
+ if (!event.ok) return event;
887
+ return ok({ event_path: event.data.path, operation_id: planned.data.operation_id, created: event.data.created });
888
+ }
889
+ async function listCompileStatus(input) {
890
+ const inventory = await inventorySources({ vault: input.vault });
891
+ if (!inventory.output) return err("SOURCE_COMPILE_INVENTORY_FAILED", { cause: inventory.error });
892
+ const events = await readSourceCompileEvents(input.vault);
893
+ if (!events.ok) return events;
894
+ const now = input.now ?? (/* @__PURE__ */ new Date()).toISOString();
895
+ const items = [];
896
+ for (const item of inventory.output.items) {
897
+ const textResult = await resolveExistingRegularFileInsideVault(input.vault, item.raw_path);
898
+ if (!textResult.ok) continue;
899
+ const text = await readFile3(textResult.data, "utf8");
900
+ const state = effectiveCompileState({
901
+ events: events.data,
902
+ rawPath: item.raw_path,
903
+ completeSha256: completeSha256(text),
904
+ now
905
+ });
906
+ if (state.status === "none" && !state.identityMismatch) continue;
907
+ if (state.status === "review-closed") continue;
908
+ items.push({ raw_path: item.raw_path, status: state.status, identityMismatch: state.identityMismatch });
909
+ }
910
+ return ok({ items });
911
+ }
912
+ async function listSourceReviews(input) {
913
+ const events = await readSourceCompileEvents(input.vault);
914
+ if (!events.ok) return events;
915
+ const now = input.now ?? (/* @__PURE__ */ new Date()).toISOString();
916
+ const byPath = /* @__PURE__ */ new Map();
917
+ for (const event of events.data) {
918
+ const list = byPath.get(event.raw_path) ?? [];
919
+ list.push(event);
920
+ byPath.set(event.raw_path, list);
921
+ }
922
+ const items = [];
923
+ for (const [rawPath, pathEvents] of byPath) {
924
+ const latestSha = [...pathEvents].reverse().find((event) => event.complete_sha256)?.complete_sha256 ?? "0".repeat(64);
925
+ const state = effectiveCompileState({ events: pathEvents, rawPath, completeSha256: latestSha, now });
926
+ if (state.status !== "review-open" || !state.review?.status) continue;
927
+ items.push({ raw_path: rawPath, status: state.review.status, typed_paths: state.review.typed_paths });
928
+ }
929
+ return ok({ items });
930
+ }
931
+
932
+ // src/commands/sources.ts
933
+ function validDate2(value) {
934
+ return /^\d{4}-\d{2}-\d{2}$/.test(value) && (/* @__PURE__ */ new Date(`${value}T00:00:00Z`)).toISOString().slice(0, 10) === value;
935
+ }
936
+ function validate(input) {
937
+ if (input.since && !validDate2(input.since)) return err("SOURCES_DATE_INVALID", { field: "since", value: input.since });
938
+ if (input.olderThan !== void 0 && (!Number.isInteger(input.olderThan) || input.olderThan < 0)) {
939
+ return err("SOURCES_AGE_INVALID", { field: "older-than", value: input.olderThan });
940
+ }
941
+ if (input.limit !== void 0 && (!Number.isInteger(input.limit) || input.limit < 1 || input.limit > 1e4)) {
942
+ return err("SOURCES_LIMIT_INVALID", { value: input.limit });
943
+ }
944
+ if (input.scope && !["articles", "papers", "all"].includes(input.scope)) {
945
+ return err("SOURCES_SCOPE_INVALID", { value: input.scope });
946
+ }
947
+ if (input.sort && !["newest", "oldest"].includes(input.sort)) {
948
+ return err("SOURCES_SORT_INVALID", { value: input.sort });
949
+ }
950
+ return ok(true);
951
+ }
952
+ function channelMatches(item, selected) {
953
+ if (!selected) return true;
954
+ const channel2 = item.capture_channel;
955
+ if (selected === "manual") return channel2 === "manual" || channel2 === "web-clipper-legacy";
956
+ return channel2 === selected;
957
+ }
958
+ function summary(items) {
959
+ return {
960
+ total: items.length,
961
+ pending: items.filter((item) => item.lifecycle_status === "pending").length,
962
+ integrated: items.filter((item) => item.lifecycle_status === "integrated").length,
963
+ valid: items.filter((item) => item.schema_status === "valid").length,
964
+ legacy: items.filter((item) => item.schema_status === "legacy").length,
965
+ invalid: items.filter((item) => item.schema_status === "invalid").length,
966
+ fresh: items.filter((item) => item.age_bucket === "fresh").length,
967
+ aging: items.filter((item) => item.age_bucket === "aging").length,
968
+ stale: items.filter((item) => item.age_bucket === "stale").length,
969
+ old: items.filter((item) => item.age_bucket === "old").length,
970
+ unknown_age: items.filter((item) => item.age_bucket === "unknown").length
971
+ };
972
+ }
973
+ async function runSourcesPending(input) {
974
+ const validated = validate(input);
975
+ if (!validated.ok) return { exitCode: ExitCode.USAGE, result: validated };
976
+ const inventory = await inventorySources({ vault: input.vault, today: input.today });
977
+ if (!inventory.output) {
978
+ return { exitCode: inventory.exitCode, result: inventory.error };
979
+ }
980
+ const today = input.today ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
981
+ const cutoff = input.olderThan === void 0 ? null : new Date(Date.parse(`${today}T00:00:00Z`) - input.olderThan * 864e5).toISOString().slice(0, 10);
982
+ let items = inventory.output.items.filter((item) => {
983
+ if (!input.includeIntegrated && item.lifecycle_status !== "pending") return false;
984
+ if (!input.includeArchived && item.storage_status === "archived") return false;
985
+ if (!input.includeDuplicates && item.storage_status === "duplicate") return false;
986
+ if (!input.includeLegacyArchived && item.storage_status === "legacy-archived") return false;
987
+ if (input.scope && input.scope !== "all" && !item.raw_path.startsWith(`raw/${input.scope}/`) && !item.raw_path.includes(`/${input.scope}/`)) return false;
988
+ if (input.since && (!item.captured || item.captured < input.since)) return false;
989
+ if (cutoff && (!item.captured || item.captured > cutoff)) return false;
990
+ if (input.match && !sourceMatches(item, input.match)) return false;
991
+ if (!channelMatches(item, input.ingestedBy)) return false;
992
+ return true;
993
+ });
994
+ if ((input.sort ?? "newest") === "oldest") {
995
+ items = items.sort((a, b) => (a.captured ?? "9999-99-99").localeCompare(b.captured ?? "9999-99-99") || a.raw_path.localeCompare(b.raw_path));
996
+ }
997
+ const counts = summary(items);
998
+ const unbounded = input.all === true;
999
+ items = items.slice(0, unbounded ? void 0 : input.limit ?? 50);
1000
+ const compileEvents = await readSourceCompileEvents(input.vault);
1001
+ if (compileEvents.ok && compileEvents.data.length > 0) {
1002
+ const now = `${today}T00:00:00.000Z`;
1003
+ items = await Promise.all(items.map(async (item) => {
1004
+ const resolved = await resolveExistingRegularFileInsideVault(input.vault, item.raw_path);
1005
+ if (!resolved.ok) return item;
1006
+ const text = await readFile4(resolved.data, "utf8");
1007
+ const state = effectiveCompileState({
1008
+ events: compileEvents.data,
1009
+ rawPath: item.raw_path,
1010
+ completeSha256: completeSha256(text),
1011
+ now
1012
+ });
1013
+ return {
1014
+ ...item,
1015
+ ...state.status !== "none" ? { compile_status: state.status } : {},
1016
+ ...state.review?.status ? { review_status: state.review.status } : {}
1017
+ };
1018
+ }));
1019
+ }
1020
+ const visibleRawPaths = new Set(items.map((item) => item.raw_path));
1021
+ const diagnostics = inventory.output.diagnostics.filter(
1022
+ (diagnostic) => visibleRawPaths.has(diagnostic.raw_path) || diagnostic.raw_path === "meta/log-events"
1023
+ );
1024
+ const humanHint = items.length === 0 ? "no pending sources" : items.map((item) => `${item.captured ?? "unknown-date"} ${item.schema_status} ${item.raw_path} \u2014 ${item.title}`).join("\n");
1025
+ return { exitCode: ExitCode.OK, result: ok({ items, summary: counts, diagnostics, humanHint }) };
1026
+ }
1027
+
1028
+ export {
1029
+ encodeSourceActionApproval,
1030
+ decodeSourceActionApproval,
1031
+ planSourceDisposition,
1032
+ applySourceDisposition,
1033
+ inventorySources,
1034
+ planSourceCompileClaim,
1035
+ applySourceCompileClaim,
1036
+ planSourceCompileRelease,
1037
+ applySourceCompileRelease,
1038
+ planSourceCompilePublished,
1039
+ applySourceCompilePublished,
1040
+ planSourceReview,
1041
+ applySourceReview,
1042
+ listCompileStatus,
1043
+ listSourceReviews,
1044
+ runSourcesPending
1045
+ };