skillwiki 0.10.22 → 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 (43) hide show
  1. package/dist/chunk-6ZDKTNLA.js +294 -0
  2. package/dist/chunk-EQPU2BPM.js +468 -0
  3. package/dist/{chunk-54JVBNWK.js → chunk-QBZYEEBD.js} +12 -295
  4. package/dist/chunk-QNTBNNEL.js +616 -0
  5. package/dist/{chunk-7YDWC6D3.js → chunk-SYKSL3JQ.js} +852 -699
  6. package/dist/cli.js +613 -146
  7. package/dist/{index-projection-Q6WL76SO.js → index-projection-FRWTZB5Y.js} +2 -1
  8. package/dist/skillwiki-mcp.js +4 -2
  9. package/dist/sources-L2SQV63E.js +10 -0
  10. package/package.json +1 -1
  11. package/skills/.claude-plugin/plugin.json +1 -1
  12. package/skills/.codex-plugin/plugin.json +1 -1
  13. package/skills/README.md +13 -2
  14. package/skills/agents/wiki-add-task.md +2 -1
  15. package/skills/agents/wiki-archive.md +8 -9
  16. package/skills/agents/wiki-audit.md +2 -1
  17. package/skills/agents/wiki-lint.md +2 -1
  18. package/skills/agents/wiki-query.md +3 -1
  19. package/skills/agents/wiki-reingest.md +9 -8
  20. package/skills/package.json +1 -1
  21. package/skills/skills/using-skillwiki/SKILL.md +19 -13
  22. package/skills/skills/wiki-add-task/SKILL.md +4 -3
  23. package/skills/skills/wiki-archive/SKILL.md +23 -18
  24. package/skills/skills/wiki-audit/SKILL.md +3 -1
  25. package/skills/skills/wiki-init/SKILL.md +12 -1
  26. package/skills/skills/wiki-lint/SKILL.md +2 -1
  27. package/skills/skills/wiki-query/SKILL.md +7 -0
  28. package/skills/skills/wiki-reingest/SKILL.md +12 -8
  29. package/skills/skills/wiki-remove/SKILL.md +22 -5
  30. package/skills/skills/wiki-sync/SKILL.md +2 -3
  31. package/skills/using-skillwiki/SKILL.md +19 -13
  32. package/skills/wiki-add-task/SKILL.md +4 -3
  33. package/skills/wiki-archive/SKILL.md +23 -18
  34. package/skills/wiki-audit/SKILL.md +3 -1
  35. package/skills/wiki-init/SKILL.md +12 -1
  36. package/skills/wiki-lint/SKILL.md +2 -1
  37. package/skills/wiki-query/SKILL.md +7 -0
  38. package/skills/wiki-reingest/SKILL.md +12 -8
  39. package/skills/wiki-remove/SKILL.md +22 -5
  40. package/skills/wiki-sync/SKILL.md +2 -3
  41. package/templates/SCHEMA.md +8 -5
  42. package/templates/web-clipper/llm-wiki-clippings.json +56 -0
  43. package/templates/web-clipper/readme.txt +16 -0
package/dist/cli.js CHANGED
@@ -9,16 +9,14 @@ import {
9
9
  acquireLock,
10
10
  acquireOwnedSyncLock,
11
11
  appendLastOp,
12
+ applyRawStructuralMove,
12
13
  assessSourceIdentity,
13
14
  buildDegradedReasons,
14
15
  buildRemoteObjectPath,
15
- canonicalEventJson,
16
16
  clearLastOp,
17
17
  configPath,
18
18
  evaluateDirtyVolumeGate,
19
19
  evaluateSatelliteRunHealth,
20
- eventPathFor,
21
- extractCitationMarkers,
22
20
  extractTaxonomy,
23
21
  findPlugin,
24
22
  fixPathTooLong,
@@ -30,11 +28,11 @@ import {
30
28
  mergeTaxonomyConflict,
31
29
  normalizeRemoteRoot,
32
30
  planAndMaybePruneRemoteObjects,
31
+ planRawStructuralMove,
33
32
  probeRemoteHealth,
34
33
  readCliPackageJson,
35
34
  readLastOp,
36
35
  readLock,
37
- readLogEvents,
38
36
  readSatelliteLatestRunFromText,
39
37
  reconcileTaxonomyDocument,
40
38
  releaseLock,
@@ -43,6 +41,7 @@ import {
43
41
  renderProjectIndex,
44
42
  resolveInitTimePath,
45
43
  resolveRuntimePath,
44
+ rewriteRawSourceReferences,
46
45
  runAudit,
47
46
  runConfigGet,
48
47
  runConfigList,
@@ -78,12 +77,11 @@ import {
78
77
  safeWritePage,
79
78
  satelliteLatestRunPath,
80
79
  scanConflictMarkerBlocksInText,
80
+ snapshotMaintainedPageState,
81
81
  snapshotterHealthChecks,
82
82
  taxonomyCommentForPage,
83
- upsertIndexEntry,
84
- validateLogEvent,
85
- writeLogEvent
86
- } from "./chunk-7YDWC6D3.js";
83
+ upsertIndexEntry
84
+ } from "./chunk-SYKSL3JQ.js";
87
85
  import {
88
86
  normalizeDistTag,
89
87
  readCache,
@@ -96,16 +94,10 @@ import {
96
94
  UNMANAGED_START,
97
95
  assertTargetInsideVault,
98
96
  atomicWriteText,
99
- extractFrontmatter,
100
97
  prepareTypedPage,
101
- readPage,
102
- redactSensitiveContent,
103
98
  renderRootIndex,
104
- scanSensitiveContent,
105
- scanVault,
106
- splitFrontmatter,
107
99
  writeRootIndexProjection
108
- } from "./chunk-54JVBNWK.js";
100
+ } from "./chunk-QBZYEEBD.js";
109
101
  import {
110
102
  runManagedWritePreflight,
111
103
  runManagedWriteTransaction
@@ -138,6 +130,38 @@ import {
138
130
  supersedeStaleReviewRequiredJournals,
139
131
  writeDotenv
140
132
  } from "./chunk-PQG26AGJ.js";
133
+ import {
134
+ applySourceDisposition,
135
+ decodeSourceActionApproval,
136
+ encodeSourceActionApproval,
137
+ inventorySources,
138
+ planSourceDisposition,
139
+ runSourcesPending
140
+ } from "./chunk-EQPU2BPM.js";
141
+ import {
142
+ authorizeRawOperation,
143
+ buildSourceReferenceIndex,
144
+ buildSourceRelocationProjection,
145
+ canonicalEventJson,
146
+ classifyRawPath,
147
+ eventPathFor,
148
+ extractCitationMarkers,
149
+ lifecycleDestination,
150
+ operationId,
151
+ readLogEvents,
152
+ readSourceRelocations,
153
+ resolveExistingRegularFileInsideVault,
154
+ validateLogEvent,
155
+ writeLogEvent
156
+ } from "./chunk-QNTBNNEL.js";
157
+ import {
158
+ extractFrontmatter,
159
+ readPage,
160
+ redactSensitiveContent,
161
+ scanSensitiveContent,
162
+ scanVault,
163
+ splitFrontmatter
164
+ } from "./chunk-6ZDKTNLA.js";
141
165
  import {
142
166
  ExitCode,
143
167
  MetaSchema,
@@ -150,7 +174,7 @@ import {
150
174
  } from "./chunk-C2DKFJFA.js";
151
175
 
152
176
  // src/cli.ts
153
- import { join as join38 } from "path";
177
+ import { join as join39 } from "path";
154
178
  import { Command } from "commander";
155
179
 
156
180
  // src/utils/output.ts
@@ -212,10 +236,10 @@ async function runHash(input) {
212
236
  const split = splitFrontmatter(text);
213
237
  if (!split.ok) return { exitCode: ExitCode.MISSING_CLOSING_DELIMITER, result: split };
214
238
  const bodyBytes = Buffer.from(split.data.body, "utf8");
215
- const sha256 = createHash("sha256").update(bodyBytes).digest("hex");
239
+ const sha2562 = createHash("sha256").update(bodyBytes).digest("hex");
216
240
  return {
217
241
  exitCode: ExitCode.OK,
218
- result: ok({ path: input.file, sha256, byte_count: bodyBytes.byteLength, humanHint: sha256 })
242
+ result: ok({ path: input.file, sha256: sha2562, byte_count: bodyBytes.byteLength, humanHint: sha2562 })
219
243
  };
220
244
  }
221
245
 
@@ -590,7 +614,7 @@ async function runLang(input) {
590
614
 
591
615
  // src/commands/init.ts
592
616
  import { mkdir as mkdir3, readFile as readFile3, readdir as readdir2, writeFile as writeFile2 } from "fs/promises";
593
- import { join as join5 } from "path";
617
+ import { join as join5, dirname as dirname3 } from "path";
594
618
  var DEFAULT_TAXONOMY = [
595
619
  "research",
596
620
  "comparison",
@@ -608,6 +632,12 @@ var VAULT_DIRS = [
608
632
  "raw/papers",
609
633
  "raw/transcripts",
610
634
  "raw/assets",
635
+ "raw/archived/articles",
636
+ "raw/archived/papers",
637
+ "raw/archived/transcripts",
638
+ "raw/duplicates/articles",
639
+ "raw/duplicates/papers",
640
+ "raw/duplicates/transcripts",
611
641
  "entities",
612
642
  "concepts",
613
643
  "comparisons",
@@ -620,6 +650,8 @@ var VAULT_DIRS = [
620
650
  var ATTACHMENT_FOLDER = "raw/assets";
621
651
  var NEW_FILE_FOLDER = "raw/transcripts";
622
652
  var TEMPLATE_FOLDER = "_Templates";
653
+ var WEB_CLIPPER_TEMPLATE_REL = `${TEMPLATE_FOLDER}/web-clipper/llm-wiki-clippings.json`;
654
+ var WEB_CLIPPER_README_REL = `${TEMPLATE_FOLDER}/web-clipper/readme.txt`;
623
655
  function extractDomainFromSchema(text) {
624
656
  const m = text.match(/^##\s+Domain\s*\n([\s\S]*?)(?=\n\n|\n##|\s*$)/m);
625
657
  if (!m) return "";
@@ -746,6 +778,23 @@ async function runInit(input) {
746
778
  return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: fileName, message: String(e) }) };
747
779
  }
748
780
  }
781
+ async function copyTemplateOrPreserve(fileName, sourceName) {
782
+ const destination = join5(target, fileName);
783
+ try {
784
+ await readFile3(destination);
785
+ preserved.push(fileName);
786
+ return void 0;
787
+ } catch {
788
+ }
789
+ try {
790
+ await mkdir3(dirname3(destination), { recursive: true });
791
+ await writeFile2(destination, await readFile3(join5(input.templates, sourceName)));
792
+ created.push(fileName);
793
+ return void 0;
794
+ } catch (e) {
795
+ return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: fileName, message: String(e) }) };
796
+ }
797
+ }
749
798
  const err1 = await writeOrPreserve("index.md", async () => {
750
799
  const tpl = await readFile3(join5(input.templates, "index.md"), "utf8");
751
800
  return tpl.replace("{{INIT_DATE}}", today);
@@ -773,6 +822,16 @@ async function runInit(input) {
773
822
  ].join("\n");
774
823
  });
775
824
  if (errTemplate) return errTemplate;
825
+ const errWebClipperTemplate = await copyTemplateOrPreserve(
826
+ WEB_CLIPPER_TEMPLATE_REL,
827
+ "web-clipper/llm-wiki-clippings.json"
828
+ );
829
+ if (errWebClipperTemplate) return errWebClipperTemplate;
830
+ const errWebClipperReadme = await copyTemplateOrPreserve(
831
+ WEB_CLIPPER_README_REL,
832
+ "web-clipper/readme.txt"
833
+ );
834
+ if (errWebClipperReadme) return errWebClipperReadme;
776
835
  const err22 = await writeOrPreserve("log.md", async () => {
777
836
  const tpl = await readFile3(join5(input.templates, "log.md"), "utf8");
778
837
  return tpl.replace(/\{\{INIT_DATE\}\}/g, today).replace("{{DOMAIN}}", domain).replace("{{WIKI_LANG}}", canonicalLang);
@@ -804,7 +863,9 @@ async function runInit(input) {
804
863
  `lang: ${canonicalLang}`,
805
864
  `created: ${created.length}, preserved: ${preserved.length}`,
806
865
  `discovered tags: ${discovered_tags}`,
807
- skipEnv ? "env: skipped" : `env: ${envWritten}`
866
+ skipEnv ? "env: skipped" : `env: ${envWritten}`,
867
+ `web clipper template: ${WEB_CLIPPER_TEMPLATE_REL}`,
868
+ "web clipper import: open Obsidian Web Clipper Settings and import the JSON in each browser profile"
808
869
  ].join("\n");
809
870
  if (created.length > 0) {
810
871
  appendLastOp(target, {
@@ -828,7 +889,11 @@ async function runInit(input) {
828
889
  imported_from_hermes: importedFromHermes,
829
890
  discovered_tags,
830
891
  humanHint,
831
- templates_created: created.includes(`${TEMPLATE_FOLDER}/tpl-ad-hoc-capture.md`)
892
+ templates_created: created.includes(`${TEMPLATE_FOLDER}/tpl-ad-hoc-capture.md`),
893
+ web_clipper_template_path: WEB_CLIPPER_TEMPLATE_REL,
894
+ web_clipper_readme_path: WEB_CLIPPER_README_REL,
895
+ web_clipper_template_created: created.includes(WEB_CLIPPER_TEMPLATE_REL),
896
+ web_clipper_template_preserved: preserved.includes(WEB_CLIPPER_TEMPLATE_REL)
832
897
  })
833
898
  };
834
899
  }
@@ -1198,21 +1263,21 @@ function rollbackPath(vault, path, stage2, stage3) {
1198
1263
  function hasMarkers(path, text) {
1199
1264
  const lines = text.split(/\r?\n/);
1200
1265
  let open2 = false;
1201
- let sep3 = false;
1266
+ let sep4 = false;
1202
1267
  for (const line of lines) {
1203
1268
  if (line.startsWith("<<<<<<< ")) {
1204
1269
  open2 = true;
1205
- sep3 = false;
1270
+ sep4 = false;
1206
1271
  continue;
1207
1272
  }
1208
1273
  if (line === "=======" && open2) {
1209
- sep3 = true;
1274
+ sep4 = true;
1210
1275
  continue;
1211
1276
  }
1212
- if (line.startsWith(">>>>>>> ") && open2 && sep3) return true;
1277
+ if (line.startsWith(">>>>>>> ") && open2 && sep4) return true;
1213
1278
  if (line.startsWith(">>>>>>> ")) {
1214
1279
  open2 = false;
1215
- sep3 = false;
1280
+ sep4 = false;
1216
1281
  }
1217
1282
  }
1218
1283
  return false;
@@ -1489,16 +1554,6 @@ async function runLogMaterialize(input) {
1489
1554
  // src/commands/log-migrate-legacy.ts
1490
1555
  import { readFileSync as readFileSync6 } from "fs";
1491
1556
  import { join as join10 } from "path";
1492
-
1493
- // src/utils/operation-id.ts
1494
- import { createHash as createHash2 } from "crypto";
1495
- function operationId(namespace, parts) {
1496
- const hash = createHash2("sha256").update(namespace).update("\0");
1497
- for (const part of parts) hash.update(part).update("\0");
1498
- return hash.digest("hex");
1499
- }
1500
-
1501
- // src/commands/log-migrate-legacy.ts
1502
1557
  var BLOCK_RE = /^## \[(\d{4}-\d{2}-\d{2})\][^\n]*$/gm;
1503
1558
  var PUBLISH_RE = /<!--\s*skillwiki-(?:page-publish|project-page-publish):([a-f0-9]{64})\s*-->/i;
1504
1559
  function splitLegacyBlocks(text) {
@@ -2797,7 +2852,7 @@ function selfCheckReport(report) {
2797
2852
  if (lint.summary.info !== sumBySeverity("info")) {
2798
2853
  errors.push(`lint.summary.info=${lint.summary.info} but info bucket counts sum to ${sumBySeverity("info")}`);
2799
2854
  }
2800
- for (const component of ["doctor", "lint", "vault_sync", "query_readiness", "source_freshness"]) {
2855
+ for (const component of ["doctor", "lint", "vault_sync", "query_readiness", "source_freshness", "source_lifecycle"]) {
2801
2856
  if (!report.coverage[component]) errors.push(`missing coverage entry for ${component}`);
2802
2857
  }
2803
2858
  for (const flag of report.risk_flags) {
@@ -2817,6 +2872,7 @@ function buildHumanHint(report) {
2817
2872
  }
2818
2873
  lines.push(`vault_sync: ${report.components.vault_sync.summary.pass} pass, ${report.components.vault_sync.summary.warn} warn, ${report.components.vault_sync.summary.error} error`);
2819
2874
  lines.push(`query_readiness: ${report.components.query_readiness.status} (${report.components.query_readiness.summary.score}/100)`);
2875
+ lines.push(`source_lifecycle: ${report.components.source_lifecycle.summary.pending} pending, ${report.components.source_lifecycle.summary.deferred} deferred, ${report.components.source_lifecycle.summary.reviewed_no_op} reviewed-no-op`);
2820
2876
  if (report.risk_flags.length > 0) {
2821
2877
  lines.push("risk flags:");
2822
2878
  for (const flag of report.risk_flags) {
@@ -2876,7 +2932,8 @@ function buildIncompleteReport(input, syncMode) {
2876
2932
  lint: { state: "unknown", status: "unknown", reason: "lint summary still running" },
2877
2933
  vault_sync: vaultSyncCoverage,
2878
2934
  query_readiness: { state: "unknown", status: "unknown", reason: "health run not complete" },
2879
- source_freshness: { state: "unknown", status: "unknown", reason: "health run not complete" }
2935
+ source_freshness: { state: "unknown", status: "unknown", reason: "health run not complete" },
2936
+ source_lifecycle: { state: "unknown", status: "unknown", reason: "health run not complete" }
2880
2937
  },
2881
2938
  components: {
2882
2939
  doctor: {
@@ -2904,6 +2961,11 @@ function buildIncompleteReport(input, syncMode) {
2904
2961
  status: "unknown",
2905
2962
  blocking: false,
2906
2963
  summary: { stale_pages: 0, file_source_url: 0, stale_sections: 0 }
2964
+ },
2965
+ source_lifecycle: {
2966
+ status: "unknown",
2967
+ blocking: false,
2968
+ summary: { pending: 0, deferred: 0, reviewed_no_op: 0, archived_raw: 0, preserved_duplicates: 0, legacy_archive_raw: 0, legacy_schema: 0, invalid_schema: 0, oldest_pending: null, fresh_pending_examples: [] }
2907
2969
  }
2908
2970
  },
2909
2971
  risk_flags: [],
@@ -2958,6 +3020,26 @@ async function runHealth(input) {
2958
3020
  stale_sections: bucketCount(lint.result.data.buckets, "stale_sections")
2959
3021
  }
2960
3022
  };
3023
+ const sourceInventory = await inventorySources({ vault: input.vault });
3024
+ const sourceItems = sourceInventory.output?.items ?? [];
3025
+ const pendingItems = sourceItems.filter((item) => item.lifecycle_status === "pending" && item.storage_status === "active");
3026
+ const pendingDates = pendingItems.map((item) => item.captured).filter((value) => value !== null).sort();
3027
+ const sourceLifecycle = {
3028
+ status: pendingItems.length > 0 ? "info" : "pass",
3029
+ blocking: false,
3030
+ summary: {
3031
+ pending: pendingItems.length,
3032
+ deferred: sourceItems.filter((item) => item.lifecycle_status === "deferred").length,
3033
+ reviewed_no_op: sourceItems.filter((item) => item.lifecycle_status === "reviewed-no-op").length,
3034
+ archived_raw: sourceItems.filter((item) => item.storage_status === "archived").length,
3035
+ preserved_duplicates: sourceItems.filter((item) => item.storage_status === "duplicate").length,
3036
+ legacy_archive_raw: sourceItems.filter((item) => item.storage_status === "legacy-archived").length,
3037
+ legacy_schema: sourceItems.filter((item) => item.schema_status === "legacy").length,
3038
+ invalid_schema: sourceItems.filter((item) => item.schema_status === "invalid").length,
3039
+ oldest_pending: pendingDates[0] ?? null,
3040
+ fresh_pending_examples: pendingItems.filter((item) => item.age_bucket === "fresh").slice(0, 3).map((item) => item.raw_path)
3041
+ }
3042
+ };
2961
3043
  const doctorComponent = {
2962
3044
  status: doctorStatus,
2963
3045
  blocking: false,
@@ -3002,14 +3084,16 @@ async function runHealth(input) {
3002
3084
  lint: { state: "checked", status: lintComponent.status },
3003
3085
  vault_sync: vaultSyncCoverage,
3004
3086
  query_readiness: { state: "checked", status: queryReadiness.status },
3005
- source_freshness: { state: "checked", status: sourceFreshness.status }
3087
+ source_freshness: { state: "checked", status: sourceFreshness.status },
3088
+ source_lifecycle: { state: "checked", status: sourceLifecycle.status }
3006
3089
  },
3007
3090
  components: {
3008
3091
  doctor: doctorComponent,
3009
3092
  lint: lintComponent,
3010
3093
  vault_sync: vaultSync,
3011
3094
  query_readiness: queryReadiness,
3012
- source_freshness: sourceFreshness
3095
+ source_freshness: sourceFreshness,
3096
+ source_lifecycle: sourceLifecycle
3013
3097
  },
3014
3098
  risk_flags: riskFlags,
3015
3099
  details_included: false,
@@ -3056,7 +3140,7 @@ async function runHealth(input) {
3056
3140
  }
3057
3141
 
3058
3142
  // src/commands/archive.ts
3059
- import { rename as rename2, mkdir as mkdir6, readFile as readFile7, writeFile as writeFile5 } from "fs/promises";
3143
+ import { rename as rename2, mkdir as mkdir6, readFile as readFile7 } from "fs/promises";
3060
3144
  import { join as join18, dirname as dirname5 } from "path";
3061
3145
 
3062
3146
  // src/utils/delete-intent.ts
@@ -3127,18 +3211,21 @@ async function runArchive(input) {
3127
3211
  let relPath = lookup(scan.data.typedKnowledge);
3128
3212
  let isRaw = false;
3129
3213
  if (!relPath) {
3130
- relPath = lookup(scan.data.raw);
3214
+ relPath = input.page.includes("/") ? lookup(scan.data.raw) : void 0;
3131
3215
  isRaw = relPath != null;
3132
3216
  }
3133
3217
  if (!relPath) return { exitCode: ExitCode.ARCHIVE_TARGET_NOT_FOUND, result: err("ARCHIVE_TARGET_NOT_FOUND", { page: input.page }) };
3134
3218
  if (relPath.startsWith("_archive/")) return { exitCode: ExitCode.ARCHIVE_ALREADY_ARCHIVED, result: err("ARCHIVE_ALREADY_ARCHIVED", { page: relPath }) };
3135
3219
  const slug = relPath.replace(/\.md$/, "").split("/").pop();
3136
- const archivePath = join18("_archive", relPath).replace(/\\/g, "/");
3220
+ const rawArchive = isRaw ? lifecycleDestination(relPath, "archive") : void 0;
3221
+ if (rawArchive && !rawArchive.ok) return { exitCode: ExitCode.USAGE, result: rawArchive };
3222
+ const archivePath = isRaw ? rawArchive.data : join18("_archive", relPath).replace(/\\/g, "/");
3137
3223
  const remoteRoot = normalizeRemoteRoot(input.remote);
3138
3224
  const remoteObjectPath = buildRemoteObjectPath(remoteRoot, relPath);
3139
3225
  let cascade;
3140
- if (input.cascade) {
3226
+ if (input.cascade || isRaw) {
3141
3227
  const wikilinkRefs = [];
3228
+ const citationRefs = [];
3142
3229
  const sourceArrayRefs = [];
3143
3230
  for (const page of scan.data.typedKnowledge) {
3144
3231
  if (page.relPath === relPath) continue;
@@ -3147,12 +3234,15 @@ async function runArchive(input) {
3147
3234
  if (!split.ok) continue;
3148
3235
  const wl = countWikilinks(split.data.body, slug);
3149
3236
  if (wl > 0) wikilinkRefs.push({ page: page.relPath, count: wl });
3237
+ const rewritten = isRaw ? rewriteRawSourceReferences(text, relPath, archivePath) : void 0;
3238
+ const citationCount = rewritten?.bodyCitationCount ?? 0;
3239
+ if (citationCount > 0) citationRefs.push({ page: page.relPath, count: citationCount });
3150
3240
  const fm = extractFrontmatter(text);
3151
3241
  if (!fm.ok) continue;
3152
3242
  const sources = fm.data.sources;
3153
- if (Array.isArray(sources) && sources.includes(relPath)) {
3243
+ if (Array.isArray(sources) && (isRaw ? rewritten.sourcesBefore.some((source, index) => source !== rewritten.sourcesAfter[index]) : sources.includes(relPath))) {
3154
3244
  const before = sources.filter((s) => typeof s === "string");
3155
- const after = before.filter((s) => s !== relPath);
3245
+ const after = isRaw ? rewritten.sourcesAfter : before.filter((s) => s !== relPath);
3156
3246
  sourceArrayRefs.push({ page: page.relPath, sources_before: before, sources_after: after });
3157
3247
  }
3158
3248
  }
@@ -3167,9 +3257,23 @@ async function runArchive(input) {
3167
3257
  if (e instanceof Error && "code" in e && e.code !== "ENOENT") throw e;
3168
3258
  }
3169
3259
  }
3170
- cascade = { wikilink_refs: wikilinkRefs, index_refs: indexRefs, source_array_refs: sourceArrayRefs };
3171
- }
3172
- if (input.cascade && !input.apply) {
3260
+ cascade = { wikilink_refs: wikilinkRefs, citation_refs: citationRefs, index_refs: indexRefs, source_array_refs: sourceArrayRefs };
3261
+ }
3262
+ let rawPlan;
3263
+ if (isRaw) {
3264
+ rawPlan = await planRawStructuralMove({ vault: input.vault, operation: "archive", source: relPath, destination: archivePath });
3265
+ if (!rawPlan.ok) return { exitCode: ExitCode.WRITE_FAILED, result: rawPlan };
3266
+ }
3267
+ const citationState = isRaw ? await snapshotMaintainedPageState(scan.data.allMarkdown) : [];
3268
+ const rawApprovalToken = rawPlan?.ok ? operationId("raw-archive-approval", [
3269
+ rawPlan.data.operation_id,
3270
+ rawPlan.data.approval_token,
3271
+ rawPlan.data.source_sha256,
3272
+ archivePath,
3273
+ ...citationState
3274
+ ]) : void 0;
3275
+ const rawStructuralApproval = rawPlan?.ok ? rawPlan.data.approval_token : void 0;
3276
+ if ((input.cascade || isRaw) && !input.apply) {
3173
3277
  const summary = `DRY-RUN \u2014 would archive ${relPath}; ${cascade.wikilink_refs.length} wikilink ref(s), ${cascade.index_refs.length} index ref(s), ${cascade.source_array_refs.length} source array ref(s).`;
3174
3278
  return {
3175
3279
  exitCode: ExitCode.OK,
@@ -3178,37 +3282,73 @@ async function runArchive(input) {
3178
3282
  archived_to: archivePath,
3179
3283
  index_updated: false,
3180
3284
  applied: false,
3285
+ ...rawApprovalToken ? { approval_token: rawApprovalToken } : {},
3181
3286
  cascade,
3182
3287
  ...remoteObjectPath ? { remote: { plannedDeletes: [remoteObjectPath], deleted: [] } } : {},
3183
3288
  humanHint: summary + (remoteObjectPath ? ` (remote planned ${input.remoteDelete ? "delete" : "preview"}: ${remoteObjectPath})` : "")
3184
3289
  })
3185
3290
  };
3186
3291
  }
3187
- if (input.cascade && input.apply && cascade) {
3292
+ if (isRaw) {
3293
+ if (!input.approve || !rawStructuralApproval || input.approve !== rawApprovalToken) {
3294
+ return { exitCode: ExitCode.USAGE, result: err("APPROVAL_INVALID", { message: "raw archive requires the live dry-run --approve token" }) };
3295
+ }
3296
+ const moved = await applyRawStructuralMove({
3297
+ vault: input.vault,
3298
+ operation: "archive",
3299
+ source: relPath,
3300
+ destination: archivePath,
3301
+ approve: rawStructuralApproval,
3302
+ command: "skillwiki archive",
3303
+ citationChanges: [.../* @__PURE__ */ new Set([...cascade?.source_array_refs.map((ref) => ref.page) ?? [], ...cascade?.citation_refs.map((ref) => ref.page) ?? []])]
3304
+ });
3305
+ if (!moved.ok) return { exitCode: ExitCode.WRITE_FAILED, result: moved };
3306
+ }
3307
+ if ((input.cascade || isRaw) && input.apply && cascade) {
3188
3308
  for (const ref of cascade.source_array_refs) {
3189
3309
  const absPath = join18(input.vault, ref.page);
3190
3310
  const text = await readFile7(absPath, "utf8");
3191
- const split = splitFrontmatter(text);
3192
- if (!split.ok) continue;
3193
- const before = split.data.rawFrontmatter;
3194
- const newSourcesYaml = ref.sources_after.length === 0 ? "sources: []" : "sources:\n" + ref.sources_after.map((s) => ` - ${s}`).join("\n");
3195
- const fmRewritten = before.replace(
3196
- /^sources:\s*(?:\[[^\]]*\]|(?:\r?\n(?:\s*-\s.*))+)/m,
3197
- newSourcesYaml
3198
- );
3199
- if (fmRewritten === before) continue;
3200
3311
  if (!arraysEqual(ref.sources_after, ref.sources_before)) {
3201
- await writeFile5(absPath, `---
3312
+ let updated;
3313
+ if (isRaw) {
3314
+ updated = rewriteRawSourceReferences(text, relPath, archivePath).text;
3315
+ } else {
3316
+ const split = splitFrontmatter(text);
3317
+ if (!split.ok) continue;
3318
+ const newSourcesYaml = ref.sources_after.length === 0 ? "sources: []" : "sources:\n" + ref.sources_after.map((s) => ` - ${s}`).join("\n");
3319
+ const fmRewritten = split.data.rawFrontmatter.replace(
3320
+ /^sources:\s*(?:\[[^\]]*\]|(?:\r?\n(?:\s*-\s.*))+)/m,
3321
+ newSourcesYaml
3322
+ );
3323
+ if (fmRewritten === split.data.rawFrontmatter) continue;
3324
+ updated = `---
3202
3325
  ${fmRewritten}
3203
- ---${split.data.body}`, "utf8");
3326
+ ---${split.data.body}`;
3327
+ }
3328
+ const write = await safeWritePage(absPath, updated);
3329
+ if (!write.ok) return { exitCode: ExitCode.WRITE_FAILED, result: write };
3330
+ }
3331
+ }
3332
+ if (isRaw) {
3333
+ for (const ref of cascade.citation_refs) {
3334
+ if (cascade.source_array_refs.some((sourceRef) => sourceRef.page === ref.page)) continue;
3335
+ const absPath = join18(input.vault, ref.page);
3336
+ const original = await readFile7(absPath, "utf8");
3337
+ const updated = rewriteRawSourceReferences(original, relPath, archivePath).text;
3338
+ if (updated !== original) {
3339
+ const write = await safeWritePage(absPath, updated);
3340
+ if (!write.ok) return { exitCode: ExitCode.WRITE_FAILED, result: write };
3341
+ }
3204
3342
  }
3205
3343
  }
3206
3344
  }
3207
- await mkdir6(dirname5(join18(input.vault, archivePath)), { recursive: true });
3208
- await rename2(join18(input.vault, relPath), join18(input.vault, archivePath));
3345
+ if (!isRaw) {
3346
+ await mkdir6(dirname5(join18(input.vault, archivePath)), { recursive: true });
3347
+ await rename2(join18(input.vault, relPath), join18(input.vault, archivePath));
3348
+ }
3209
3349
  let indexUpdated = false;
3210
3350
  if (!isRaw) {
3211
- const { renderRootIndex: renderRootIndex2, writeRootIndexProjection: writeRootIndexProjection2 } = await import("./index-projection-Q6WL76SO.js");
3351
+ const { renderRootIndex: renderRootIndex2, writeRootIndexProjection: writeRootIndexProjection2 } = await import("./index-projection-FRWTZB5Y.js");
3212
3352
  const before = await readFile7(join18(input.vault, "index.md"), "utf8").catch(() => "");
3213
3353
  const fullTarget = relPath.replace(/\.md$/, "");
3214
3354
  const bare = fullTarget.split("/").pop() ?? fullTarget;
@@ -3241,7 +3381,7 @@ ${fmRewritten}
3241
3381
  }
3242
3382
  remote = pruned.data;
3243
3383
  }
3244
- const applied = input.cascade ? true : void 0;
3384
+ const applied = input.cascade || isRaw ? true : void 0;
3245
3385
  const cascadeNote = input.cascade ? ` (cascade: ${cascade.source_array_refs.length} src arrays updated, ${cascade.wikilink_refs.length} wikilinks reported)` : "";
3246
3386
  const remoteNote = remote ? ` (remote ${input.remoteDelete ? `deleted ${remote.deleted.length}` : `planned ${remote.plannedDeletes.length}`})` : "";
3247
3387
  return {
@@ -3298,6 +3438,15 @@ async function runRemove(input) {
3298
3438
  result: err("FILE_NOT_FOUND", { page: input.page })
3299
3439
  };
3300
3440
  }
3441
+ if (relPath.startsWith("raw/")) {
3442
+ return {
3443
+ exitCode: ExitCode.USAGE,
3444
+ result: err("RAW_DISPOSAL_REQUIRES_EXACT_TARGET_APPROVAL", {
3445
+ path: relPath,
3446
+ message: "ordinary remove never deletes raw evidence; use sources dispose with an exact target and approved preview"
3447
+ })
3448
+ };
3449
+ }
3301
3450
  if (relPath.startsWith("_archive/")) {
3302
3451
  return {
3303
3452
  exitCode: ExitCode.USAGE,
@@ -3317,10 +3466,10 @@ async function runRemove(input) {
3317
3466
  const tombstonePath = await writeDeleteIntent(input.vault, intent);
3318
3467
  await unlink2(join19(input.vault, relPath));
3319
3468
  if (relPath.endsWith(".md") && !relPath.startsWith("raw/")) {
3320
- const { readFile: readFile18 } = await import("fs/promises");
3469
+ const { readFile: readFile20 } = await import("fs/promises");
3321
3470
  const { join: pathJoin } = await import("path");
3322
- const { renderRootIndex: renderRootIndex2, writeRootIndexProjection: writeRootIndexProjection2 } = await import("./index-projection-Q6WL76SO.js");
3323
- const before = await readFile18(pathJoin(input.vault, "index.md"), "utf8").catch(() => "");
3471
+ const { renderRootIndex: renderRootIndex2, writeRootIndexProjection: writeRootIndexProjection2 } = await import("./index-projection-FRWTZB5Y.js");
3472
+ const before = await readFile20(pathJoin(input.vault, "index.md"), "utf8").catch(() => "");
3324
3473
  const fullTarget = relPath.replace(/\.md$/, "");
3325
3474
  const bare = fullTarget.split("/").pop() ?? fullTarget;
3326
3475
  const hadIndexEntry = before.includes(`[[${fullTarget}]]`) || before.includes(`[[${bare}]]`);
@@ -3358,7 +3507,7 @@ async function runRemove(input) {
3358
3507
  }
3359
3508
 
3360
3509
  // src/commands/drift.ts
3361
- import { createHash as createHash3 } from "crypto";
3510
+ import { createHash as createHash2 } from "crypto";
3362
3511
 
3363
3512
  // src/utils/fetch.ts
3364
3513
  async function controlledFetch(url, opts) {
@@ -3437,7 +3586,7 @@ async function runDrift(input) {
3437
3586
  });
3438
3587
  continue;
3439
3588
  }
3440
- const currentHash = createHash3("sha256").update(Buffer.from(resp.data.body, "utf8")).digest("hex");
3589
+ const currentHash = createHash2("sha256").update(Buffer.from(resp.data.body, "utf8")).digest("hex");
3441
3590
  const identity = assessSourceIdentity({
3442
3591
  rawPath: raw.relPath,
3443
3592
  sourceUrl,
@@ -3455,29 +3604,13 @@ async function runDrift(input) {
3455
3604
  continue;
3456
3605
  }
3457
3606
  const drifted2 = currentHash !== storedHash;
3458
- if (drifted2 && input.apply) {
3459
- const newFm = rawFrontmatter.replace(/^sha256:\s*[a-f0-9]+$/m, `sha256: ${currentHash}`);
3460
- const newText = `---
3461
- ${newFm}
3462
- ---
3463
- ${body}`;
3464
- await safeWritePage(raw.absPath, newText);
3465
- results.push({
3466
- raw_path: raw.relPath,
3467
- source_url: sourceUrl,
3468
- stored_sha256: storedHash,
3469
- current_sha256: currentHash,
3470
- status: "updated"
3471
- });
3472
- } else {
3473
- results.push({
3474
- raw_path: raw.relPath,
3475
- source_url: sourceUrl,
3476
- stored_sha256: storedHash,
3477
- current_sha256: currentHash,
3478
- status: drifted2 ? "drifted" : "unchanged"
3479
- });
3480
- }
3607
+ results.push({
3608
+ raw_path: raw.relPath,
3609
+ source_url: sourceUrl,
3610
+ stored_sha256: storedHash,
3611
+ current_sha256: currentHash,
3612
+ status: drifted2 ? "drifted" : "unchanged"
3613
+ });
3481
3614
  }
3482
3615
  const drifted = results.filter((r) => r.status === "drifted");
3483
3616
  const fetchFailed = results.filter((r) => r.status === "fetch_failed");
@@ -3496,14 +3629,7 @@ ${body}`;
3496
3629
  }
3497
3630
  if (fetchFailed.length > 0) hintLines.push(`fetch_failed: ${fetchFailed.length}`, ...fetchFailed.map((f) => ` ${f.raw_path}: ${f.fetch_error}`));
3498
3631
  if (updated.length > 0) hintLines.push(`updated: ${updated.length}`, ...updated.map((u) => ` ${u.raw_path}`));
3499
- if (input.apply && updated.length > 0) {
3500
- appendLastOp(input.vault, {
3501
- operation: "drift-apply",
3502
- summary: `updated ${updated.length} raw sources`,
3503
- files: updated.map((u) => u.raw_path),
3504
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
3505
- });
3506
- }
3632
+ if (input.apply && drifted.length > 0) hintLines.push("raw evidence is immutable; use attended reingest to create a new capture and archive the prior bytes");
3507
3633
  return {
3508
3634
  exitCode,
3509
3635
  result: ok({ scanned: results.length, drifted, fetch_failed: fetchFailed, identity_conflicts: identityConflicts, updated, newFiles: newResults, unchanged, humanHint: hintLines.join("\n") })
@@ -3977,7 +4103,7 @@ async function runTranscripts(input) {
3977
4103
  }
3978
4104
 
3979
4105
  // src/commands/compound.ts
3980
- import { writeFile as writeFile7, mkdir as mkdir7, readdir as readdir6, unlink as unlink3 } from "fs/promises";
4106
+ import { writeFile as writeFile6, mkdir as mkdir7, readdir as readdir6, unlink as unlink3 } from "fs/promises";
3981
4107
  import { join as join23 } from "path";
3982
4108
  import { existsSync as existsSync7 } from "fs";
3983
4109
  import { readFile as readFile10 } from "fs/promises";
@@ -4138,7 +4264,7 @@ async function runCompound(input) {
4138
4264
  if (!existsSync7(compoundDir)) {
4139
4265
  await mkdir7(compoundDir, { recursive: true });
4140
4266
  }
4141
- await writeFile7(compoundPath, content, "utf8");
4267
+ await writeFile6(compoundPath, content, "utf8");
4142
4268
  }
4143
4269
  promoted.push(`${slug}.md`);
4144
4270
  }
@@ -4262,7 +4388,7 @@ no compound entries found`;
4262
4388
  }
4263
4389
 
4264
4390
  // src/commands/session-brief.ts
4265
- import { mkdir as mkdir8, readFile as readFile11, writeFile as writeFile8 } from "fs/promises";
4391
+ import { mkdir as mkdir8, readFile as readFile11, writeFile as writeFile7 } from "fs/promises";
4266
4392
  import { join as join24, relative, sep } from "path";
4267
4393
  var MAX_WORDS = 900;
4268
4394
  async function runSessionBrief(input) {
@@ -4282,7 +4408,8 @@ async function runSessionBrief(input) {
4282
4408
  baseHealthWarnings,
4283
4409
  satelliteHealth,
4284
4410
  sessionPins,
4285
- memoryTopics
4411
+ memoryTopics,
4412
+ pendingSources
4286
4413
  ] = await Promise.all([
4287
4414
  loadTranscriptInfo(scan.data.raw),
4288
4415
  loadWorkItems(scan.data.workItems),
@@ -4290,7 +4417,8 @@ async function runSessionBrief(input) {
4290
4417
  loadHealthWarnings(input.vault),
4291
4418
  loadSatelliteHealth(input.vault),
4292
4419
  loadSessionPins(input.vault, project),
4293
- project ? loadMemoryTopics(input.vault, project) : Promise.resolve([])
4420
+ project ? loadMemoryTopics(input.vault, project) : Promise.resolve([]),
4421
+ loadPendingSources(input.vault, today)
4294
4422
  ]);
4295
4423
  const healthWarnings = [
4296
4424
  ...baseHealthWarnings,
@@ -4313,6 +4441,7 @@ async function runSessionBrief(input) {
4313
4441
  sessionPins,
4314
4442
  latestLogs,
4315
4443
  unclaimedCaptures,
4444
+ pendingSources,
4316
4445
  activeWork,
4317
4446
  latestDigest,
4318
4447
  projectLogs,
@@ -4347,6 +4476,7 @@ async function runSessionBrief(input) {
4347
4476
  log_updated: logUpdated,
4348
4477
  generated_at: generatedAt,
4349
4478
  session_pins: sessionPins,
4479
+ pending_sources: pendingSources,
4350
4480
  memory_topics: memoryTopics,
4351
4481
  humanHint
4352
4482
  })
@@ -4412,6 +4542,17 @@ async function loadTranscriptInfo(rawPages) {
4412
4542
  }
4413
4543
  return out;
4414
4544
  }
4545
+ async function loadPendingSources(vault, today) {
4546
+ const inventory = await inventorySources({ vault, today });
4547
+ if (!inventory.output) return [];
4548
+ return inventory.output.items.filter((item) => item.storage_status === "active" && item.lifecycle_status === "pending").slice(0, 5).map((item) => ({
4549
+ path: item.raw_path,
4550
+ title: item.title,
4551
+ summary: item.source_url ?? `${item.schema_status} source awaiting integration`,
4552
+ date: item.captured ?? "",
4553
+ status: item.schema_status
4554
+ }));
4555
+ }
4415
4556
  async function loadWorkItems(workItemPages) {
4416
4557
  const out = [];
4417
4558
  for (const page of workItemPages.filter((p) => p.relPath.endsWith("/spec.md"))) {
@@ -4482,6 +4623,7 @@ function renderBrief(input) {
4482
4623
  appendPinnedContextSection(lines, input.sessionPins);
4483
4624
  appendSection(lines, "Active Work", input.activeWork, "No active project work found.");
4484
4625
  appendSection(lines, "Unclaimed Captures", input.unclaimedCaptures, "No unclaimed task or bug captures found.");
4626
+ appendSection(lines, "Recent Pending Sources", input.pendingSources, "No pending articles or papers found.");
4485
4627
  appendSection(lines, "Recent Session Logs", input.project ? input.projectLogs : input.latestLogs, "No recent session logs found.");
4486
4628
  appendSection(lines, "Latest Agent Memory Trends", input.latestDigest, "No agent memory trends digest found.");
4487
4629
  appendMemoryTopicsSection(lines, input.project, input.memoryTopics);
@@ -4492,6 +4634,7 @@ function renderBrief(input) {
4492
4634
  "- `skillwiki status`",
4493
4635
  input.project ? `- \`skillwiki project-index ${input.project}\`` : '- `skillwiki query "active work"`',
4494
4636
  "- `skillwiki transcripts --since <date>`",
4637
+ "- `skillwiki sources pending`",
4495
4638
  ""
4496
4639
  );
4497
4640
  return lines.join("\n").trimEnd() + "\n";
@@ -4613,9 +4756,9 @@ async function writeBriefArtifacts(vault, input) {
4613
4756
  const previousComparable = comparableBrief(await readIfExists(metaPath));
4614
4757
  const nextComparable = comparableBrief(committed);
4615
4758
  const materialChange = previousComparable !== nextComparable;
4616
- await writeFile8(metaPath, committed, "utf8");
4617
- await writeFile8(cacheMdPath, input.brief, "utf8");
4618
- await writeFile8(cacheJsonPath, `${JSON.stringify({
4759
+ await writeFile7(metaPath, committed, "utf8");
4760
+ await writeFile7(cacheMdPath, input.brief, "utf8");
4761
+ await writeFile7(cacheJsonPath, `${JSON.stringify({
4619
4762
  project: input.project,
4620
4763
  brief: input.brief,
4621
4764
  word_count: input.wordCount,
@@ -4732,7 +4875,7 @@ function dateFromPath(path) {
4732
4875
  // src/commands/ingest.ts
4733
4876
  import { readFile as readFile13, open, unlink as unlink4, mkdir as mkdir9 } from "fs/promises";
4734
4877
  import { join as join26 } from "path";
4735
- import { createHash as createHash5 } from "crypto";
4878
+ import { createHash as createHash4 } from "crypto";
4736
4879
 
4737
4880
  // src/commands/page-publish.ts
4738
4881
  import { readFileSync as readFileSync12, realpathSync } from "fs";
@@ -4740,13 +4883,13 @@ import { readFile as readFile12 } from "fs/promises";
4740
4883
  import { join as join25, resolve as resolve3 } from "path";
4741
4884
 
4742
4885
  // src/utils/publication-approval.ts
4743
- import { createHash as createHash4 } from "crypto";
4886
+ import { createHash as createHash3 } from "crypto";
4744
4887
  var APPROVAL_CONTRACT = "skillwiki-publication-approval-v1";
4745
4888
  var APPROVAL_TOKEN_VERSION = "swpub1";
4746
4889
  var SHA256_HEX = /^[0-9a-f]{64}$/;
4747
4890
  var PUBLISHERS = /* @__PURE__ */ new Set(["page", "project-page"]);
4748
4891
  function sha256Hex(bytes) {
4749
- return createHash4("sha256").update(bytes).digest("hex");
4892
+ return createHash3("sha256").update(bytes).digest("hex");
4750
4893
  }
4751
4894
  function normalizeLogNote(note) {
4752
4895
  if (note === void 0) return "";
@@ -5545,13 +5688,13 @@ function isUrl(source) {
5545
5688
  function todayIso() {
5546
5689
  return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
5547
5690
  }
5548
- function buildRawContent(sourceUrl, ingested, sha256, body) {
5691
+ function buildRawContent(sourceUrl, ingested, sha2562, body) {
5549
5692
  const lines = [
5550
5693
  "---",
5551
5694
  sourceUrl !== null ? `source_url: "${sourceUrl}"` : "source_url:",
5552
5695
  `created: ${ingested}`,
5553
5696
  `ingested: ${ingested}`,
5554
- `sha256: ${sha256}`,
5697
+ `sha256: ${sha2562}`,
5555
5698
  `ingested_by: wiki-ingest`,
5556
5699
  "---",
5557
5700
  "",
@@ -5757,7 +5900,7 @@ async function runIngest(input, pagePublishDeps = defaultPagePublishDeps()) {
5757
5900
  })
5758
5901
  };
5759
5902
  }
5760
- const sha256 = createHash5("sha256").update(Buffer.from(sourceContent, "utf8")).digest("hex");
5903
+ const sha2562 = createHash4("sha256").update(Buffer.from(sourceContent, "utf8")).digest("hex");
5761
5904
  const today = todayIso();
5762
5905
  const slug = slugify2(input.title);
5763
5906
  const tags = input.tags && input.tags.length > 0 ? input.tags : [];
@@ -5788,7 +5931,7 @@ async function runIngest(input, pagePublishDeps = defaultPagePublishDeps()) {
5788
5931
  path: rawAbsPath,
5789
5932
  sourceUrl,
5790
5933
  sourceContent,
5791
- sha256,
5934
+ sha256: sha2562,
5792
5935
  today
5793
5936
  });
5794
5937
  if (!resolvedRaw.ok) {
@@ -5845,11 +5988,11 @@ async function runIngest(input, pagePublishDeps = defaultPagePublishDeps()) {
5845
5988
  result: ok({
5846
5989
  raw_path: rawRelPath,
5847
5990
  typed_path: typedRelPath,
5848
- sha256,
5991
+ sha256: sha2562,
5849
5992
  dry_run: true,
5850
5993
  humanHint: [
5851
5994
  "DRY RUN \u2014 would create:",
5852
- ` ${rawRelPath} (sha256: ${sha256.slice(0, 12)}...)`,
5995
+ ` ${rawRelPath} (sha256: ${sha2562.slice(0, 12)}...)`,
5853
5996
  ` ${typedRelPath}`,
5854
5997
  ` type: ${input.type}, tags: [${tags.join(", ")}]`,
5855
5998
  input.provenance ? ` provenance: ${input.provenance}` : ""
@@ -5877,7 +6020,7 @@ async function runIngest(input, pagePublishDeps = defaultPagePublishDeps()) {
5877
6020
  path: rawAbsPath,
5878
6021
  sourceUrl,
5879
6022
  sourceContent,
5880
- sha256,
6023
+ sha256: sha2562,
5881
6024
  today,
5882
6025
  capture: resolvedRaw.data
5883
6026
  });
@@ -5944,11 +6087,11 @@ async function runIngest(input, pagePublishDeps = defaultPagePublishDeps()) {
5944
6087
  result: ok({
5945
6088
  raw_path: rawRelPath,
5946
6089
  typed_path: typedRelPath,
5947
- sha256,
6090
+ sha256: sha2562,
5948
6091
  dry_run: false,
5949
6092
  humanHint: [
5950
6093
  "created:",
5951
- ` ${rawRelPath} (sha256: ${sha256.slice(0, 12)}...)`,
6094
+ ` ${rawRelPath} (sha256: ${sha2562.slice(0, 12)}...)`,
5952
6095
  ` ${typedRelPath}`
5953
6096
  ].join("\n"),
5954
6097
  publication: published.result.data
@@ -8128,7 +8271,7 @@ function runSyncJournalClearStale(input) {
8128
8271
 
8129
8272
  // src/commands/snapshot-maintenance.ts
8130
8273
  import { existsSync as existsSync10, mkdirSync as mkdirSync4, appendFileSync } from "fs";
8131
- import { createHash as createHash6 } from "crypto";
8274
+ import { createHash as createHash5 } from "crypto";
8132
8275
  import { execSync as execSync3, spawn } from "child_process";
8133
8276
  import { platform as platform2 } from "os";
8134
8277
  import { join as join31, resolve as resolvePath2 } from "path";
@@ -8153,7 +8296,7 @@ function computeApprovalId(plan) {
8153
8296
  targets,
8154
8297
  normalizeReason(plan.operator_reason)
8155
8298
  ].join("|");
8156
- return "smap1-" + createHash6("sha256").update(payload).digest("hex").slice(0, 32);
8299
+ return "smap1-" + createHash5("sha256").update(payload).digest("hex").slice(0, 32);
8157
8300
  }
8158
8301
  function normalizeReason(reason) {
8159
8302
  return reason.trim().replace(/\s+/g, " ");
@@ -8700,7 +8843,7 @@ async function runStatus(input) {
8700
8843
  }
8701
8844
 
8702
8845
  // src/commands/seed.ts
8703
- import { mkdir as mkdir11, writeFile as writeFile9, stat as stat4 } from "fs/promises";
8846
+ import { mkdir as mkdir11, writeFile as writeFile8, stat as stat4 } from "fs/promises";
8704
8847
  import { join as join34 } from "path";
8705
8848
  var TODAY = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
8706
8849
  var EXAMPLE_PAGES = {
@@ -8783,7 +8926,7 @@ async function runSeed(input) {
8783
8926
  skipped.push(relPath);
8784
8927
  } catch {
8785
8928
  await mkdir11(join34(absPath, ".."), { recursive: true });
8786
- await writeFile9(absPath, content, "utf8");
8929
+ await writeFile8(absPath, content, "utf8");
8787
8930
  created.push(relPath);
8788
8931
  }
8789
8932
  }
@@ -8793,7 +8936,7 @@ async function runSeed(input) {
8793
8936
  skipped.push("raw/articles/example-source.md");
8794
8937
  } catch {
8795
8938
  await mkdir11(join34(rawPath, ".."), { recursive: true });
8796
- await writeFile9(rawPath, EXAMPLE_RAW, "utf8");
8939
+ await writeFile8(rawPath, EXAMPLE_RAW, "utf8");
8797
8940
  created.push("raw/articles/example-source.md");
8798
8941
  }
8799
8942
  if (created.length > 0) {
@@ -8815,7 +8958,7 @@ async function runSeed(input) {
8815
8958
  }
8816
8959
 
8817
8960
  // src/commands/canvas.ts
8818
- import { readFile as readFile17, writeFile as writeFile10 } from "fs/promises";
8961
+ import { readFile as readFile17, writeFile as writeFile9 } from "fs/promises";
8819
8962
  import { existsSync as existsSync12 } from "fs";
8820
8963
  import { join as join35 } from "path";
8821
8964
  var NODE_WIDTH = 240;
@@ -8935,7 +9078,7 @@ async function runCanvasGenerate(input) {
8935
9078
  const canvas = { nodes, edges };
8936
9079
  const outPath = join35(input.vault, "vault-graph.canvas");
8937
9080
  try {
8938
- await writeFile10(outPath, JSON.stringify(canvas, null, 2));
9081
+ await writeFile9(outPath, JSON.stringify(canvas, null, 2));
8939
9082
  } catch (e) {
8940
9083
  return {
8941
9084
  exitCode: ExitCode.WRITE_FAILED,
@@ -8954,11 +9097,252 @@ written: ${outPath}`
8954
9097
  };
8955
9098
  }
8956
9099
 
9100
+ // src/commands/source-disposition.ts
9101
+ async function runSourceDisposition(input) {
9102
+ if (!input.write) {
9103
+ const plan = await planSourceDisposition(input);
9104
+ return { exitCode: plan.ok ? ExitCode.OK : ExitCode.USAGE, result: plan };
9105
+ }
9106
+ if (!input.approve) return { exitCode: ExitCode.USAGE, result: { ok: false, error: "APPROVAL_INVALID", detail: { message: "--write requires --approve" } } };
9107
+ const applied = await applySourceDisposition({ ...input, approve: input.approve });
9108
+ if (!applied.ok) return { exitCode: ExitCode.USAGE, result: applied };
9109
+ return { exitCode: ExitCode.OK, result: ok({ ...applied.data, humanHint: `${input.status} recorded for ${input.rawPath}` }) };
9110
+ }
9111
+
9112
+ // src/utils/source-disposal.ts
9113
+ import { createHash as createHash6 } from "crypto";
9114
+ import { readFile as readFile19, unlink as unlink5 } from "fs/promises";
9115
+
9116
+ // src/utils/raw-asset-reference-index.ts
9117
+ import { readdir as readdir7, readFile as readFile18 } from "fs/promises";
9118
+ import { join as join36, relative as relative4, sep as sep3 } from "path";
9119
+ var PREVIEW_EXTENSIONS = /* @__PURE__ */ new Set(["png", "jpg", "jpeg", "gif", "webp", "svg", "bmp", "avif", "pdf", "mp3", "wav", "m4a", "mp4", "webm"]);
9120
+ async function walkFiles(root, dir = root) {
9121
+ let entries;
9122
+ try {
9123
+ entries = await readdir7(dir, { withFileTypes: true });
9124
+ } catch {
9125
+ return [];
9126
+ }
9127
+ const out = [];
9128
+ for (const entry of entries) {
9129
+ const path = join36(dir, entry.name);
9130
+ if (entry.isDirectory()) out.push(...await walkFiles(root, path));
9131
+ else if (entry.isFile()) out.push(relative4(root, path).split(sep3).join("/"));
9132
+ }
9133
+ return out;
9134
+ }
9135
+ function supportedPreview(path) {
9136
+ const ext = path.includes(".") ? path.slice(path.lastIndexOf(".") + 1).toLowerCase() : "";
9137
+ return PREVIEW_EXTENSIONS.has(ext);
9138
+ }
9139
+ function addInbound(map, target, source) {
9140
+ const refs = map.get(target) ?? /* @__PURE__ */ new Set();
9141
+ refs.add(source);
9142
+ map.set(target, refs);
9143
+ }
9144
+ async function buildRawAssetReferenceIndex(vault) {
9145
+ const assetRoot = join36(vault, "raw", "assets");
9146
+ const relativeAssets = await walkFiles(assetRoot);
9147
+ const assets = relativeAssets.map((path) => `raw/assets/${path}`).sort((a, b) => a.localeCompare(b));
9148
+ const assetSet = new Set(assets);
9149
+ const byBasename = /* @__PURE__ */ new Map();
9150
+ for (const asset of assets) {
9151
+ const name = asset.slice(asset.lastIndexOf("/") + 1);
9152
+ byBasename.set(name, [...byBasename.get(name) ?? [], asset]);
9153
+ }
9154
+ const scan = await scanVault(vault);
9155
+ if (!scan.ok) return { assets, inbound: /* @__PURE__ */ new Map(), references: [] };
9156
+ const inboundSets = /* @__PURE__ */ new Map();
9157
+ const references = [];
9158
+ for (const page of scan.data.allMarkdown) {
9159
+ let text;
9160
+ try {
9161
+ text = await readFile18(page.absPath, "utf8");
9162
+ } catch {
9163
+ continue;
9164
+ }
9165
+ for (const match of text.matchAll(/!\[\[([^\]|#]+)(?:[|#][^\]]*)?\]\]/g)) {
9166
+ const target = match[1].trim();
9167
+ if (target.startsWith("raw/assets/")) {
9168
+ const exists = assetSet.has(target);
9169
+ references.push({ source_path: page.relPath, target, kind: "obsidian-embed", exists, preview_supported: supportedPreview(target) });
9170
+ if (exists) addInbound(inboundSets, target, page.relPath);
9171
+ } else if (!target.includes("/")) {
9172
+ const candidates = (byBasename.get(target) ?? []).sort((a, b) => a.localeCompare(b));
9173
+ if (candidates.length > 0) {
9174
+ references.push({ source_path: page.relPath, target, kind: "ambiguous-embed", exists: candidates.length === 1, preview_supported: supportedPreview(target), candidates });
9175
+ if (candidates.length === 1) addInbound(inboundSets, candidates[0], page.relPath);
9176
+ }
9177
+ }
9178
+ }
9179
+ for (const match of text.matchAll(/!\[[^\]]*\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g)) {
9180
+ const target = match[1].trim().replace(/^<|>$/g, "");
9181
+ if (/^https?:\/\//i.test(target)) {
9182
+ references.push({ source_path: page.relPath, target, kind: "external-image", exists: true, preview_supported: supportedPreview(new URL(target).pathname) });
9183
+ } else if (target.startsWith("raw/assets/")) {
9184
+ const exists = assetSet.has(target);
9185
+ references.push({ source_path: page.relPath, target, kind: "markdown-image", exists, preview_supported: supportedPreview(target) });
9186
+ if (exists) addInbound(inboundSets, target, page.relPath);
9187
+ }
9188
+ }
9189
+ }
9190
+ references.sort((a, b) => a.target.localeCompare(b.target) || a.source_path.localeCompare(b.source_path));
9191
+ const inbound = new Map([...inboundSets.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([target, refs]) => [target, [...refs].sort()]));
9192
+ return { assets, inbound, references };
9193
+ }
9194
+
9195
+ // src/utils/source-disposal.ts
9196
+ function sha256(bytes) {
9197
+ return createHash6("sha256").update(bytes).digest("hex");
9198
+ }
9199
+ function exactTarget(path) {
9200
+ if (/[?*\[\]{}]/.test(path) || !path.includes("/") || path.endsWith("/")) return err("RAW_DISPOSAL_TARGET_INVALID", { path, message: "exact file path required; globs, basenames, and directories are refused" });
9201
+ const classified = classifyRawPath(path);
9202
+ if (!classified.ok) return classified;
9203
+ return ok(true);
9204
+ }
9205
+ async function planSourceDisposal(input) {
9206
+ const exact = exactTarget(input.rawPath);
9207
+ if (!exact.ok) return exact;
9208
+ const authorized = authorizeRawOperation({
9209
+ operationClass: "destructive-remove",
9210
+ trigger: "attended-apply",
9211
+ source: input.rawPath,
9212
+ explicitExactTarget: true
9213
+ });
9214
+ if (!authorized.ok) return authorized;
9215
+ const reason = input.reason.normalize("NFC").trim();
9216
+ if (!reason) return err("RAW_DISPOSAL_TARGET_INVALID", { field: "reason" });
9217
+ const sensitive = scanSensitiveContent(reason);
9218
+ if (sensitive.length > 0) return err("SENSITIVE_CONTENT_DETECTED", { findings: sensitive });
9219
+ const target = await resolveExistingRegularFileInsideVault(input.vault, input.rawPath);
9220
+ if (!target.ok) return err("RAW_DISPOSAL_TARGET_INVALID", { path: input.rawPath, cause: target });
9221
+ const bytes = await readFile19(target.data);
9222
+ const size = bytes.byteLength;
9223
+ let typedReferences = [];
9224
+ let otherReferences = [];
9225
+ let assetInboundReferences = [];
9226
+ if (input.rawPath.startsWith("raw/assets/")) {
9227
+ const assets = await buildRawAssetReferenceIndex(input.vault);
9228
+ assetInboundReferences = assets.inbound.get(input.rawPath) ?? [];
9229
+ } else if (input.rawPath.endsWith(".md")) {
9230
+ const scan = await scanVault(input.vault);
9231
+ if (!scan.ok) return scan;
9232
+ const typedPaths = new Set(scan.data.typedKnowledge.map((page) => page.relPath));
9233
+ const relocations = await readSourceRelocations(input.vault);
9234
+ if (!relocations.ok) return relocations;
9235
+ const refs = await buildSourceReferenceIndex({
9236
+ typedPages: scan.data.typedKnowledge,
9237
+ otherPages: scan.data.allMarkdown.filter((page) => !typedPaths.has(page.relPath) && !page.relPath.startsWith("raw/")),
9238
+ availableRawPaths: scan.data.raw.map((page) => page.relPath),
9239
+ relocationProjection: buildSourceRelocationProjection(relocations.data)
9240
+ });
9241
+ typedReferences = refs.integratedBy.get(input.rawPath) ?? [];
9242
+ otherReferences = refs.referencedElsewhereBy.get(input.rawPath) ?? [];
9243
+ }
9244
+ const completeHash = sha256(bytes);
9245
+ const occurredAt = input.now ?? (/* @__PURE__ */ new Date()).toISOString();
9246
+ const deleteIntentPath = `meta/delete-intents/${pathToIntentFilename(input.rawPath)}`;
9247
+ const operation_id = operationId("source-disposal", [input.rawPath, completeHash, reason, ...typedReferences, ...otherReferences, ...assetInboundReferences]);
9248
+ const base = {
9249
+ raw_path: input.rawPath,
9250
+ complete_sha256: completeHash,
9251
+ size_bytes: size,
9252
+ typed_references: typedReferences,
9253
+ other_references: otherReferences,
9254
+ asset_inbound_references: assetInboundReferences,
9255
+ delete_intent_path: deleteIntentPath,
9256
+ recoverability: "Recoverable only from Git/S3 history or another backup after deletion; the delete intent prevents automatic resurrection.",
9257
+ reason,
9258
+ operation_id,
9259
+ occurred_at: occurredAt
9260
+ };
9261
+ return ok({ ...base, approval_token: encodeSourceActionApproval({ contract: "source-disposal/v1", ...base }), write: false });
9262
+ }
9263
+ async function applySourceDisposal(input) {
9264
+ if (!input.attended) return err("RAW_DISPOSAL_ATTENDED_REQUIRED", { message: "permanent disposal is refused in scheduled/headless sessions" });
9265
+ const authorized = authorizeRawOperation({
9266
+ operationClass: "destructive-remove",
9267
+ trigger: "attended-apply",
9268
+ source: input.rawPath,
9269
+ explicitExactTarget: true
9270
+ });
9271
+ if (!authorized.ok) return authorized;
9272
+ const decoded = decodeSourceActionApproval(input.approve);
9273
+ if (!decoded.ok) return decoded;
9274
+ const occurredAt = typeof decoded.data.occurred_at === "string" ? decoded.data.occurred_at : void 0;
9275
+ const planned = await planSourceDisposal({ vault: input.vault, rawPath: input.rawPath, reason: input.reason, now: occurredAt });
9276
+ if (!planned.ok) return planned;
9277
+ if (planned.data.approval_token !== input.approve) return err("APPROVAL_INVALID", { message: "source disposal approval does not match live target or references" });
9278
+ const event = await writeLogEvent(input.vault, {
9279
+ schema: "skillwiki-log-event/v1",
9280
+ operation_id: planned.data.operation_id,
9281
+ occurred_at: planned.data.occurred_at,
9282
+ host_id: input.hostId ?? "local",
9283
+ actor: input.actor ?? "skillwiki-cli",
9284
+ kind: "source-disposal-approved",
9285
+ target: input.rawPath,
9286
+ note: input.reason,
9287
+ metadata: {
9288
+ raw_path: input.rawPath,
9289
+ complete_sha256: planned.data.complete_sha256,
9290
+ typed_references: planned.data.typed_references,
9291
+ other_references: planned.data.other_references,
9292
+ asset_inbound_references: planned.data.asset_inbound_references,
9293
+ delete_intent_path: planned.data.delete_intent_path
9294
+ }
9295
+ });
9296
+ if (!event.ok) return event;
9297
+ const tombstonePath = await writeDeleteIntent(input.vault, buildDeleteIntent({
9298
+ path: input.rawPath,
9299
+ action: "remove",
9300
+ actor: input.actor ?? "skillwiki-cli",
9301
+ source: "cli",
9302
+ reason: input.reason,
9303
+ created: planned.data.occurred_at
9304
+ }));
9305
+ const target = await resolveExistingRegularFileInsideVault(input.vault, input.rawPath);
9306
+ if (!target.ok) return err("APPROVAL_INVALID", { message: "source disposal target is no longer a safe exact file", cause: target });
9307
+ await unlink5(target.data);
9308
+ const completedOperationId = operationId("source-disposal-completed", [planned.data.operation_id]);
9309
+ const completed = await writeLogEvent(input.vault, {
9310
+ schema: "skillwiki-log-event/v1",
9311
+ operation_id: completedOperationId,
9312
+ occurred_at: planned.data.occurred_at,
9313
+ host_id: input.hostId ?? "local",
9314
+ actor: input.actor ?? "skillwiki-cli",
9315
+ kind: "source-disposal-completed",
9316
+ target: input.rawPath,
9317
+ note: input.reason,
9318
+ metadata: {
9319
+ approval_operation_id: planned.data.operation_id,
9320
+ raw_path: input.rawPath,
9321
+ complete_sha256: planned.data.complete_sha256,
9322
+ delete_intent_path: planned.data.delete_intent_path
9323
+ }
9324
+ });
9325
+ if (!completed.ok) return completed;
9326
+ return ok({ removed: input.rawPath, tombstone_path: tombstonePath, event_path: completed.data.path, operation_id: planned.data.operation_id });
9327
+ }
9328
+
9329
+ // src/commands/source-disposal.ts
9330
+ async function runSourceDisposal(input) {
9331
+ if (!input.write) {
9332
+ const plan = await planSourceDisposal(input);
9333
+ return { exitCode: plan.ok ? ExitCode.OK : ExitCode.USAGE, result: plan };
9334
+ }
9335
+ if (!input.approve) return { exitCode: ExitCode.USAGE, result: { ok: false, error: "APPROVAL_INVALID", detail: { message: "--write requires --approve" } } };
9336
+ const applied = await applySourceDisposal({ ...input, approve: input.approve, attended: input.attended === true });
9337
+ if (!applied.ok) return { exitCode: ExitCode.USAGE, result: applied };
9338
+ return { exitCode: ExitCode.OK, result: ok({ ...applied.data, humanHint: `permanently disposed ${input.rawPath}; ${applied.data.tombstone_path}` }) };
9339
+ }
9340
+
8957
9341
  // src/commands/fleet-health.ts
8958
9342
  import { existsSync as existsSync13, readFileSync as readFileSync16 } from "fs";
8959
9343
  import { execSync as nodeExecSync } from "child_process";
8960
9344
  import { hostname as nodeHostname, platform as nodePlatform } from "os";
8961
- import { join as join36 } from "path";
9345
+ import { join as join37 } from "path";
8962
9346
  var SSH_TIMEOUT_MS = 15e3;
8963
9347
  var TIMER_UNIT = "agent-memory-trends.timer";
8964
9348
  var SERVICE_UNIT = "agent-memory-trends.service";
@@ -9188,7 +9572,7 @@ async function runFleetHealth(input) {
9188
9572
  const home = input.home ?? env.HOME ?? "";
9189
9573
  const osHostname = input.osHostname ?? env.HOSTNAME ?? nodeHostname();
9190
9574
  const vault = input.vault ?? env.WIKI_PATH;
9191
- const file = input.file ?? (vault ? join36(vault, FLEET_REL_PATH) : void 0);
9575
+ const file = input.file ?? (vault ? join37(vault, FLEET_REL_PATH) : void 0);
9192
9576
  if (!file) {
9193
9577
  return {
9194
9578
  exitCode: ExitCode.NO_VAULT_CONFIGURED,
@@ -9237,7 +9621,7 @@ async function runFleetHealth(input) {
9237
9621
  healthy
9238
9622
  });
9239
9623
  } else {
9240
- if (!hasDeclaredSshAccess(t.host, localHostId, t.sshAlias)) {
9624
+ if (!localHostId || !hasDeclaredSshAccess(t.host, localHostId, t.sshAlias)) {
9241
9625
  rows.push({
9242
9626
  host: t.id,
9243
9627
  timer: "unknown",
@@ -9273,14 +9657,14 @@ async function runFleetHealth(input) {
9273
9657
 
9274
9658
  // src/utils/auto-commit.ts
9275
9659
  import { existsSync as existsSync14 } from "fs";
9276
- import { join as join37 } from "path";
9660
+ import { join as join38 } from "path";
9277
9661
  async function postCommit(vault, exitCode) {
9278
9662
  if (exitCode !== 0) return;
9279
9663
  const home = process.env.HOME ?? "";
9280
9664
  const dotenv = await parseDotenvFile(configPath(home));
9281
9665
  const autoCommit = process.env.AUTO_COMMIT ?? dotenv["AUTO_COMMIT"];
9282
9666
  if (autoCommit === "false") return;
9283
- if (!existsSync14(join37(vault, ".git"))) return;
9667
+ if (!existsSync14(join38(vault, ".git"))) return;
9284
9668
  const lastOps = readLastOp(vault);
9285
9669
  if (lastOps.length === 0) return;
9286
9670
  const porcelain = git(vault, ["status", "--porcelain", "--", ...VAULT_COMMIT_PATHSPEC]);
@@ -9464,7 +9848,7 @@ program.command("validate <file>").description("validate vault page frontmatter
9464
9848
  emit(await runValidate({ file, apply: !!opts.apply, vault }), vault);
9465
9849
  });
9466
9850
  program.command("graph").description("graph subcommands").command("build <vault>").option("--out <path>", "graph output path (default: <vault>/.skillwiki/graph.json)").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
9467
- const out = opts.out ?? join38(vault, ".skillwiki", "graph.json");
9851
+ const out = opts.out ?? join39(vault, ".skillwiki", "graph.json");
9468
9852
  return emitGuardedVaultWrite(vault, "graph build", () => runGraphBuild({ vault, out }));
9469
9853
  });
9470
9854
  var canvasCmd = program.command("canvas").description("manage Obsidian canvas files");
@@ -9478,10 +9862,76 @@ program.command("overlap [vault]").description("detect typed-knowledge pages tha
9478
9862
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
9479
9863
  else emit(await runOverlap({ vault: v.vault }), v.vault);
9480
9864
  });
9481
- program.command("query <text> [vault]").description("score and rank vault pages by relevance to a query").option("--limit <n>", "max results to return", (s) => parseInt(s, 10), 10).option("--wiki <name>", "wiki profile name").action(async (text, vault, opts) => {
9865
+ program.command("query <text> [vault]").description("score and rank vault pages by relevance to a query").option("--limit <n>", "max results to return", (s) => parseInt(s, 10), 10).option("--include-pending", "include a separate pending-evidence channel").option("--wiki <name>", "wiki profile name").action(async (text, vault, opts) => {
9482
9866
  const v = await resolveVaultArg(vault, opts.wiki);
9483
9867
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
9484
- else emit(await runQuery({ text, vault: v.vault, limit: opts.limit }), v.vault);
9868
+ else emit(await runQuery({ text, vault: v.vault, limit: opts.limit, includePending: opts.includePending }), v.vault, { postCommit: false });
9869
+ });
9870
+ var sourcesCmd = program.command("sources").description("inspect raw-source lifecycle state");
9871
+ sourcesCmd.command("pending [vault]").description("list captured articles and papers awaiting typed integration").option("--since <date>", "include sources captured on or after YYYY-MM-DD").option("--older-than <days>", "include sources at least this many days old", (s) => parseInt(s, 10)).option("--match <text>", "literal title, URL, or path match").option("--ingested-by <channel>", "manual, wiki-ingest, proj-work, or unknown").option("--scope <scope>", "articles, papers, or all", "all").option("--sort <order>", "newest or oldest", "newest").option("--limit <n>", "max items to return", (s) => parseInt(s, 10), 50).option("--all", "return the complete matching inventory").option("--include-integrated", "also return actively integrated sources").option("--include-archived", "also return raw/archived sources").option("--include-duplicates", "also return raw/duplicates sources").option("--include-legacy-archived", "also return legacy _archive/raw sources").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
9872
+ const v = await resolveVaultArg(vault, opts.wiki);
9873
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
9874
+ else emit(await runSourcesPending({
9875
+ vault: v.vault,
9876
+ since: opts.since,
9877
+ olderThan: opts.olderThan,
9878
+ match: opts.match,
9879
+ ingestedBy: opts.ingestedBy,
9880
+ scope: opts.scope,
9881
+ sort: opts.sort,
9882
+ limit: opts.limit,
9883
+ all: opts.all,
9884
+ includeIntegrated: opts.includeIntegrated,
9885
+ includeArchived: opts.includeArchived,
9886
+ includeDuplicates: opts.includeDuplicates,
9887
+ includeLegacyArchived: opts.includeLegacyArchived
9888
+ }), v.vault, { postCommit: false });
9889
+ });
9890
+ sourcesCmd.command("disposition <raw-path> [vault]").description("record an append-only editorial decision for an exact raw source").requiredOption("--status <status>", "reviewed-no-op, deferred, duplicate, out-of-scope, superseded, or reopened").requiredOption("--reason <text>", "editorial reason").option("--review-after <date>", "future review date for deferred").option("--duplicate-of <raw-path>", "canonical exact raw path for duplicate").option("--write", "write the approved disposition event").option("--approve <token>", "approve the exact state-bound disposition plan").option("--wiki <name>", "wiki profile name").action(async (rawPath, vault, opts) => {
9891
+ const v = await resolveVaultArg(vault, opts.wiki);
9892
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
9893
+ else if (!opts.write) emit(await runSourceDisposition({
9894
+ vault: v.vault,
9895
+ rawPath,
9896
+ status: opts.status,
9897
+ reason: opts.reason,
9898
+ reviewAfter: opts.reviewAfter,
9899
+ duplicateOf: opts.duplicateOf,
9900
+ write: false
9901
+ }), v.vault, { postCommit: false });
9902
+ else return emitManagedVaultWrite(
9903
+ v.vault,
9904
+ "sources disposition",
9905
+ (receipt) => runSourceDisposition({
9906
+ vault: v.vault,
9907
+ rawPath,
9908
+ status: opts.status,
9909
+ reason: opts.reason,
9910
+ reviewAfter: opts.reviewAfter,
9911
+ duplicateOf: opts.duplicateOf,
9912
+ write: true,
9913
+ approve: opts.approve
9914
+ }),
9915
+ { allowImmutableRecord: true }
9916
+ );
9917
+ });
9918
+ sourcesCmd.command("dispose <raw-path> [vault]").description("permanently delete one exact raw object after an attended approved preview").requiredOption("--reason <text>", "permanent-disposal reason").option("--write", "apply the approved exact-target disposal").option("--approve <token>", "approve the exact state-bound disposal plan").option("--wiki <name>", "wiki profile name").action(async (rawPath, vault, opts) => {
9919
+ const v = await resolveVaultArg(vault, opts.wiki);
9920
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
9921
+ else if (!opts.write) emit(await runSourceDisposal({ vault: v.vault, rawPath, reason: opts.reason, write: false }), v.vault, { postCommit: false });
9922
+ else return emitManagedVaultWrite(
9923
+ v.vault,
9924
+ "sources dispose",
9925
+ () => runSourceDisposal({
9926
+ vault: v.vault,
9927
+ rawPath,
9928
+ reason: opts.reason,
9929
+ write: true,
9930
+ approve: opts.approve,
9931
+ attended: process.stdin.isTTY === true && process.stdout.isTTY === true
9932
+ }),
9933
+ { allowImmutableRecord: true }
9934
+ );
9485
9935
  });
9486
9936
  program.command("orphans [vault]").description("find pages not referenced by any other page").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => emit(await runOrphans({
9487
9937
  vault,
@@ -9647,15 +10097,15 @@ program.command("topic-map-check [vault]").description("check whether a topic ma
9647
10097
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
9648
10098
  else emit(await runTopicMapCheck({ vault: v.vault, threshold: opts.threshold }), v.vault);
9649
10099
  });
9650
- program.command("stale [vault]").description("identify stale transcripts and incomplete work items").option("--archive", "move stale items to _archive/", false).option("--days <n>", "staleness threshold in days", (s) => parseInt(s, 10), 3).option("--force-scan", "infer kind/project from filename and content when frontmatter is missing", false).option("--project <slug>", "scope to a single project").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
10100
+ program.command("stale [vault]").description("identify stale transcripts and incomplete work items").option("--archive", "preview lifecycle-first archives for stale items", false).option("--apply", "apply an approved stale archive preview", false).option("--approve <token>", "approve the exact state-bound stale archive plan").option("--days <n>", "staleness threshold in days", (s) => parseInt(s, 10), 3).option("--force-scan", "infer kind/project from filename and content when frontmatter is missing", false).option("--project <slug>", "scope to a single project").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
9651
10101
  const v = await resolveVaultArg(vault, opts.wiki);
9652
10102
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
9653
- else if (opts.archive) return emitGuardedVaultWrite(
10103
+ else if (opts.archive && opts.apply) return emitManagedVaultWrite(
9654
10104
  v.vault,
9655
10105
  "stale --archive",
9656
- () => runStale({ vault: v.vault, days: opts.days, archive: true, forceScan: !!opts.forceScan, project: opts.project })
10106
+ () => runStale({ vault: v.vault, days: opts.days, archive: true, apply: true, approve: opts.approve, forceScan: !!opts.forceScan, project: opts.project })
9657
10107
  );
9658
- else emit(await runStale({ vault: v.vault, days: opts.days, archive: false, forceScan: !!opts.forceScan, project: opts.project }), v.vault);
10108
+ else emit(await runStale({ vault: v.vault, days: opts.days, archive: !!opts.archive, apply: false, forceScan: !!opts.forceScan, project: opts.project }), v.vault, { postCommit: false });
9659
10109
  });
9660
10110
  program.command("claim <transcript> [vault]").description("claim an unclaimed transcript by creating a work item with source: link").option("--project <slug>", "project slug (overrides transcript frontmatter)").option("--slug <slug>", "work-item slug (defaults to transcript filename without date/kind prefix)").option("--wiki <name>", "wiki profile name").action(async (transcript, vault, opts) => {
9661
10111
  const v = await resolveVaultArg(vault, opts.wiki);
@@ -9786,6 +10236,21 @@ projectionsCmd.command("repair-legacy [vault]").description("repair one supporte
9786
10236
  program.command("lint [vault]").description("run all vault health checks").option("--days <n>", "stale threshold", (s) => parseInt(s, 10), 90).option("--lines <n>", "pagesize threshold", (s) => parseInt(s, 10), 200).option("--log-threshold <n>", "log rotation threshold", (s) => parseInt(s, 10), 500).option("--fix", "auto-fix supported lint violations").option("--only <bucket>", "run only the specified lint bucket").option("--summary", "emit bounded bucket counts instead of full item arrays", false).option("--examples <n>", "example count per bucket in summary mode", (s) => parseInt(s, 10), 3).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
9787
10237
  const v = await resolveVaultArg(vault, opts.wiki);
9788
10238
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
10239
+ else if (opts.fix && opts.summary) return emitGuardedVaultWrite(
10240
+ v.vault,
10241
+ "lint --fix",
10242
+ () => runLint({
10243
+ vault: v.vault,
10244
+ source: vault ? "flag" : void 0,
10245
+ days: opts.days,
10246
+ lines: opts.lines,
10247
+ logThreshold: opts.logThreshold,
10248
+ fix: true,
10249
+ only: opts.only,
10250
+ summary: true,
10251
+ examplesLimit: opts.examples
10252
+ })
10253
+ );
9789
10254
  else if (opts.fix) return emitGuardedVaultWrite(
9790
10255
  v.vault,
9791
10256
  "lint --fix",
@@ -9797,8 +10262,7 @@ program.command("lint [vault]").description("run all vault health checks").optio
9797
10262
  logThreshold: opts.logThreshold,
9798
10263
  fix: true,
9799
10264
  only: opts.only,
9800
- summary: !!opts.summary,
9801
- examplesLimit: opts.summary ? opts.examples : void 0
10265
+ summary: false
9802
10266
  })
9803
10267
  );
9804
10268
  else if (opts.summary) emit(await runLint({
@@ -9865,10 +10329,10 @@ program.command("status [vault]").description("output vault diagnostics").option
9865
10329
  langEnvValue: process.env.WIKI_LANG
9866
10330
  }), v.vault);
9867
10331
  });
9868
- program.command("archive <page> [vault]").description("archive a typed-knowledge or raw page").option("--wiki <name>", "wiki profile name").option("--cascade", "scan vault for references (wikilinks + sources arrays); preview by default", false).option("--apply", "with --cascade: mutate sources arrays and archive (without --apply, --cascade is preview-only)", false).option("--remote <remote>", "rclone remote root to prune the archived source path, for example seaweed-wiki:cloud/wiki").option("--remote-delete", "delete the archived source path from the remote after local archive", false).option("--max-remote-deletes <n>", "maximum remote object deletes allowed", "1").action(async (page, vault, opts) => {
10332
+ program.command("archive <page> [vault]").description("archive a typed-knowledge or raw page").option("--wiki <name>", "wiki profile name").option("--cascade", "scan vault for references (wikilinks + sources arrays); preview by default", false).option("--apply", "with --cascade: mutate sources arrays and archive (without --apply, --cascade is preview-only)", false).option("--approve <token>", "approve an exact state-bound raw archive plan").option("--remote <remote>", "rclone remote root to prune the archived source path, for example seaweed-wiki:cloud/wiki").option("--remote-delete", "delete the archived source path from the remote after local archive", false).option("--max-remote-deletes <n>", "maximum remote object deletes allowed", "1").action(async (page, vault, opts) => {
9869
10333
  const v = await resolveVaultArg(vault, opts.wiki);
9870
10334
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
9871
- else if (opts.cascade && !opts.apply) emit(await runArchive({
10335
+ else if (!opts.apply && (opts.cascade || String(page).startsWith("raw/"))) emit(await runArchive({
9872
10336
  vault: v.vault,
9873
10337
  page,
9874
10338
  cascade: true,
@@ -9885,6 +10349,7 @@ program.command("archive <page> [vault]").description("archive a typed-knowledge
9885
10349
  page,
9886
10350
  cascade: !!opts.cascade,
9887
10351
  apply: !!opts.apply,
10352
+ approve: opts.approve,
9888
10353
  remote: opts.remote,
9889
10354
  remoteDelete: !!opts.remoteDelete,
9890
10355
  maxRemoteDeletes: Number.parseInt(opts.maxRemoteDeletes, 10)
@@ -9917,15 +10382,16 @@ program.command("drift [vault]").description("detect content drift in raw source
9917
10382
  );
9918
10383
  else emit(await runDrift({ vault: v.vault, apply: false, newSince: opts.new }), v.vault);
9919
10384
  });
9920
- program.command("dedup [vault]").description("detect duplicate raw sources by sha256").option("--apply", "rewire citations and remove duplicate raw files", false).option("--canonical-policy <policy>", "canonical policy: stable-path or scan-order", "stable-path").option("--manifest-out <path>", "write raw dedup delete manifest before applying").option("--manifest-in <path>", "read existing raw dedup delete manifest for remote pruning").option("--remote <remote>", "rclone remote root, for example seaweed-wiki:cloud/wiki").option("--remote-delete", "delete manifest duplicate paths from the remote", false).option("--max-remote-deletes <n>", "maximum remote object deletes allowed", "50").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
10385
+ program.command("dedup [vault]").description("detect duplicate raw sources by sha256").option("--apply", "rewire citations and preserve duplicate raw files under raw/duplicates", false).option("--approve <token>", "approve the exact state-bound dedup plan").option("--canonical-policy <policy>", "canonical policy: stable-path or scan-order", "stable-path").option("--manifest-out <path>", "write raw dedup delete manifest before applying").option("--manifest-in <path>", "read existing raw dedup delete manifest for remote pruning").option("--remote <remote>", "rclone remote root, for example seaweed-wiki:cloud/wiki").option("--remote-delete", "delete manifest duplicate paths from the remote", false).option("--max-remote-deletes <n>", "maximum remote object deletes allowed", "50").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
9921
10386
  const v = await resolveVaultArg(vault, opts.wiki);
9922
10387
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
9923
- else if (opts.apply || opts.manifestOut) return emitGuardedVaultWrite(
10388
+ else if (opts.apply || opts.manifestOut) return emitManagedVaultWrite(
9924
10389
  v.vault,
9925
10390
  opts.apply ? "dedup --apply" : "dedup --manifest-out",
9926
10391
  () => runDedup({
9927
10392
  vault: v.vault,
9928
10393
  apply: opts.apply,
10394
+ approve: opts.approve,
9929
10395
  canonicalPolicy: opts.canonicalPolicy,
9930
10396
  manifestOut: opts.manifestOut,
9931
10397
  manifestIn: opts.manifestIn,
@@ -9937,6 +10403,7 @@ program.command("dedup [vault]").description("detect duplicate raw sources by sh
9937
10403
  else emit(await runDedup({
9938
10404
  vault: v.vault,
9939
10405
  apply: opts.apply,
10406
+ approve: opts.approve,
9940
10407
  canonicalPolicy: opts.canonicalPolicy,
9941
10408
  manifestOut: opts.manifestOut,
9942
10409
  manifestIn: opts.manifestIn,