skillwiki 0.10.21 → 0.10.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/chunk-6ZDKTNLA.js +294 -0
  2. package/dist/{chunk-65Q5UGND.js → chunk-BASWDOQB.js} +2 -2
  3. package/dist/{chunk-Y6KRDGI2.js → chunk-C2DKFJFA.js} +6 -1
  4. package/dist/chunk-EQPU2BPM.js +468 -0
  5. package/dist/{chunk-GNS2ZV5P.js → chunk-PQG26AGJ.js} +1 -1
  6. package/dist/{chunk-I5JD3BQZ.js → chunk-QBZYEEBD.js} +13 -296
  7. package/dist/chunk-QNTBNNEL.js +616 -0
  8. package/dist/{chunk-UNPZDCWN.js → chunk-SYKSL3JQ.js} +860 -700
  9. package/dist/cli.js +2334 -273
  10. package/dist/{index-projection-HAXDEM2F.js → index-projection-FRWTZB5Y.js} +3 -2
  11. package/dist/{managed-write-preflight-CTXX2MHQ.js → managed-write-preflight-4LPVMP42.js} +3 -3
  12. package/dist/skillwiki-mcp.js +6 -4
  13. package/dist/sources-L2SQV63E.js +10 -0
  14. package/package.json +1 -1
  15. package/skills/.claude-plugin/plugin.json +1 -1
  16. package/skills/.codex-plugin/plugin.json +1 -1
  17. package/skills/README.md +13 -2
  18. package/skills/agents/wiki-add-task.md +2 -1
  19. package/skills/agents/wiki-archive.md +8 -9
  20. package/skills/agents/wiki-audit.md +2 -1
  21. package/skills/agents/wiki-lint.md +2 -1
  22. package/skills/agents/wiki-query.md +3 -1
  23. package/skills/agents/wiki-reingest.md +9 -8
  24. package/skills/package.json +1 -1
  25. package/skills/proj-decide/SKILL.md +35 -5
  26. package/skills/skills/proj-decide/SKILL.md +35 -5
  27. package/skills/skills/using-skillwiki/SKILL.md +19 -13
  28. package/skills/skills/wiki-add-task/SKILL.md +4 -3
  29. package/skills/skills/wiki-archive/SKILL.md +23 -18
  30. package/skills/skills/wiki-audit/SKILL.md +3 -1
  31. package/skills/skills/wiki-init/SKILL.md +12 -1
  32. package/skills/skills/wiki-lint/SKILL.md +2 -1
  33. package/skills/skills/wiki-query/SKILL.md +7 -0
  34. package/skills/skills/wiki-reingest/SKILL.md +12 -8
  35. package/skills/skills/wiki-remove/SKILL.md +22 -5
  36. package/skills/skills/wiki-sync/SKILL.md +39 -54
  37. package/skills/using-skillwiki/SKILL.md +19 -13
  38. package/skills/wiki-add-task/SKILL.md +4 -3
  39. package/skills/wiki-archive/SKILL.md +23 -18
  40. package/skills/wiki-audit/SKILL.md +3 -1
  41. package/skills/wiki-init/SKILL.md +12 -1
  42. package/skills/wiki-lint/SKILL.md +2 -1
  43. package/skills/wiki-query/SKILL.md +7 -0
  44. package/skills/wiki-reingest/SKILL.md +12 -8
  45. package/skills/wiki-remove/SKILL.md +22 -5
  46. package/skills/wiki-sync/SKILL.md +39 -54
  47. package/templates/SCHEMA.md +8 -5
  48. package/templates/web-clipper/llm-wiki-clippings.json +56 -0
  49. package/templates/web-clipper/readme.txt +16 -0
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-UNPZDCWN.js";
83
+ upsertIndexEntry
84
+ } from "./chunk-SYKSL3JQ.js";
87
85
  import {
88
86
  normalizeDistTag,
89
87
  readCache,
@@ -96,20 +94,14 @@ 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-I5JD3BQZ.js";
100
+ } from "./chunk-QBZYEEBD.js";
109
101
  import {
110
102
  runManagedWritePreflight,
111
103
  runManagedWriteTransaction
112
- } from "./chunk-65Q5UGND.js";
104
+ } from "./chunk-BASWDOQB.js";
113
105
  import {
114
106
  FLEET_REL_PATH,
115
107
  canSupersedeJournal,
@@ -137,18 +129,52 @@ import {
137
129
  snapshotterAliasForLocalHost,
138
130
  supersedeStaleReviewRequiredJournals,
139
131
  writeDotenv
140
- } from "./chunk-GNS2ZV5P.js";
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,
144
168
  RawSourceSchema,
169
+ TypedKnowledgeSchema,
170
+ detectSchema,
145
171
  err,
146
172
  isBlockedHost,
147
173
  ok
148
- } from "./chunk-Y6KRDGI2.js";
174
+ } from "./chunk-C2DKFJFA.js";
149
175
 
150
176
  // src/cli.ts
151
- import { join as join36 } from "path";
177
+ import { join as join39 } from "path";
152
178
  import { Command } from "commander";
153
179
 
154
180
  // src/utils/output.ts
@@ -210,10 +236,10 @@ async function runHash(input) {
210
236
  const split = splitFrontmatter(text);
211
237
  if (!split.ok) return { exitCode: ExitCode.MISSING_CLOSING_DELIMITER, result: split };
212
238
  const bodyBytes = Buffer.from(split.data.body, "utf8");
213
- const sha256 = createHash("sha256").update(bodyBytes).digest("hex");
239
+ const sha2562 = createHash("sha256").update(bodyBytes).digest("hex");
214
240
  return {
215
241
  exitCode: ExitCode.OK,
216
- 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 })
217
243
  };
218
244
  }
219
245
 
@@ -588,7 +614,7 @@ async function runLang(input) {
588
614
 
589
615
  // src/commands/init.ts
590
616
  import { mkdir as mkdir3, readFile as readFile3, readdir as readdir2, writeFile as writeFile2 } from "fs/promises";
591
- import { join as join5 } from "path";
617
+ import { join as join5, dirname as dirname3 } from "path";
592
618
  var DEFAULT_TAXONOMY = [
593
619
  "research",
594
620
  "comparison",
@@ -606,6 +632,12 @@ var VAULT_DIRS = [
606
632
  "raw/papers",
607
633
  "raw/transcripts",
608
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",
609
641
  "entities",
610
642
  "concepts",
611
643
  "comparisons",
@@ -618,6 +650,8 @@ var VAULT_DIRS = [
618
650
  var ATTACHMENT_FOLDER = "raw/assets";
619
651
  var NEW_FILE_FOLDER = "raw/transcripts";
620
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`;
621
655
  function extractDomainFromSchema(text) {
622
656
  const m = text.match(/^##\s+Domain\s*\n([\s\S]*?)(?=\n\n|\n##|\s*$)/m);
623
657
  if (!m) return "";
@@ -744,6 +778,23 @@ async function runInit(input) {
744
778
  return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: fileName, message: String(e) }) };
745
779
  }
746
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
+ }
747
798
  const err1 = await writeOrPreserve("index.md", async () => {
748
799
  const tpl = await readFile3(join5(input.templates, "index.md"), "utf8");
749
800
  return tpl.replace("{{INIT_DATE}}", today);
@@ -771,6 +822,16 @@ async function runInit(input) {
771
822
  ].join("\n");
772
823
  });
773
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;
774
835
  const err22 = await writeOrPreserve("log.md", async () => {
775
836
  const tpl = await readFile3(join5(input.templates, "log.md"), "utf8");
776
837
  return tpl.replace(/\{\{INIT_DATE\}\}/g, today).replace("{{DOMAIN}}", domain).replace("{{WIKI_LANG}}", canonicalLang);
@@ -802,7 +863,9 @@ async function runInit(input) {
802
863
  `lang: ${canonicalLang}`,
803
864
  `created: ${created.length}, preserved: ${preserved.length}`,
804
865
  `discovered tags: ${discovered_tags}`,
805
- 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"
806
869
  ].join("\n");
807
870
  if (created.length > 0) {
808
871
  appendLastOp(target, {
@@ -826,7 +889,11 @@ async function runInit(input) {
826
889
  imported_from_hermes: importedFromHermes,
827
890
  discovered_tags,
828
891
  humanHint,
829
- 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)
830
897
  })
831
898
  };
832
899
  }
@@ -1000,7 +1067,7 @@ import { mkdtempSync, writeFileSync, rmSync } from "fs";
1000
1067
  import { tmpdir } from "os";
1001
1068
  import { join as join7 } from "path";
1002
1069
  var ENTRY_RE = /^## \[/m;
1003
- var OPERATION_MARKER_RE = /<!--\s*skillwiki-(?:page-publish|log-op):([a-f0-9]{64})\s*-->/i;
1070
+ var OPERATION_MARKER_RE = /<!--\s*skillwiki-(?:page-publish|project-page-publish|log-op):([a-f0-9]{64})\s*-->/i;
1004
1071
  function splitLog(text) {
1005
1072
  const normalized = text.replace(/\r\n/g, "\n");
1006
1073
  const match = ENTRY_RE.exec(normalized);
@@ -1196,21 +1263,21 @@ function rollbackPath(vault, path, stage2, stage3) {
1196
1263
  function hasMarkers(path, text) {
1197
1264
  const lines = text.split(/\r?\n/);
1198
1265
  let open2 = false;
1199
- let sep2 = false;
1266
+ let sep4 = false;
1200
1267
  for (const line of lines) {
1201
1268
  if (line.startsWith("<<<<<<< ")) {
1202
1269
  open2 = true;
1203
- sep2 = false;
1270
+ sep4 = false;
1204
1271
  continue;
1205
1272
  }
1206
1273
  if (line === "=======" && open2) {
1207
- sep2 = true;
1274
+ sep4 = true;
1208
1275
  continue;
1209
1276
  }
1210
- if (line.startsWith(">>>>>>> ") && open2 && sep2) return true;
1277
+ if (line.startsWith(">>>>>>> ") && open2 && sep4) return true;
1211
1278
  if (line.startsWith(">>>>>>> ")) {
1212
1279
  open2 = false;
1213
- sep2 = false;
1280
+ sep4 = false;
1214
1281
  }
1215
1282
  }
1216
1283
  return false;
@@ -1322,10 +1389,12 @@ function marker(event) {
1322
1389
  return `<!-- skillwiki-log-event:${event.operation_id} -->`;
1323
1390
  }
1324
1391
  function bodyFor(event) {
1325
- if (event.kind === "page-publish") {
1392
+ if (event.kind === "page-publish" || event.kind === "project-page-publish") {
1326
1393
  const tax = Array.isArray(event.metadata.taxonomy_added) ? event.metadata.taxonomy_added : [];
1394
+ const project = typeof event.metadata.project === "string" ? event.metadata.project : void 0;
1327
1395
  return [
1328
1396
  `- Published: [[${event.target.replace(/\.md$/, "")}]]`,
1397
+ ...event.kind === "project-page-publish" && project ? [`- Project: [[${project}]]`] : [],
1329
1398
  `- Taxonomy: ${tax.length > 0 ? `added ${tax.join(", ")}` : "no additions"}`,
1330
1399
  event.note ? `- Note: ${event.note}` : ""
1331
1400
  ].filter(Boolean).join("\n");
@@ -1485,18 +1554,8 @@ async function runLogMaterialize(input) {
1485
1554
  // src/commands/log-migrate-legacy.ts
1486
1555
  import { readFileSync as readFileSync6 } from "fs";
1487
1556
  import { join as join10 } from "path";
1488
-
1489
- // src/utils/operation-id.ts
1490
- import { createHash as createHash2 } from "crypto";
1491
- function operationId(namespace, parts) {
1492
- const hash = createHash2("sha256").update(namespace).update("\0");
1493
- for (const part of parts) hash.update(part).update("\0");
1494
- return hash.digest("hex");
1495
- }
1496
-
1497
- // src/commands/log-migrate-legacy.ts
1498
1557
  var BLOCK_RE = /^## \[(\d{4}-\d{2}-\d{2})\][^\n]*$/gm;
1499
- var PUBLISH_RE = /<!--\s*skillwiki-page-publish:([a-f0-9]{64})\s*-->/i;
1558
+ var PUBLISH_RE = /<!--\s*skillwiki-(?:page-publish|project-page-publish):([a-f0-9]{64})\s*-->/i;
1500
1559
  function splitLegacyBlocks(text) {
1501
1560
  const matches = [...text.matchAll(BLOCK_RE)];
1502
1561
  if (matches.length === 0) return [];
@@ -2793,7 +2852,7 @@ function selfCheckReport(report) {
2793
2852
  if (lint.summary.info !== sumBySeverity("info")) {
2794
2853
  errors.push(`lint.summary.info=${lint.summary.info} but info bucket counts sum to ${sumBySeverity("info")}`);
2795
2854
  }
2796
- 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"]) {
2797
2856
  if (!report.coverage[component]) errors.push(`missing coverage entry for ${component}`);
2798
2857
  }
2799
2858
  for (const flag of report.risk_flags) {
@@ -2813,6 +2872,7 @@ function buildHumanHint(report) {
2813
2872
  }
2814
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`);
2815
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`);
2816
2876
  if (report.risk_flags.length > 0) {
2817
2877
  lines.push("risk flags:");
2818
2878
  for (const flag of report.risk_flags) {
@@ -2872,7 +2932,8 @@ function buildIncompleteReport(input, syncMode) {
2872
2932
  lint: { state: "unknown", status: "unknown", reason: "lint summary still running" },
2873
2933
  vault_sync: vaultSyncCoverage,
2874
2934
  query_readiness: { state: "unknown", status: "unknown", reason: "health run not complete" },
2875
- 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" }
2876
2937
  },
2877
2938
  components: {
2878
2939
  doctor: {
@@ -2900,6 +2961,11 @@ function buildIncompleteReport(input, syncMode) {
2900
2961
  status: "unknown",
2901
2962
  blocking: false,
2902
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: [] }
2903
2969
  }
2904
2970
  },
2905
2971
  risk_flags: [],
@@ -2954,6 +3020,26 @@ async function runHealth(input) {
2954
3020
  stale_sections: bucketCount(lint.result.data.buckets, "stale_sections")
2955
3021
  }
2956
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
+ };
2957
3043
  const doctorComponent = {
2958
3044
  status: doctorStatus,
2959
3045
  blocking: false,
@@ -2998,14 +3084,16 @@ async function runHealth(input) {
2998
3084
  lint: { state: "checked", status: lintComponent.status },
2999
3085
  vault_sync: vaultSyncCoverage,
3000
3086
  query_readiness: { state: "checked", status: queryReadiness.status },
3001
- source_freshness: { state: "checked", status: sourceFreshness.status }
3087
+ source_freshness: { state: "checked", status: sourceFreshness.status },
3088
+ source_lifecycle: { state: "checked", status: sourceLifecycle.status }
3002
3089
  },
3003
3090
  components: {
3004
3091
  doctor: doctorComponent,
3005
3092
  lint: lintComponent,
3006
3093
  vault_sync: vaultSync,
3007
3094
  query_readiness: queryReadiness,
3008
- source_freshness: sourceFreshness
3095
+ source_freshness: sourceFreshness,
3096
+ source_lifecycle: sourceLifecycle
3009
3097
  },
3010
3098
  risk_flags: riskFlags,
3011
3099
  details_included: false,
@@ -3052,7 +3140,7 @@ async function runHealth(input) {
3052
3140
  }
3053
3141
 
3054
3142
  // src/commands/archive.ts
3055
- 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";
3056
3144
  import { join as join18, dirname as dirname5 } from "path";
3057
3145
 
3058
3146
  // src/utils/delete-intent.ts
@@ -3123,18 +3211,21 @@ async function runArchive(input) {
3123
3211
  let relPath = lookup(scan.data.typedKnowledge);
3124
3212
  let isRaw = false;
3125
3213
  if (!relPath) {
3126
- relPath = lookup(scan.data.raw);
3214
+ relPath = input.page.includes("/") ? lookup(scan.data.raw) : void 0;
3127
3215
  isRaw = relPath != null;
3128
3216
  }
3129
3217
  if (!relPath) return { exitCode: ExitCode.ARCHIVE_TARGET_NOT_FOUND, result: err("ARCHIVE_TARGET_NOT_FOUND", { page: input.page }) };
3130
3218
  if (relPath.startsWith("_archive/")) return { exitCode: ExitCode.ARCHIVE_ALREADY_ARCHIVED, result: err("ARCHIVE_ALREADY_ARCHIVED", { page: relPath }) };
3131
3219
  const slug = relPath.replace(/\.md$/, "").split("/").pop();
3132
- 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, "/");
3133
3223
  const remoteRoot = normalizeRemoteRoot(input.remote);
3134
3224
  const remoteObjectPath = buildRemoteObjectPath(remoteRoot, relPath);
3135
3225
  let cascade;
3136
- if (input.cascade) {
3226
+ if (input.cascade || isRaw) {
3137
3227
  const wikilinkRefs = [];
3228
+ const citationRefs = [];
3138
3229
  const sourceArrayRefs = [];
3139
3230
  for (const page of scan.data.typedKnowledge) {
3140
3231
  if (page.relPath === relPath) continue;
@@ -3143,12 +3234,15 @@ async function runArchive(input) {
3143
3234
  if (!split.ok) continue;
3144
3235
  const wl = countWikilinks(split.data.body, slug);
3145
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 });
3146
3240
  const fm = extractFrontmatter(text);
3147
3241
  if (!fm.ok) continue;
3148
3242
  const sources = fm.data.sources;
3149
- 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))) {
3150
3244
  const before = sources.filter((s) => typeof s === "string");
3151
- const after = before.filter((s) => s !== relPath);
3245
+ const after = isRaw ? rewritten.sourcesAfter : before.filter((s) => s !== relPath);
3152
3246
  sourceArrayRefs.push({ page: page.relPath, sources_before: before, sources_after: after });
3153
3247
  }
3154
3248
  }
@@ -3163,9 +3257,23 @@ async function runArchive(input) {
3163
3257
  if (e instanceof Error && "code" in e && e.code !== "ENOENT") throw e;
3164
3258
  }
3165
3259
  }
3166
- cascade = { wikilink_refs: wikilinkRefs, index_refs: indexRefs, source_array_refs: sourceArrayRefs };
3167
- }
3168
- 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) {
3169
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).`;
3170
3278
  return {
3171
3279
  exitCode: ExitCode.OK,
@@ -3174,37 +3282,73 @@ async function runArchive(input) {
3174
3282
  archived_to: archivePath,
3175
3283
  index_updated: false,
3176
3284
  applied: false,
3285
+ ...rawApprovalToken ? { approval_token: rawApprovalToken } : {},
3177
3286
  cascade,
3178
3287
  ...remoteObjectPath ? { remote: { plannedDeletes: [remoteObjectPath], deleted: [] } } : {},
3179
3288
  humanHint: summary + (remoteObjectPath ? ` (remote planned ${input.remoteDelete ? "delete" : "preview"}: ${remoteObjectPath})` : "")
3180
3289
  })
3181
3290
  };
3182
3291
  }
3183
- 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) {
3184
3308
  for (const ref of cascade.source_array_refs) {
3185
3309
  const absPath = join18(input.vault, ref.page);
3186
3310
  const text = await readFile7(absPath, "utf8");
3187
- const split = splitFrontmatter(text);
3188
- if (!split.ok) continue;
3189
- const before = split.data.rawFrontmatter;
3190
- const newSourcesYaml = ref.sources_after.length === 0 ? "sources: []" : "sources:\n" + ref.sources_after.map((s) => ` - ${s}`).join("\n");
3191
- const fmRewritten = before.replace(
3192
- /^sources:\s*(?:\[[^\]]*\]|(?:\r?\n(?:\s*-\s.*))+)/m,
3193
- newSourcesYaml
3194
- );
3195
- if (fmRewritten === before) continue;
3196
3311
  if (!arraysEqual(ref.sources_after, ref.sources_before)) {
3197
- 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 = `---
3198
3325
  ${fmRewritten}
3199
- ---${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
+ }
3200
3342
  }
3201
3343
  }
3202
3344
  }
3203
- await mkdir6(dirname5(join18(input.vault, archivePath)), { recursive: true });
3204
- 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
+ }
3205
3349
  let indexUpdated = false;
3206
3350
  if (!isRaw) {
3207
- const { renderRootIndex: renderRootIndex2, writeRootIndexProjection: writeRootIndexProjection2 } = await import("./index-projection-HAXDEM2F.js");
3351
+ const { renderRootIndex: renderRootIndex2, writeRootIndexProjection: writeRootIndexProjection2 } = await import("./index-projection-FRWTZB5Y.js");
3208
3352
  const before = await readFile7(join18(input.vault, "index.md"), "utf8").catch(() => "");
3209
3353
  const fullTarget = relPath.replace(/\.md$/, "");
3210
3354
  const bare = fullTarget.split("/").pop() ?? fullTarget;
@@ -3237,7 +3381,7 @@ ${fmRewritten}
3237
3381
  }
3238
3382
  remote = pruned.data;
3239
3383
  }
3240
- const applied = input.cascade ? true : void 0;
3384
+ const applied = input.cascade || isRaw ? true : void 0;
3241
3385
  const cascadeNote = input.cascade ? ` (cascade: ${cascade.source_array_refs.length} src arrays updated, ${cascade.wikilink_refs.length} wikilinks reported)` : "";
3242
3386
  const remoteNote = remote ? ` (remote ${input.remoteDelete ? `deleted ${remote.deleted.length}` : `planned ${remote.plannedDeletes.length}`})` : "";
3243
3387
  return {
@@ -3294,6 +3438,15 @@ async function runRemove(input) {
3294
3438
  result: err("FILE_NOT_FOUND", { page: input.page })
3295
3439
  };
3296
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
+ }
3297
3450
  if (relPath.startsWith("_archive/")) {
3298
3451
  return {
3299
3452
  exitCode: ExitCode.USAGE,
@@ -3313,10 +3466,10 @@ async function runRemove(input) {
3313
3466
  const tombstonePath = await writeDeleteIntent(input.vault, intent);
3314
3467
  await unlink2(join19(input.vault, relPath));
3315
3468
  if (relPath.endsWith(".md") && !relPath.startsWith("raw/")) {
3316
- const { readFile: readFile17 } = await import("fs/promises");
3469
+ const { readFile: readFile20 } = await import("fs/promises");
3317
3470
  const { join: pathJoin } = await import("path");
3318
- const { renderRootIndex: renderRootIndex2, writeRootIndexProjection: writeRootIndexProjection2 } = await import("./index-projection-HAXDEM2F.js");
3319
- const before = await readFile17(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(() => "");
3320
3473
  const fullTarget = relPath.replace(/\.md$/, "");
3321
3474
  const bare = fullTarget.split("/").pop() ?? fullTarget;
3322
3475
  const hadIndexEntry = before.includes(`[[${fullTarget}]]`) || before.includes(`[[${bare}]]`);
@@ -3354,7 +3507,7 @@ async function runRemove(input) {
3354
3507
  }
3355
3508
 
3356
3509
  // src/commands/drift.ts
3357
- import { createHash as createHash3 } from "crypto";
3510
+ import { createHash as createHash2 } from "crypto";
3358
3511
 
3359
3512
  // src/utils/fetch.ts
3360
3513
  async function controlledFetch(url, opts) {
@@ -3433,7 +3586,7 @@ async function runDrift(input) {
3433
3586
  });
3434
3587
  continue;
3435
3588
  }
3436
- 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");
3437
3590
  const identity = assessSourceIdentity({
3438
3591
  rawPath: raw.relPath,
3439
3592
  sourceUrl,
@@ -3451,29 +3604,13 @@ async function runDrift(input) {
3451
3604
  continue;
3452
3605
  }
3453
3606
  const drifted2 = currentHash !== storedHash;
3454
- if (drifted2 && input.apply) {
3455
- const newFm = rawFrontmatter.replace(/^sha256:\s*[a-f0-9]+$/m, `sha256: ${currentHash}`);
3456
- const newText = `---
3457
- ${newFm}
3458
- ---
3459
- ${body}`;
3460
- await safeWritePage(raw.absPath, newText);
3461
- results.push({
3462
- raw_path: raw.relPath,
3463
- source_url: sourceUrl,
3464
- stored_sha256: storedHash,
3465
- current_sha256: currentHash,
3466
- status: "updated"
3467
- });
3468
- } else {
3469
- results.push({
3470
- raw_path: raw.relPath,
3471
- source_url: sourceUrl,
3472
- stored_sha256: storedHash,
3473
- current_sha256: currentHash,
3474
- status: drifted2 ? "drifted" : "unchanged"
3475
- });
3476
- }
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
+ });
3477
3614
  }
3478
3615
  const drifted = results.filter((r) => r.status === "drifted");
3479
3616
  const fetchFailed = results.filter((r) => r.status === "fetch_failed");
@@ -3492,14 +3629,7 @@ ${body}`;
3492
3629
  }
3493
3630
  if (fetchFailed.length > 0) hintLines.push(`fetch_failed: ${fetchFailed.length}`, ...fetchFailed.map((f) => ` ${f.raw_path}: ${f.fetch_error}`));
3494
3631
  if (updated.length > 0) hintLines.push(`updated: ${updated.length}`, ...updated.map((u) => ` ${u.raw_path}`));
3495
- if (input.apply && updated.length > 0) {
3496
- appendLastOp(input.vault, {
3497
- operation: "drift-apply",
3498
- summary: `updated ${updated.length} raw sources`,
3499
- files: updated.map((u) => u.raw_path),
3500
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
3501
- });
3502
- }
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");
3503
3633
  return {
3504
3634
  exitCode,
3505
3635
  result: ok({ scanned: results.length, drifted, fetch_failed: fetchFailed, identity_conflicts: identityConflicts, updated, newFiles: newResults, unchanged, humanHint: hintLines.join("\n") })
@@ -3973,7 +4103,7 @@ async function runTranscripts(input) {
3973
4103
  }
3974
4104
 
3975
4105
  // src/commands/compound.ts
3976
- 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";
3977
4107
  import { join as join23 } from "path";
3978
4108
  import { existsSync as existsSync7 } from "fs";
3979
4109
  import { readFile as readFile10 } from "fs/promises";
@@ -4134,7 +4264,7 @@ async function runCompound(input) {
4134
4264
  if (!existsSync7(compoundDir)) {
4135
4265
  await mkdir7(compoundDir, { recursive: true });
4136
4266
  }
4137
- await writeFile7(compoundPath, content, "utf8");
4267
+ await writeFile6(compoundPath, content, "utf8");
4138
4268
  }
4139
4269
  promoted.push(`${slug}.md`);
4140
4270
  }
@@ -4258,7 +4388,7 @@ no compound entries found`;
4258
4388
  }
4259
4389
 
4260
4390
  // src/commands/session-brief.ts
4261
- 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";
4262
4392
  import { join as join24, relative, sep } from "path";
4263
4393
  var MAX_WORDS = 900;
4264
4394
  async function runSessionBrief(input) {
@@ -4278,7 +4408,8 @@ async function runSessionBrief(input) {
4278
4408
  baseHealthWarnings,
4279
4409
  satelliteHealth,
4280
4410
  sessionPins,
4281
- memoryTopics
4411
+ memoryTopics,
4412
+ pendingSources
4282
4413
  ] = await Promise.all([
4283
4414
  loadTranscriptInfo(scan.data.raw),
4284
4415
  loadWorkItems(scan.data.workItems),
@@ -4286,7 +4417,8 @@ async function runSessionBrief(input) {
4286
4417
  loadHealthWarnings(input.vault),
4287
4418
  loadSatelliteHealth(input.vault),
4288
4419
  loadSessionPins(input.vault, project),
4289
- project ? loadMemoryTopics(input.vault, project) : Promise.resolve([])
4420
+ project ? loadMemoryTopics(input.vault, project) : Promise.resolve([]),
4421
+ loadPendingSources(input.vault, today)
4290
4422
  ]);
4291
4423
  const healthWarnings = [
4292
4424
  ...baseHealthWarnings,
@@ -4309,6 +4441,7 @@ async function runSessionBrief(input) {
4309
4441
  sessionPins,
4310
4442
  latestLogs,
4311
4443
  unclaimedCaptures,
4444
+ pendingSources,
4312
4445
  activeWork,
4313
4446
  latestDigest,
4314
4447
  projectLogs,
@@ -4343,6 +4476,7 @@ async function runSessionBrief(input) {
4343
4476
  log_updated: logUpdated,
4344
4477
  generated_at: generatedAt,
4345
4478
  session_pins: sessionPins,
4479
+ pending_sources: pendingSources,
4346
4480
  memory_topics: memoryTopics,
4347
4481
  humanHint
4348
4482
  })
@@ -4408,6 +4542,17 @@ async function loadTranscriptInfo(rawPages) {
4408
4542
  }
4409
4543
  return out;
4410
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
+ }
4411
4556
  async function loadWorkItems(workItemPages) {
4412
4557
  const out = [];
4413
4558
  for (const page of workItemPages.filter((p) => p.relPath.endsWith("/spec.md"))) {
@@ -4478,6 +4623,7 @@ function renderBrief(input) {
4478
4623
  appendPinnedContextSection(lines, input.sessionPins);
4479
4624
  appendSection(lines, "Active Work", input.activeWork, "No active project work found.");
4480
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.");
4481
4627
  appendSection(lines, "Recent Session Logs", input.project ? input.projectLogs : input.latestLogs, "No recent session logs found.");
4482
4628
  appendSection(lines, "Latest Agent Memory Trends", input.latestDigest, "No agent memory trends digest found.");
4483
4629
  appendMemoryTopicsSection(lines, input.project, input.memoryTopics);
@@ -4488,6 +4634,7 @@ function renderBrief(input) {
4488
4634
  "- `skillwiki status`",
4489
4635
  input.project ? `- \`skillwiki project-index ${input.project}\`` : '- `skillwiki query "active work"`',
4490
4636
  "- `skillwiki transcripts --since <date>`",
4637
+ "- `skillwiki sources pending`",
4491
4638
  ""
4492
4639
  );
4493
4640
  return lines.join("\n").trimEnd() + "\n";
@@ -4609,9 +4756,9 @@ async function writeBriefArtifacts(vault, input) {
4609
4756
  const previousComparable = comparableBrief(await readIfExists(metaPath));
4610
4757
  const nextComparable = comparableBrief(committed);
4611
4758
  const materialChange = previousComparable !== nextComparable;
4612
- await writeFile8(metaPath, committed, "utf8");
4613
- await writeFile8(cacheMdPath, input.brief, "utf8");
4614
- await writeFile8(cacheJsonPath, `${JSON.stringify({
4759
+ await writeFile7(metaPath, committed, "utf8");
4760
+ await writeFile7(cacheMdPath, input.brief, "utf8");
4761
+ await writeFile7(cacheJsonPath, `${JSON.stringify({
4615
4762
  project: input.project,
4616
4763
  brief: input.brief,
4617
4764
  word_count: input.wordCount,
@@ -4731,9 +4878,236 @@ import { join as join26 } from "path";
4731
4878
  import { createHash as createHash4 } from "crypto";
4732
4879
 
4733
4880
  // src/commands/page-publish.ts
4734
- import { realpathSync } from "fs";
4881
+ import { readFileSync as readFileSync12, realpathSync } from "fs";
4735
4882
  import { readFile as readFile12 } from "fs/promises";
4736
4883
  import { join as join25, resolve as resolve3 } from "path";
4884
+
4885
+ // src/utils/publication-approval.ts
4886
+ import { createHash as createHash3 } from "crypto";
4887
+ var APPROVAL_CONTRACT = "skillwiki-publication-approval-v1";
4888
+ var APPROVAL_TOKEN_VERSION = "swpub1";
4889
+ var SHA256_HEX = /^[0-9a-f]{64}$/;
4890
+ var PUBLISHERS = /* @__PURE__ */ new Set(["page", "project-page"]);
4891
+ function sha256Hex(bytes) {
4892
+ return createHash3("sha256").update(bytes).digest("hex");
4893
+ }
4894
+ function normalizeLogNote(note) {
4895
+ if (note === void 0) return "";
4896
+ return note.normalize("NFC").trim();
4897
+ }
4898
+ function isPlainObject2(value) {
4899
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4900
+ }
4901
+ function canonicalApprovalJson(payload) {
4902
+ const ordered = {
4903
+ contract: payload.contract,
4904
+ publisher: payload.publisher,
4905
+ draft_sha256: payload.draft_sha256,
4906
+ target: payload.target
4907
+ };
4908
+ if (payload.publisher === "project-page" && payload.project !== void 0) {
4909
+ ordered.project = payload.project;
4910
+ }
4911
+ ordered.log_note = payload.log_note;
4912
+ ordered.prior_target_sha256 = payload.prior_target_sha256;
4913
+ return JSON.stringify(ordered);
4914
+ }
4915
+ function base64UrlEncode(text) {
4916
+ return Buffer.from(text, "utf8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
4917
+ }
4918
+ function base64UrlDecode(segment) {
4919
+ if (!/^[A-Za-z0-9_-]+$/.test(segment)) {
4920
+ return err("APPROVAL_INVALID", { message: "payload is not base64url" });
4921
+ }
4922
+ const padded = segment + "=".repeat((4 - segment.length % 4) % 4);
4923
+ const b64 = padded.replace(/-/g, "+").replace(/_/g, "/");
4924
+ try {
4925
+ return ok(Buffer.from(b64, "base64").toString("utf8"));
4926
+ } catch (error) {
4927
+ return err("APPROVAL_INVALID", { message: `payload decode failed: ${String(error)}` });
4928
+ }
4929
+ }
4930
+ function validatePayloadShape(value) {
4931
+ if (!isPlainObject2(value)) {
4932
+ return err("APPROVAL_INVALID", { message: "payload must be a JSON object" });
4933
+ }
4934
+ if (value.contract !== APPROVAL_CONTRACT) {
4935
+ return err("APPROVAL_INVALID", { message: "unknown approval contract version" });
4936
+ }
4937
+ if (typeof value.publisher !== "string" || !PUBLISHERS.has(value.publisher)) {
4938
+ return err("APPROVAL_INVALID", { message: "unknown publisher kind" });
4939
+ }
4940
+ const publisher = value.publisher;
4941
+ if (typeof value.draft_sha256 !== "string" || !SHA256_HEX.test(value.draft_sha256)) {
4942
+ return err("APPROVAL_INVALID", { message: "draft_sha256 must be 64 lowercase hex chars" });
4943
+ }
4944
+ if (typeof value.target !== "string" || value.target.length === 0 || value.target.length > 500) {
4945
+ return err("APPROVAL_INVALID", { message: "invalid target" });
4946
+ }
4947
+ if (typeof value.log_note !== "string" || /[\r\n]/.test(value.log_note)) {
4948
+ return err("APPROVAL_INVALID", { message: "log_note must be a single line string" });
4949
+ }
4950
+ if (Buffer.byteLength(value.log_note, "utf8") > 500) {
4951
+ return err("APPROVAL_INVALID", { message: "log_note exceeds 500 UTF-8 bytes" });
4952
+ }
4953
+ const prior = value.prior_target_sha256;
4954
+ if (typeof prior !== "string" || prior !== "absent" && !SHA256_HEX.test(prior)) {
4955
+ return err("APPROVAL_INVALID", { message: "prior_target_sha256 must be hex or absent" });
4956
+ }
4957
+ if (publisher === "project-page") {
4958
+ if (typeof value.project !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(value.project)) {
4959
+ return err("APPROVAL_INVALID", { message: "project-page token requires a valid project slug" });
4960
+ }
4961
+ return ok({
4962
+ contract: APPROVAL_CONTRACT,
4963
+ publisher,
4964
+ draft_sha256: value.draft_sha256,
4965
+ target: value.target,
4966
+ project: value.project,
4967
+ log_note: value.log_note,
4968
+ prior_target_sha256: prior
4969
+ });
4970
+ }
4971
+ if (value.project !== void 0 && value.project !== null) {
4972
+ return err("APPROVAL_INVALID", { message: "page publisher token must omit project" });
4973
+ }
4974
+ return ok({
4975
+ contract: APPROVAL_CONTRACT,
4976
+ publisher,
4977
+ draft_sha256: value.draft_sha256,
4978
+ target: value.target,
4979
+ log_note: value.log_note,
4980
+ prior_target_sha256: prior
4981
+ });
4982
+ }
4983
+ function buildApprovalPayload(input) {
4984
+ const base = {
4985
+ contract: APPROVAL_CONTRACT,
4986
+ publisher: input.publisher,
4987
+ draft_sha256: input.draft_sha256,
4988
+ target: input.target,
4989
+ log_note: normalizeLogNote(input.log_note),
4990
+ prior_target_sha256: input.prior_target_sha256,
4991
+ ...input.publisher === "project-page" ? { project: input.project } : {}
4992
+ };
4993
+ return validatePayloadShape(base);
4994
+ }
4995
+ function encodeApprovalToken(payload) {
4996
+ const validated = validatePayloadShape(payload);
4997
+ if (!validated.ok) return validated;
4998
+ const json = canonicalApprovalJson(validated.data);
4999
+ const middle = base64UrlEncode(json);
5000
+ const checksum = sha256Hex(middle);
5001
+ return ok(`${APPROVAL_TOKEN_VERSION}.${middle}.${checksum}`);
5002
+ }
5003
+ function decodeApprovalToken(token) {
5004
+ if (typeof token !== "string" || token.length === 0) {
5005
+ return err("APPROVAL_INVALID", { message: "empty approval token" });
5006
+ }
5007
+ if (token.includes("\n") || token.includes("---\n") || token.includes("```")) {
5008
+ return err("APPROVAL_INVALID", { message: "token must not contain raw draft content" });
5009
+ }
5010
+ const parts = token.split(".");
5011
+ if (parts.length !== 3) {
5012
+ return err("APPROVAL_INVALID", { message: "token must have three segments" });
5013
+ }
5014
+ const [version, middle, checksum] = parts;
5015
+ if (version !== APPROVAL_TOKEN_VERSION) {
5016
+ return err("APPROVAL_INVALID", { message: "unknown token version" });
5017
+ }
5018
+ if (!SHA256_HEX.test(checksum)) {
5019
+ return err("APPROVAL_INVALID", { message: "checksum must be 64 lowercase hex chars" });
5020
+ }
5021
+ const expectedChecksum = sha256Hex(middle);
5022
+ if (expectedChecksum !== checksum) {
5023
+ return err("APPROVAL_INVALID", { message: "checksum mismatch" });
5024
+ }
5025
+ const decoded = base64UrlDecode(middle);
5026
+ if (!decoded.ok) return decoded;
5027
+ let parsed;
5028
+ try {
5029
+ parsed = JSON.parse(decoded.data);
5030
+ } catch {
5031
+ return err("APPROVAL_INVALID", { message: "payload is not valid JSON" });
5032
+ }
5033
+ const shaped = validatePayloadShape(parsed);
5034
+ if (!shaped.ok) return shaped;
5035
+ if (base64UrlEncode(canonicalApprovalJson(shaped.data)) !== middle) {
5036
+ return err("APPROVAL_INVALID", { message: "payload is not in canonical form" });
5037
+ }
5038
+ return shaped;
5039
+ }
5040
+ function verifyApprovalToken(token, expected) {
5041
+ const decoded = decodeApprovalToken(token);
5042
+ if (!decoded.ok) return decoded;
5043
+ const payload = decoded.data;
5044
+ if (payload.publisher !== expected.publisher) {
5045
+ return err("APPROVAL_MISMATCH", {
5046
+ field: "publisher",
5047
+ expected: expected.publisher,
5048
+ actual: payload.publisher
5049
+ });
5050
+ }
5051
+ if (payload.draft_sha256 !== expected.draft_sha256) {
5052
+ return err("APPROVAL_MISMATCH", {
5053
+ field: "draft_sha256",
5054
+ expected: expected.draft_sha256,
5055
+ actual: payload.draft_sha256
5056
+ });
5057
+ }
5058
+ if (payload.target !== expected.target) {
5059
+ return err("APPROVAL_MISMATCH", {
5060
+ field: "target",
5061
+ expected: expected.target,
5062
+ actual: payload.target
5063
+ });
5064
+ }
5065
+ if (payload.prior_target_sha256 !== expected.prior_target_sha256) {
5066
+ return err("APPROVAL_MISMATCH", {
5067
+ field: "prior_target_sha256",
5068
+ expected: expected.prior_target_sha256,
5069
+ actual: payload.prior_target_sha256
5070
+ });
5071
+ }
5072
+ const expectedNote = normalizeLogNote(expected.log_note);
5073
+ if (payload.log_note !== expectedNote) {
5074
+ return err("APPROVAL_MISMATCH", {
5075
+ field: "log_note",
5076
+ expected: expectedNote,
5077
+ actual: payload.log_note
5078
+ });
5079
+ }
5080
+ if (expected.publisher === "project-page") {
5081
+ if (payload.project !== expected.project) {
5082
+ return err("APPROVAL_MISMATCH", {
5083
+ field: "project",
5084
+ expected: expected.project,
5085
+ actual: payload.project
5086
+ });
5087
+ }
5088
+ } else if (payload.project !== void 0) {
5089
+ return err("APPROVAL_MISMATCH", {
5090
+ field: "project",
5091
+ expected: null,
5092
+ actual: payload.project
5093
+ });
5094
+ }
5095
+ return ok(payload);
5096
+ }
5097
+ function operationIdFromApproval(payload) {
5098
+ const namespace = `${payload.contract}:${payload.publisher}`;
5099
+ return operationId(namespace, [
5100
+ payload.draft_sha256,
5101
+ payload.target,
5102
+ payload.project ?? "",
5103
+ payload.log_note
5104
+ ]);
5105
+ }
5106
+ function redactApprovalTokens(text) {
5107
+ return text.replace(/swpub1\.[A-Za-z0-9_-]+\.[0-9a-f]{64}/g, "swpub1.[REDACTED]");
5108
+ }
5109
+
5110
+ // src/commands/page-publish.ts
4737
5111
  var DEFAULT_DEPS2 = {
4738
5112
  afterStage: async () => void 0,
4739
5113
  preflight: (input) => runManagedWritePreflight(input)
@@ -4758,6 +5132,16 @@ function errorExitCode(error) {
4758
5132
  return ExitCode.SYNC_LOCK_HELD;
4759
5133
  case "PREFLIGHT_FAILED":
4760
5134
  return ExitCode.PREFLIGHT_FAILED;
5135
+ case "APPROVAL_REQUIRED":
5136
+ return ExitCode.APPROVAL_REQUIRED;
5137
+ case "APPROVAL_INVALID":
5138
+ return ExitCode.APPROVAL_INVALID;
5139
+ case "APPROVAL_MISMATCH":
5140
+ return ExitCode.APPROVAL_MISMATCH;
5141
+ case "TARGET_DRIFT":
5142
+ return ExitCode.TARGET_DRIFT;
5143
+ case "USAGE":
5144
+ return ExitCode.USAGE;
4761
5145
  case "EVENT_IDENTITY_COLLISION":
4762
5146
  case "WRITE_FAILED":
4763
5147
  return ExitCode.WRITE_FAILED;
@@ -4765,9 +5149,6 @@ function errorExitCode(error) {
4765
5149
  return ExitCode.INVALID_FRONTMATTER;
4766
5150
  }
4767
5151
  }
4768
- function publicationId(target, content, logNote = "") {
4769
- return operationId("skillwiki-page-publish-v1", [target, content, logNote]);
4770
- }
4771
5152
  function resolveRootAggregateMode(env = process.env) {
4772
5153
  if (env.SKILLWIKI_ROOT_AGGREGATE_MODE === "source-only") return "source-only";
4773
5154
  return "dual";
@@ -4780,7 +5161,7 @@ function prepareFrozenPublication(input, source) {
4780
5161
  if (input.logNote !== void 0 && /[\r\n]/.test(input.logNote)) {
4781
5162
  return err("SCHEME_REJECTED", { message: "log note must be one line" });
4782
5163
  }
4783
- const logNote = input.logNote?.trim() || void 0;
5164
+ const logNote = normalizeLogNote(input.logNote) || void 0;
4784
5165
  if (logNote && Buffer.byteLength(logNote, "utf8") > 500) {
4785
5166
  return err("SCHEME_REJECTED", { message: "log note must be one line and at most 500 UTF-8 bytes" });
4786
5167
  }
@@ -4790,14 +5171,41 @@ function prepareFrozenPublication(input, source) {
4790
5171
  const date = (input.now ?? /* @__PURE__ */ new Date()).toISOString().slice(0, 10);
4791
5172
  const taxonomyComment = taxonomyCommentForPage(input.target, date);
4792
5173
  if (!taxonomyComment.ok) return taxonomyComment;
5174
+ const draftSha256 = sha256Hex(input.content);
5175
+ let priorTargetSha256 = input.priorTargetSha256;
5176
+ if (priorTargetSha256 === void 0) {
5177
+ try {
5178
+ const existing = readFileSync12(target.data.absolutePath, "utf8");
5179
+ priorTargetSha256 = sha256Hex(existing);
5180
+ } catch (error) {
5181
+ if (error.code === "ENOENT") priorTargetSha256 = "absent";
5182
+ else {
5183
+ return err("WRITE_FAILED", {
5184
+ path: target.data.absolutePath,
5185
+ message: String(error)
5186
+ });
5187
+ }
5188
+ }
5189
+ }
5190
+ const approvalPayload = buildApprovalPayload({
5191
+ publisher: "page",
5192
+ draft_sha256: draftSha256,
5193
+ target: input.target,
5194
+ log_note: logNote ?? "",
5195
+ prior_target_sha256: priorTargetSha256
5196
+ });
5197
+ if (!approvalPayload.ok) return approvalPayload;
4793
5198
  return ok({
4794
5199
  page: page.data,
4795
5200
  source,
4796
5201
  targetPath: target.data.absolutePath,
4797
5202
  logNote,
4798
- operationId: publicationId(input.target, input.content, logNote),
5203
+ operationId: operationIdFromApproval(approvalPayload.data),
4799
5204
  date,
4800
- taxonomyComment: taxonomyComment.data
5205
+ taxonomyComment: taxonomyComment.data,
5206
+ draftSha256,
5207
+ priorTargetSha256,
5208
+ approvalPayload: approvalPayload.data
4801
5209
  });
4802
5210
  }
4803
5211
  function preparePagePublicationFromContent(input) {
@@ -4944,7 +5352,7 @@ function redactDetail(detail) {
4944
5352
  if (detail === void 0) return void 0;
4945
5353
  try {
4946
5354
  const encoded = JSON.stringify(detail);
4947
- return JSON.parse(redactSensitiveContent(encoded).text);
5355
+ return JSON.parse(redactApprovalTokens(redactSensitiveContent(encoded).text));
4948
5356
  } catch {
4949
5357
  return { message: "unserializable error detail omitted" };
4950
5358
  }
@@ -4964,7 +5372,7 @@ function phaseFailure(stage, input, published, cause, context = {}, exitCode = E
4964
5372
  })
4965
5373
  };
4966
5374
  }
4967
- function successReceipt(input, taxonomyAdded, pageChanged, indexUpdated, logAppended, filesChanged, dryRun = false, receipt = null) {
5375
+ function successReceipt(input, taxonomyAdded, pageChanged, indexUpdated, logAppended, filesChanged, dryRun = false, receipt = null, approvalToken) {
4968
5376
  return {
4969
5377
  exitCode: ExitCode.OK,
4970
5378
  result: ok({
@@ -4980,6 +5388,9 @@ function successReceipt(input, taxonomyAdded, pageChanged, indexUpdated, logAppe
4980
5388
  files_changed: filesChanged,
4981
5389
  base_oid: receipt?.base_oid ?? null,
4982
5390
  write_mode: receipt?.mode ?? null,
5391
+ draft_sha256: input.draftSha256,
5392
+ target_before: input.priorTargetSha256,
5393
+ ...approvalToken ? { approval_token: approvalToken } : {},
4983
5394
  ...receipt ? {
4984
5395
  mutation_vault: receipt.mutation_vault,
4985
5396
  git_vault: receipt.git_vault,
@@ -5053,6 +5464,8 @@ async function previewPreparedPagePublication(input, vault) {
5053
5464
  ...index.data.changed ? ["index.md"] : [],
5054
5465
  ...logAppended ? ["log.md"] : []
5055
5466
  ];
5467
+ const token = encodeApprovalToken(input.approvalPayload);
5468
+ if (!token.ok) return { exitCode: errorExitCode(token.error), result: token };
5056
5469
  return successReceipt(
5057
5470
  input,
5058
5471
  reconciled.data.added,
@@ -5060,7 +5473,9 @@ async function previewPreparedPagePublication(input, vault) {
5060
5473
  index.data.changed,
5061
5474
  logAppended,
5062
5475
  filesChanged,
5063
- true
5476
+ true,
5477
+ null,
5478
+ token.data
5064
5479
  );
5065
5480
  }
5066
5481
  async function publishPreparedPageWithReceipt(input, vault, writeReceipt, deps = DEFAULT_DEPS2) {
@@ -5223,9 +5638,30 @@ async function publishPreparedPage(input, vault, deps = DEFAULT_DEPS2) {
5223
5638
  });
5224
5639
  }
5225
5640
  async function runPagePublish(input, deps = DEFAULT_DEPS2) {
5641
+ if (input.approve && !input.write) {
5642
+ return {
5643
+ exitCode: ExitCode.USAGE,
5644
+ result: err("USAGE", { message: "--approve requires --write" })
5645
+ };
5646
+ }
5226
5647
  const prepared = await preparePagePublication(input);
5227
5648
  if (!prepared.ok) return { exitCode: errorExitCode(prepared.error), result: prepared };
5228
5649
  if (!input.write) return previewPreparedPagePublication(prepared.data, input.vault);
5650
+ if (input.approve) {
5651
+ const verified = verifyApprovalToken(input.approve, {
5652
+ publisher: "page",
5653
+ draft_sha256: prepared.data.draftSha256,
5654
+ target: prepared.data.page.target,
5655
+ log_note: prepared.data.logNote ?? "",
5656
+ prior_target_sha256: prepared.data.priorTargetSha256
5657
+ });
5658
+ if (!verified.ok) {
5659
+ return {
5660
+ exitCode: errorExitCode(verified.error),
5661
+ result: err(verified.error, redactDetail(verified.detail))
5662
+ };
5663
+ }
5664
+ }
5229
5665
  return publishPreparedPage(prepared.data, input.vault, deps);
5230
5666
  }
5231
5667
 
@@ -5252,13 +5688,13 @@ function isUrl(source) {
5252
5688
  function todayIso() {
5253
5689
  return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
5254
5690
  }
5255
- function buildRawContent(sourceUrl, ingested, sha256, body) {
5691
+ function buildRawContent(sourceUrl, ingested, sha2562, body) {
5256
5692
  const lines = [
5257
5693
  "---",
5258
5694
  sourceUrl !== null ? `source_url: "${sourceUrl}"` : "source_url:",
5259
5695
  `created: ${ingested}`,
5260
5696
  `ingested: ${ingested}`,
5261
- `sha256: ${sha256}`,
5697
+ `sha256: ${sha2562}`,
5262
5698
  `ingested_by: wiki-ingest`,
5263
5699
  "---",
5264
5700
  "",
@@ -5372,7 +5808,7 @@ async function acquireRawCaptureLock(path) {
5372
5808
  if (error.code !== "EEXIST") {
5373
5809
  return err("WRITE_FAILED", { path: lockPath, phase: "raw-lock", message: String(error) });
5374
5810
  }
5375
- await new Promise((resolve4) => setTimeout(resolve4, 10));
5811
+ await new Promise((resolve6) => setTimeout(resolve6, 10));
5376
5812
  }
5377
5813
  }
5378
5814
  return err("WRITE_FAILED", { path: lockPath, phase: "raw-lock", message: "raw capture lock held" });
@@ -5464,7 +5900,7 @@ async function runIngest(input, pagePublishDeps = defaultPagePublishDeps()) {
5464
5900
  })
5465
5901
  };
5466
5902
  }
5467
- const sha256 = createHash4("sha256").update(Buffer.from(sourceContent, "utf8")).digest("hex");
5903
+ const sha2562 = createHash4("sha256").update(Buffer.from(sourceContent, "utf8")).digest("hex");
5468
5904
  const today = todayIso();
5469
5905
  const slug = slugify2(input.title);
5470
5906
  const tags = input.tags && input.tags.length > 0 ? input.tags : [];
@@ -5495,7 +5931,7 @@ async function runIngest(input, pagePublishDeps = defaultPagePublishDeps()) {
5495
5931
  path: rawAbsPath,
5496
5932
  sourceUrl,
5497
5933
  sourceContent,
5498
- sha256,
5934
+ sha256: sha2562,
5499
5935
  today
5500
5936
  });
5501
5937
  if (!resolvedRaw.ok) {
@@ -5552,11 +5988,11 @@ async function runIngest(input, pagePublishDeps = defaultPagePublishDeps()) {
5552
5988
  result: ok({
5553
5989
  raw_path: rawRelPath,
5554
5990
  typed_path: typedRelPath,
5555
- sha256,
5991
+ sha256: sha2562,
5556
5992
  dry_run: true,
5557
5993
  humanHint: [
5558
5994
  "DRY RUN \u2014 would create:",
5559
- ` ${rawRelPath} (sha256: ${sha256.slice(0, 12)}...)`,
5995
+ ` ${rawRelPath} (sha256: ${sha2562.slice(0, 12)}...)`,
5560
5996
  ` ${typedRelPath}`,
5561
5997
  ` type: ${input.type}, tags: [${tags.join(", ")}]`,
5562
5998
  input.provenance ? ` provenance: ${input.provenance}` : ""
@@ -5584,7 +6020,7 @@ async function runIngest(input, pagePublishDeps = defaultPagePublishDeps()) {
5584
6020
  path: rawAbsPath,
5585
6021
  sourceUrl,
5586
6022
  sourceContent,
5587
- sha256,
6023
+ sha256: sha2562,
5588
6024
  today,
5589
6025
  capture: resolvedRaw.data
5590
6026
  });
@@ -5651,11 +6087,11 @@ async function runIngest(input, pagePublishDeps = defaultPagePublishDeps()) {
5651
6087
  result: ok({
5652
6088
  raw_path: rawRelPath,
5653
6089
  typed_path: typedRelPath,
5654
- sha256,
6090
+ sha256: sha2562,
5655
6091
  dry_run: false,
5656
6092
  humanHint: [
5657
6093
  "created:",
5658
- ` ${rawRelPath} (sha256: ${sha256.slice(0, 12)}...)`,
6094
+ ` ${rawRelPath} (sha256: ${sha2562.slice(0, 12)}...)`,
5659
6095
  ` ${typedRelPath}`
5660
6096
  ].join("\n"),
5661
6097
  publication: published.result.data
@@ -6005,82 +6441,1361 @@ async function runTagReconcile(input) {
6005
6441
  };
6006
6442
  }
6007
6443
 
6008
- // src/commands/sync.ts
6009
- import { existsSync as existsSync8 } from "fs";
6010
- import { join as join28 } from "path";
6011
- import { execFileSync as execFileSync3 } from "child_process";
6012
-
6013
- // src/utils/vault-git-pathspec.ts
6014
- var VAULT_GENERATED_COMMIT_PATHS = [
6015
- ".skillwiki/last-op.json",
6016
- ".skillwiki/memory",
6017
- ".skillwiki/memory-topics.json"
6018
- ];
6019
- var VAULT_GENERATED_COMMIT_EXCLUDES = [
6020
- ...VAULT_GENERATED_COMMIT_PATHS.map((path) => `:!${path}`)
6021
- ];
6022
- var VAULT_COMMIT_PATHSPEC = [".", ...VAULT_GENERATED_COMMIT_EXCLUDES];
6023
- function stageVaultContentChanges(vault) {
6024
- gitStrict(vault, ["add", "-A", "--", "."]);
6025
- for (const generatedPath of VAULT_GENERATED_COMMIT_PATHS) {
6026
- try {
6027
- gitStrict(vault, ["reset", "HEAD", "--", generatedPath]);
6028
- } catch (_e) {
6029
- }
6030
- }
6031
- }
6444
+ // src/commands/project-page-publish.ts
6445
+ import { realpathSync as realpathSync4 } from "fs";
6446
+ import { mkdir as mkdir10, readFile as readFile15 } from "fs/promises";
6447
+ import { dirname as dirname8, join as join29, resolve as resolve5 } from "path";
6032
6448
 
6033
- // src/commands/sync.ts
6034
- function parseDirtyPaths(porcelain) {
6035
- if (!porcelain) return [];
6036
- return porcelain.split("\n").map((line) => line.trimEnd()).filter((line) => line.length >= 4).map((line) => {
6037
- const payload = line.length >= 3 && line[2] === " " ? line.slice(3) : line.length >= 2 && line[1] === " " ? line.slice(2) : line;
6038
- const arrow = payload.lastIndexOf(" -> ");
6039
- return arrow >= 0 ? payload.slice(arrow + 4) : payload;
6040
- }).filter((path) => path.length > 0);
6449
+ // src/utils/architecture-page.ts
6450
+ import { lstatSync, realpathSync as realpathSync2 } from "fs";
6451
+ import { dirname as dirname6, posix as posix2, relative as relative2, resolve as resolve4, sep as sep2 } from "path";
6452
+ var PROJECT_SLUG_RE = /^[a-z0-9][a-z0-9-]*$/;
6453
+ var ARCH_TARGET_RE = /^projects\/([a-z0-9][a-z0-9-]*)\/architecture\/([a-z0-9][a-z0-9._-]*\.md)$/;
6454
+ function invalidFrontmatter(target, issues) {
6455
+ return err("INVALID_FRONTMATTER", {
6456
+ target,
6457
+ errors: issues.map((issue) => ({ path: issue.path.join("."), message: issue.message }))
6458
+ });
6041
6459
  }
6042
- function splitNonEmptyLines(text) {
6043
- if (!text) return [];
6044
- return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
6460
+ function validateProjectSlug(slug) {
6461
+ if (!PROJECT_SLUG_RE.test(slug)) {
6462
+ return err("VAULT_PATH_INVALID", { project: slug, message: "invalid project slug" });
6463
+ }
6464
+ return ok(slug);
6045
6465
  }
6046
- function isTrackedNotePath(path) {
6047
- if (!path.endsWith(".md")) return false;
6048
- return !/^\.(skillwiki|claude|obsidian|antigravitycli|playwright-cli)\//.test(path);
6466
+ function validateArchitectureTarget(target, project) {
6467
+ const slug = validateProjectSlug(project);
6468
+ if (!slug.ok) return slug;
6469
+ if (target.length === 0 || posix2.isAbsolute(target) || target.includes("\\") || posix2.normalize(target) !== target || target.split("/").some((segment) => segment === "" || segment === "." || segment === "..")) {
6470
+ return err("VAULT_PATH_INVALID", { target, message: "unsafe architecture target" });
6471
+ }
6472
+ const match = ARCH_TARGET_RE.exec(target);
6473
+ if (!match) {
6474
+ return err("VAULT_PATH_INVALID", {
6475
+ target,
6476
+ message: "target must be projects/{slug}/architecture/{filename}.md"
6477
+ });
6478
+ }
6479
+ if (match[1] !== project) {
6480
+ return err("VAULT_PATH_INVALID", {
6481
+ target,
6482
+ project,
6483
+ message: "project slug does not match architecture target path"
6484
+ });
6485
+ }
6486
+ return ok(target);
6049
6487
  }
6050
- function refHasPath(vault, ref, path) {
6488
+ function assertArchitectureTargetInsideVault(vault, target, project) {
6489
+ const validated = validateArchitectureTarget(target, project);
6490
+ if (!validated.ok) return validated;
6491
+ let vaultReal;
6051
6492
  try {
6052
- execFileSync3("git", ["cat-file", "-e", `${ref}:${path}`], {
6053
- cwd: vault,
6054
- stdio: ["pipe", "pipe", "pipe"]
6055
- });
6056
- return true;
6493
+ vaultReal = realpathSync2(vault);
6057
6494
  } catch {
6058
- return false;
6495
+ return err("VAULT_PATH_INVALID", { target, message: "vault realpath failed" });
6059
6496
  }
6060
- }
6061
- function runSyncStatus(input) {
6062
- const vault = input.vault;
6063
- const includeStashes = input.includeStashes ?? false;
6064
- if (!existsSync8(join28(vault, ".git"))) {
6065
- return {
6066
- exitCode: ExitCode.VAULT_PATH_INVALID,
6067
- result: ok({
6068
- is_git_repo: false,
6069
- dirty: 0,
6070
- ahead: 0,
6071
- behind: 0,
6072
- unpromoted_note_paths: 0,
6073
- unpromoted_note_examples: [],
6074
- last_commit: "never",
6075
- status: "not_a_repo",
6076
- humanHint: "not a git repository"
6077
- })
6078
- };
6497
+ const absolutePath = resolve4(vaultReal, target);
6498
+ const parent = dirname6(absolutePath);
6499
+ let parentReal;
6500
+ try {
6501
+ parentReal = realpathSync2(parent);
6502
+ } catch {
6503
+ return err("VAULT_PATH_INVALID", { target, message: "target parent realpath failed" });
6079
6504
  }
6080
- enableGitLongPathsOnWindows(vault);
6081
- const porcelain = git(vault, ["status", "--porcelain"]);
6082
- const dirty = porcelain ? porcelain.split("\n").filter((l) => l.trim().length > 0).length : 0;
6083
- const dirtyPaths = parseDirtyPaths(porcelain);
6505
+ const parentRelative = relative2(vaultReal, parentReal).split(sep2).join("/");
6506
+ if (parentRelative === ".." || parentRelative.startsWith("../")) {
6507
+ return err("VAULT_PATH_INVALID", { target, message: "target parent escapes vault" });
6508
+ }
6509
+ if (parentReal !== parent) {
6510
+ return err("VAULT_PATH_INVALID", { target, message: "target parent may not be a symlink alias" });
6511
+ }
6512
+ let existingRealPath;
6513
+ try {
6514
+ const targetStat = lstatSync(absolutePath);
6515
+ if (targetStat.isSymbolicLink()) {
6516
+ return err("VAULT_PATH_INVALID", { target, message: "target may not be a symlink" });
6517
+ }
6518
+ if (!targetStat.isFile()) {
6519
+ return err("VAULT_PATH_INVALID", { target, message: "existing target must be a regular file" });
6520
+ }
6521
+ try {
6522
+ existingRealPath = realpathSync2(absolutePath);
6523
+ } catch {
6524
+ return err("VAULT_PATH_INVALID", { target, message: "target realpath failed" });
6525
+ }
6526
+ } catch (error) {
6527
+ if (error.code !== "ENOENT") {
6528
+ return err("VAULT_PATH_INVALID", { target, message: "target lstat failed" });
6529
+ }
6530
+ }
6531
+ return ok({ absolutePath, existingRealPath });
6532
+ }
6533
+ function provenanceIncludesProject(provenanceProjects, project) {
6534
+ if (!provenanceProjects) return false;
6535
+ const wanted = `[[${project}]]`;
6536
+ return provenanceProjects.some((entry) => entry === wanted || entry === project);
6537
+ }
6538
+ function prepareArchitecturePage(content, target, project, opts = { isNew: true }) {
6539
+ const safeTarget = validateArchitectureTarget(target, project);
6540
+ if (!safeTarget.ok) return safeTarget;
6541
+ const sensitive = scanSensitiveContent(content, { file: target });
6542
+ if (sensitive.length > 0) {
6543
+ return err("SENSITIVE_CONTENT_DETECTED", { file: target, findings: sensitive });
6544
+ }
6545
+ const frontmatter = extractFrontmatter(content);
6546
+ if (!frontmatter.ok) return frontmatter;
6547
+ const detected = detectSchema(frontmatter.data);
6548
+ if (detected.schema !== "typed-knowledge") {
6549
+ return err("SCHEME_REJECTED", {
6550
+ target,
6551
+ message: "architecture pages must use typed-knowledge frontmatter"
6552
+ });
6553
+ }
6554
+ const parsed = TypedKnowledgeSchema.safeParse(frontmatter.data);
6555
+ if (!parsed.success) return invalidFrontmatter(target, parsed.error.issues);
6556
+ if (parsed.data.type !== "concept") {
6557
+ return err("SCHEME_REJECTED", {
6558
+ target,
6559
+ type: parsed.data.type,
6560
+ message: "architecture pages require type: concept"
6561
+ });
6562
+ }
6563
+ const provenance = parsed.data.provenance;
6564
+ if (provenance !== "project" && provenance !== "mixed") {
6565
+ return err("SCHEME_REJECTED", {
6566
+ target,
6567
+ provenance,
6568
+ message: "architecture pages require provenance: project or mixed"
6569
+ });
6570
+ }
6571
+ if (!provenanceIncludesProject(parsed.data.provenance_projects, project)) {
6572
+ return err("SCHEME_REJECTED", {
6573
+ target,
6574
+ project,
6575
+ message: "provenance_projects must include the target project"
6576
+ });
6577
+ }
6578
+ const tags = [...parsed.data.tags];
6579
+ if (opts.isNew && !tags.includes("adr")) {
6580
+ return err("SCHEME_REJECTED", {
6581
+ target,
6582
+ message: "newly created architecture pages require the adr tag"
6583
+ });
6584
+ }
6585
+ return ok({
6586
+ target,
6587
+ project,
6588
+ title: parsed.data.title,
6589
+ type: "concept",
6590
+ tags,
6591
+ content,
6592
+ isNew: opts.isNew
6593
+ });
6594
+ }
6595
+
6596
+ // src/utils/publication-operation-journal.ts
6597
+ import {
6598
+ chmodSync,
6599
+ existsSync as existsSync8,
6600
+ mkdirSync as mkdirSync3,
6601
+ readFileSync as readFileSync13,
6602
+ realpathSync as realpathSync3,
6603
+ renameSync as renameSync3,
6604
+ unlinkSync,
6605
+ writeFileSync as writeFileSync5
6606
+ } from "fs";
6607
+ import { homedir } from "os";
6608
+ import { dirname as dirname7, join as join28 } from "path";
6609
+ var PUBLICATION_JOURNAL_SCHEMA = "skillwiki-publication-operation-v1";
6610
+ var PHASE_ORDER2 = [
6611
+ "locked",
6612
+ "taxonomy",
6613
+ "page",
6614
+ "verified",
6615
+ "project-index",
6616
+ "unlocked",
6617
+ "event",
6618
+ "log",
6619
+ "complete"
6620
+ ];
6621
+ function phaseIndex2(phase) {
6622
+ return PHASE_ORDER2.indexOf(phase);
6623
+ }
6624
+ function isValidPhase(phase) {
6625
+ return PHASE_ORDER2.includes(phase);
6626
+ }
6627
+ function vaultIdentity(vaultPath) {
6628
+ let real;
6629
+ try {
6630
+ real = realpathSync3(vaultPath);
6631
+ } catch {
6632
+ real = vaultPath;
6633
+ }
6634
+ return sha256Hex(real).slice(0, 16);
6635
+ }
6636
+ function publicationJournalRoot(home = process.env.HOME || homedir()) {
6637
+ return join28(home, ".skillwiki", "publication-operations");
6638
+ }
6639
+ function resolveJournalPaths(vaultPath, operationId2, home = process.env.HOME || homedir()) {
6640
+ const identity = vaultIdentity(vaultPath);
6641
+ const rootDir = publicationJournalRoot(home);
6642
+ const vaultDir = join28(rootDir, identity);
6643
+ return {
6644
+ rootDir,
6645
+ vaultDir,
6646
+ journalPath: join28(vaultDir, `${operationId2}.json`)
6647
+ };
6648
+ }
6649
+ function ensurePrivateDir(dir) {
6650
+ mkdirSync3(dir, { recursive: true, mode: 448 });
6651
+ try {
6652
+ chmodSync(dir, 448);
6653
+ } catch {
6654
+ }
6655
+ }
6656
+ function atomicWriteJson(path, body) {
6657
+ ensurePrivateDir(dirname7(path));
6658
+ const tmp = `${path}.tmp.${process.pid}.${Date.now()}`;
6659
+ writeFileSync5(tmp, body, { encoding: "utf8", mode: 384 });
6660
+ try {
6661
+ chmodSync(tmp, 384);
6662
+ } catch {
6663
+ }
6664
+ renameSync3(tmp, path);
6665
+ try {
6666
+ chmodSync(path, 384);
6667
+ } catch {
6668
+ }
6669
+ }
6670
+ function identityEqual(a, b) {
6671
+ return a.publisher === b.publisher && a.draft_sha256 === b.draft_sha256 && a.target === b.target && (a.project ?? "") === (b.project ?? "") && a.log_note_sha256 === b.log_note_sha256;
6672
+ }
6673
+ function validateRecord(value) {
6674
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
6675
+ return err("SCHEME_REJECTED", { message: "journal record must be an object" });
6676
+ }
6677
+ const rec = value;
6678
+ if (rec.schema !== PUBLICATION_JOURNAL_SCHEMA) {
6679
+ return err("SCHEME_REJECTED", { message: "unknown journal schema" });
6680
+ }
6681
+ if (typeof rec.operation_id !== "string" || !/^[0-9a-f]{64}$/.test(rec.operation_id)) {
6682
+ return err("SCHEME_REJECTED", { message: "invalid operation_id" });
6683
+ }
6684
+ if (typeof rec.phase !== "string" || !isValidPhase(rec.phase)) {
6685
+ return err("SCHEME_REJECTED", { message: "invalid phase" });
6686
+ }
6687
+ if (typeof rec.vault_identity !== "string" || !/^[0-9a-f]{16}$/.test(rec.vault_identity)) {
6688
+ return err("SCHEME_REJECTED", { message: "invalid vault_identity" });
6689
+ }
6690
+ if (typeof rec.published !== "boolean" || typeof rec.verified !== "boolean") {
6691
+ return err("SCHEME_REJECTED", { message: "published/verified must be booleans" });
6692
+ }
6693
+ if (!Array.isArray(rec.files_changed) || !rec.files_changed.every((f) => typeof f === "string")) {
6694
+ return err("SCHEME_REJECTED", { message: "files_changed must be string[]" });
6695
+ }
6696
+ if (typeof rec.updated_at !== "string") {
6697
+ return err("SCHEME_REJECTED", { message: "updated_at must be a string" });
6698
+ }
6699
+ const id = rec.identity;
6700
+ if (typeof id !== "object" || id === null || Array.isArray(id)) {
6701
+ return err("SCHEME_REJECTED", { message: "identity must be an object" });
6702
+ }
6703
+ const identity = id;
6704
+ if (identity.publisher !== "page" && identity.publisher !== "project-page") {
6705
+ return err("SCHEME_REJECTED", { message: "invalid identity.publisher" });
6706
+ }
6707
+ for (const field of ["draft_sha256", "target", "log_note_sha256"]) {
6708
+ if (typeof identity[field] !== "string") {
6709
+ return err("SCHEME_REJECTED", { message: `invalid identity.${field}` });
6710
+ }
6711
+ }
6712
+ if (identity.prior_target_sha256 !== void 0 && typeof identity.prior_target_sha256 !== "string") {
6713
+ return err("SCHEME_REJECTED", { message: "invalid identity.prior_target_sha256" });
6714
+ }
6715
+ for (const forbidden of ["token", "approval_token", "page_body", "content", "log_note", "credential", "password"]) {
6716
+ if (forbidden in rec || forbidden in identity) {
6717
+ return err("SCHEME_REJECTED", { message: `journal must not contain ${forbidden}` });
6718
+ }
6719
+ }
6720
+ return ok({
6721
+ schema: PUBLICATION_JOURNAL_SCHEMA,
6722
+ operation_id: rec.operation_id,
6723
+ phase: rec.phase,
6724
+ vault_identity: rec.vault_identity,
6725
+ identity: {
6726
+ publisher: identity.publisher,
6727
+ draft_sha256: identity.draft_sha256,
6728
+ target: identity.target,
6729
+ ...typeof identity.project === "string" ? { project: identity.project } : {},
6730
+ log_note_sha256: identity.log_note_sha256,
6731
+ ...typeof identity.prior_target_sha256 === "string" ? { prior_target_sha256: identity.prior_target_sha256 } : {}
6732
+ },
6733
+ published: rec.published,
6734
+ verified: rec.verified,
6735
+ files_changed: [...rec.files_changed],
6736
+ updated_at: rec.updated_at
6737
+ });
6738
+ }
6739
+ function buildIdentitySummary(input) {
6740
+ return {
6741
+ publisher: input.publisher,
6742
+ draft_sha256: input.draft_sha256,
6743
+ target: input.target,
6744
+ ...input.publisher === "project-page" && input.project ? { project: input.project } : {},
6745
+ log_note_sha256: sha256Hex(input.log_note),
6746
+ prior_target_sha256: input.prior_target_sha256
6747
+ };
6748
+ }
6749
+ function readPublicationJournal(vaultPath, operationId2, home = process.env.HOME || homedir()) {
6750
+ const { journalPath: journalPath2 } = resolveJournalPaths(vaultPath, operationId2, home);
6751
+ if (!existsSync8(journalPath2)) return ok(null);
6752
+ let text;
6753
+ try {
6754
+ text = readFileSync13(journalPath2, "utf8");
6755
+ } catch (error) {
6756
+ return err("WRITE_FAILED", { message: `read journal failed: ${String(error)}` });
6757
+ }
6758
+ let parsed;
6759
+ try {
6760
+ parsed = JSON.parse(text);
6761
+ } catch {
6762
+ return err("SCHEME_REJECTED", { message: "malformed journal JSON" });
6763
+ }
6764
+ return validateRecord(parsed);
6765
+ }
6766
+ function createPublicationJournal(input) {
6767
+ const home = input.home ?? process.env.HOME ?? homedir();
6768
+ const paths = resolveJournalPaths(input.vaultPath, input.operationId, home);
6769
+ if (existsSync8(paths.journalPath)) {
6770
+ return err("WRITE_FAILED", { message: "journal already exists", path: paths.journalPath });
6771
+ }
6772
+ const record = {
6773
+ schema: PUBLICATION_JOURNAL_SCHEMA,
6774
+ operation_id: input.operationId,
6775
+ phase: "locked",
6776
+ vault_identity: vaultIdentity(input.vaultPath),
6777
+ identity: input.identity,
6778
+ published: false,
6779
+ verified: false,
6780
+ files_changed: [],
6781
+ updated_at: (input.now ?? /* @__PURE__ */ new Date()).toISOString()
6782
+ };
6783
+ const body = `${JSON.stringify(record, null, 2)}
6784
+ `;
6785
+ try {
6786
+ ensurePrivateDir(paths.vaultDir);
6787
+ atomicWriteJson(paths.journalPath, body);
6788
+ } catch (error) {
6789
+ return err("WRITE_FAILED", { message: `create journal failed: ${String(error)}` });
6790
+ }
6791
+ return ok(record);
6792
+ }
6793
+ function advancePublicationJournal(input) {
6794
+ const home = input.home ?? process.env.HOME ?? homedir();
6795
+ const existing = readPublicationJournal(input.vaultPath, input.operationId, home);
6796
+ if (!existing.ok) return existing;
6797
+ if (!existing.data) {
6798
+ return err("RECOVERY_EVIDENCE_MISSING", {
6799
+ operation_id: input.operationId,
6800
+ message: "publication journal not found"
6801
+ });
6802
+ }
6803
+ const current = existing.data;
6804
+ if (!identityEqual(current.identity, input.identity)) {
6805
+ return err("APPROVAL_MISMATCH", {
6806
+ message: "journal identity does not match approved payload",
6807
+ operation_id: input.operationId
6808
+ });
6809
+ }
6810
+ const from = phaseIndex2(current.phase);
6811
+ const to = phaseIndex2(input.nextPhase);
6812
+ if (to < from) {
6813
+ return err("SCHEME_REJECTED", {
6814
+ message: "phase regression refused",
6815
+ from: current.phase,
6816
+ to: input.nextPhase
6817
+ });
6818
+ }
6819
+ if (to === from && input.nextPhase !== current.phase) {
6820
+ return err("SCHEME_REJECTED", { message: "invalid phase transition" });
6821
+ }
6822
+ const record = {
6823
+ ...current,
6824
+ phase: input.nextPhase,
6825
+ published: input.published ?? current.published,
6826
+ verified: input.verified ?? current.verified,
6827
+ files_changed: input.filesChanged ? [.../* @__PURE__ */ new Set([...current.files_changed, ...input.filesChanged])].sort() : current.files_changed,
6828
+ updated_at: (input.now ?? /* @__PURE__ */ new Date()).toISOString()
6829
+ };
6830
+ const paths = resolveJournalPaths(input.vaultPath, input.operationId, home);
6831
+ try {
6832
+ atomicWriteJson(paths.journalPath, `${JSON.stringify(record, null, 2)}
6833
+ `);
6834
+ } catch (error) {
6835
+ return err("WRITE_FAILED", { message: `update journal failed: ${String(error)}` });
6836
+ }
6837
+ return ok(record);
6838
+ }
6839
+ function deletePublicationJournal(vaultPath, operationId2, home = process.env.HOME || homedir()) {
6840
+ const { journalPath: journalPath2 } = resolveJournalPaths(vaultPath, operationId2, home);
6841
+ if (!existsSync8(journalPath2)) return ok({ deleted: false });
6842
+ try {
6843
+ unlinkSync(journalPath2);
6844
+ return ok({ deleted: true });
6845
+ } catch (error) {
6846
+ return err("WRITE_FAILED", { message: `delete journal failed: ${String(error)}` });
6847
+ }
6848
+ }
6849
+ function completePublicationJournal(input) {
6850
+ const advanced = advancePublicationJournal({
6851
+ ...input,
6852
+ nextPhase: "complete",
6853
+ published: true,
6854
+ verified: true
6855
+ });
6856
+ if (!advanced.ok) return advanced;
6857
+ const deleted = deletePublicationJournal(
6858
+ input.vaultPath,
6859
+ input.operationId,
6860
+ input.home ?? process.env.HOME ?? homedir()
6861
+ );
6862
+ if (!deleted.ok) return deleted;
6863
+ return ok({ completed: true });
6864
+ }
6865
+
6866
+ // src/commands/project-page-publish.ts
6867
+ var DEFAULT_DEPS3 = {
6868
+ afterStage: async () => void 0,
6869
+ preflight: (input) => runManagedWritePreflight(input)
6870
+ };
6871
+ function errorExitCode3(error) {
6872
+ switch (error) {
6873
+ case "FILE_NOT_FOUND":
6874
+ return ExitCode.FILE_NOT_FOUND;
6875
+ case "MISSING_CLOSING_DELIMITER":
6876
+ return ExitCode.MISSING_CLOSING_DELIMITER;
6877
+ case "SCHEME_REJECTED":
6878
+ case "NO_TAXONOMY_BLOCK":
6879
+ return ExitCode.SCHEME_REJECTED;
6880
+ case "VAULT_PATH_INVALID":
6881
+ return ExitCode.VAULT_PATH_INVALID;
6882
+ case "SENSITIVE_CONTENT_DETECTED":
6883
+ return ExitCode.SENSITIVE_CONTENT_DETECTED;
6884
+ case "SYNC_LOCK_HELD":
6885
+ return ExitCode.SYNC_LOCK_HELD;
6886
+ case "PREFLIGHT_FAILED":
6887
+ return ExitCode.PREFLIGHT_FAILED;
6888
+ case "APPROVAL_REQUIRED":
6889
+ return ExitCode.APPROVAL_REQUIRED;
6890
+ case "APPROVAL_INVALID":
6891
+ return ExitCode.APPROVAL_INVALID;
6892
+ case "APPROVAL_MISMATCH":
6893
+ return ExitCode.APPROVAL_MISMATCH;
6894
+ case "TARGET_DRIFT":
6895
+ return ExitCode.TARGET_DRIFT;
6896
+ case "RECOVERY_EVIDENCE_MISSING":
6897
+ return ExitCode.RECOVERY_EVIDENCE_MISSING;
6898
+ case "PROJECT_NOT_FOUND":
6899
+ return ExitCode.PROJECT_NOT_FOUND;
6900
+ case "USAGE":
6901
+ return ExitCode.USAGE;
6902
+ case "EVENT_IDENTITY_COLLISION":
6903
+ case "WRITE_FAILED":
6904
+ return ExitCode.WRITE_FAILED;
6905
+ default:
6906
+ return ExitCode.INVALID_FRONTMATTER;
6907
+ }
6908
+ }
6909
+ function redactDetail2(detail) {
6910
+ if (detail === void 0) return void 0;
6911
+ try {
6912
+ const encoded = JSON.stringify(detail);
6913
+ const redacted = redactApprovalTokens(redactSensitiveContent(encoded).text);
6914
+ return JSON.parse(redacted);
6915
+ } catch {
6916
+ return { message: "unserializable error detail omitted" };
6917
+ }
6918
+ }
6919
+ async function observeStage2(deps, stage) {
6920
+ try {
6921
+ await deps.afterStage(stage);
6922
+ return void 0;
6923
+ } catch (error) {
6924
+ return err("WRITE_FAILED", {
6925
+ message: redactApprovalTokens(`stage hook failed at ${stage}: ${String(error)}`)
6926
+ });
6927
+ }
6928
+ }
6929
+ async function readPriorTargetSha(targetPath) {
6930
+ try {
6931
+ const text = await readFile15(targetPath, "utf8");
6932
+ return ok(sha256Hex(text));
6933
+ } catch (error) {
6934
+ if (error.code === "ENOENT") return ok("absent");
6935
+ return err("WRITE_FAILED", { path: targetPath, message: String(error) });
6936
+ }
6937
+ }
6938
+ function validateLogNote(logNote) {
6939
+ if (logNote !== void 0 && /[\r\n]/.test(logNote)) {
6940
+ return err("SCHEME_REJECTED", { message: "log note must be one line" });
6941
+ }
6942
+ const normalized = normalizeLogNote(logNote);
6943
+ if (normalized && Buffer.byteLength(normalized, "utf8") > 500) {
6944
+ return err("SCHEME_REJECTED", { message: "log note must be one line and at most 500 UTF-8 bytes" });
6945
+ }
6946
+ if (normalized && scanSensitiveContent(normalized, { file: "project-page-publish log note" }).length > 0) {
6947
+ return err("SENSITIVE_CONTENT_DETECTED", {
6948
+ message: "log note contains sensitive authentication material"
6949
+ });
6950
+ }
6951
+ return ok(normalized);
6952
+ }
6953
+ async function prepareProjectPagePublication(input) {
6954
+ let content;
6955
+ try {
6956
+ content = await readFile15(input.draftPath, "utf8");
6957
+ } catch (error) {
6958
+ return err("FILE_NOT_FOUND", { path: input.draftPath, message: String(error) });
6959
+ }
6960
+ let draftRealPath;
6961
+ try {
6962
+ draftRealPath = realpathSync4(input.draftPath);
6963
+ } catch (error) {
6964
+ return err("VAULT_PATH_INVALID", {
6965
+ path: input.draftPath,
6966
+ message: `draft realpath failed: ${String(error)}`
6967
+ });
6968
+ }
6969
+ const target = assertArchitectureTargetInsideVault(input.vault, input.target, input.project);
6970
+ if (!target.ok) return target;
6971
+ if (resolve5(input.draftPath) === target.data.absolutePath || target.data.existingRealPath !== void 0 && draftRealPath === target.data.existingRealPath) {
6972
+ return err("VAULT_PATH_INVALID", { message: "draft must not alias the final target" });
6973
+ }
6974
+ const isNew = target.data.existingRealPath === void 0;
6975
+ const page = prepareArchitecturePage(content, input.target, input.project, { isNew });
6976
+ if (!page.ok) return page;
6977
+ const logNote = validateLogNote(input.logNote);
6978
+ if (!logNote.ok) return logNote;
6979
+ const prior = await readPriorTargetSha(target.data.absolutePath);
6980
+ if (!prior.ok) return prior;
6981
+ const draftSha256 = sha256Hex(content);
6982
+ const approvalPayload = buildApprovalPayload({
6983
+ publisher: "project-page",
6984
+ draft_sha256: draftSha256,
6985
+ target: input.target,
6986
+ project: input.project,
6987
+ log_note: logNote.data,
6988
+ prior_target_sha256: prior.data
6989
+ });
6990
+ if (!approvalPayload.ok) return approvalPayload;
6991
+ const date = (input.now ?? /* @__PURE__ */ new Date()).toISOString().slice(0, 10);
6992
+ const taxonomyComment = taxonomyCommentForPage(input.target, date);
6993
+ if (!taxonomyComment.ok) return taxonomyComment;
6994
+ const identity = buildIdentitySummary({
6995
+ publisher: "project-page",
6996
+ draft_sha256: draftSha256,
6997
+ target: input.target,
6998
+ project: input.project,
6999
+ log_note: logNote.data,
7000
+ prior_target_sha256: prior.data
7001
+ });
7002
+ return ok({
7003
+ page: page.data,
7004
+ source: { kind: "file", realPath: draftRealPath },
7005
+ targetPath: target.data.absolutePath,
7006
+ logNote: logNote.data,
7007
+ draftSha256,
7008
+ priorTargetSha256: prior.data,
7009
+ approvalPayload: approvalPayload.data,
7010
+ operationId: operationIdFromApproval(approvalPayload.data),
7011
+ identity,
7012
+ date,
7013
+ taxonomyComment: taxonomyComment.data
7014
+ });
7015
+ }
7016
+ function successReceipt2(prepared, opts) {
7017
+ return {
7018
+ exitCode: ExitCode.OK,
7019
+ result: ok({
7020
+ approval_required: true,
7021
+ ...opts.approvalToken ? { approval_token: opts.approvalToken } : {},
7022
+ operation_id: prepared.operationId,
7023
+ project: prepared.page.project,
7024
+ target: prepared.page.target,
7025
+ page_type: prepared.page.type,
7026
+ tags: [...prepared.page.tags],
7027
+ taxonomy_added: [...opts.taxonomyAdded],
7028
+ page_changed: opts.pageChanged,
7029
+ project_index_changed: opts.projectIndexChanged,
7030
+ log_appended: opts.logAppended,
7031
+ dry_run: opts.dryRun,
7032
+ files_changed: opts.filesChanged,
7033
+ draft_sha256: prepared.draftSha256,
7034
+ target_before: prepared.priorTargetSha256,
7035
+ base_oid: opts.receipt?.base_oid ?? null,
7036
+ write_mode: opts.receipt?.mode ?? null,
7037
+ ...opts.receipt ? {
7038
+ mutation_vault: opts.receipt.mutation_vault,
7039
+ git_vault: opts.receipt.git_vault,
7040
+ convergence_source: opts.receipt.convergence_source
7041
+ } : {},
7042
+ ...opts.receipt?.convergence_vault ? { convergence_vault: opts.receipt.convergence_vault } : {},
7043
+ ...opts.receipt?.host_id ? { host_id: opts.receipt.host_id } : {},
7044
+ humanHint: opts.dryRun ? `dry run: would publish ${prepared.page.target} (${prepared.operationId.slice(0, 12)})` : `published ${prepared.page.target} (${prepared.operationId.slice(0, 12)})`
7045
+ })
7046
+ };
7047
+ }
7048
+ function phaseFailure2(stage, prepared, published, verified, cause, context = {}, exitCode = ExitCode.WRITE_FAILED) {
7049
+ return {
7050
+ exitCode: exitCode === ExitCode.WRITE_FAILED ? errorExitCode3(cause.error) || ExitCode.WRITE_FAILED : exitCode,
7051
+ result: err(cause.error === "EVENT_IDENTITY_COLLISION" ? "WRITE_FAILED" : cause.error, {
7052
+ ...context,
7053
+ stage,
7054
+ published,
7055
+ verified,
7056
+ target: prepared.page.target,
7057
+ operation_id: prepared.operationId,
7058
+ retry_safe: stage !== "target" && cause.error !== "TARGET_DRIFT",
7059
+ cause_error: cause.error,
7060
+ cause_detail: redactDetail2(cause.detail)
7061
+ })
7062
+ };
7063
+ }
7064
+ async function previewProjectPagePublication(prepared, vault) {
7065
+ const schemaPath = join29(vault, "SCHEMA.md");
7066
+ let schemaText;
7067
+ try {
7068
+ schemaText = await readFile15(schemaPath, "utf8");
7069
+ } catch (error) {
7070
+ return {
7071
+ exitCode: ExitCode.FILE_NOT_FOUND,
7072
+ result: err("FILE_NOT_FOUND", { path: schemaPath, message: String(error) })
7073
+ };
7074
+ }
7075
+ const reconciled = reconcileTaxonomyDocument(schemaText, {
7076
+ tags: prepared.page.tags,
7077
+ comment: prepared.taxonomyComment
7078
+ });
7079
+ if (!reconciled.ok) return { exitCode: errorExitCode3(reconciled.error), result: reconciled };
7080
+ const prior = await readPriorTargetSha(prepared.targetPath);
7081
+ if (!prior.ok) return { exitCode: errorExitCode3(prior.error), result: prior };
7082
+ const pageChanged = prior.data === "absent" || prior.data !== prepared.draftSha256;
7083
+ const projectIndex = await renderProjectIndex(vault, prepared.page.project, { today: prepared.date });
7084
+ if (!projectIndex.ok) {
7085
+ if (projectIndex.error !== "PROJECT_NOT_FOUND") {
7086
+ return { exitCode: errorExitCode3(projectIndex.error), result: projectIndex };
7087
+ }
7088
+ }
7089
+ let projectIndexChanged = true;
7090
+ if (projectIndex.ok) {
7091
+ const indexPath = join29(vault, projectIndex.data.index_path);
7092
+ try {
7093
+ const existing = await readFile15(indexPath, "utf8");
7094
+ projectIndexChanged = existing !== projectIndex.data.text || pageChanged;
7095
+ } catch {
7096
+ projectIndexChanged = true;
7097
+ }
7098
+ }
7099
+ const logPath = join29(vault, "log.md");
7100
+ let logAppended = true;
7101
+ try {
7102
+ const logText = await readFile15(logPath, "utf8");
7103
+ logAppended = !logText.includes(`<!-- skillwiki-project-page-publish:${prepared.operationId} -->`);
7104
+ } catch {
7105
+ return {
7106
+ exitCode: ExitCode.FILE_NOT_FOUND,
7107
+ result: err("FILE_NOT_FOUND", { path: logPath })
7108
+ };
7109
+ }
7110
+ const token = encodeApprovalToken(prepared.approvalPayload);
7111
+ if (!token.ok) return { exitCode: errorExitCode3(token.error), result: token };
7112
+ const filesChanged = [
7113
+ ...reconciled.data.changed ? ["SCHEMA.md"] : [],
7114
+ ...pageChanged ? [prepared.page.target] : [],
7115
+ ...projectIndexChanged ? [`projects/${prepared.page.project}/knowledge.md`] : [],
7116
+ ...logAppended ? ["log.md"] : []
7117
+ ];
7118
+ return successReceipt2(prepared, {
7119
+ taxonomyAdded: reconciled.data.added,
7120
+ pageChanged,
7121
+ projectIndexChanged,
7122
+ logAppended,
7123
+ filesChanged,
7124
+ dryRun: true,
7125
+ approvalToken: token.data
7126
+ });
7127
+ }
7128
+ function emptyLockedState2() {
7129
+ return {
7130
+ taxonomyAdded: [],
7131
+ pageChanged: false,
7132
+ projectIndexChanged: false,
7133
+ published: false,
7134
+ verified: false,
7135
+ changed: /* @__PURE__ */ new Set()
7136
+ };
7137
+ }
7138
+ async function revalidatePriorUnderLock(prepared, vault) {
7139
+ const fresh = assertArchitectureTargetInsideVault(vault, prepared.page.target, prepared.page.project);
7140
+ if (!fresh.ok) return fresh;
7141
+ if (fresh.data.absolutePath !== prepared.targetPath) {
7142
+ return err("VAULT_PATH_INVALID", { message: "target canonical path changed after preparation" });
7143
+ }
7144
+ if (prepared.source.kind === "file" && fresh.data.existingRealPath !== void 0 && fresh.data.existingRealPath === prepared.source.realPath) {
7145
+ return err("VAULT_PATH_INVALID", { message: "draft now aliases the final target" });
7146
+ }
7147
+ const prior = await readPriorTargetSha(fresh.data.absolutePath);
7148
+ if (!prior.ok) return prior;
7149
+ if (prior.data !== prepared.priorTargetSha256) {
7150
+ if (prior.data === prepared.draftSha256) {
7151
+ return ok({ absolutePath: fresh.data.absolutePath });
7152
+ }
7153
+ return err("TARGET_DRIFT", {
7154
+ message: "target changed since approval",
7155
+ expected_prior: prepared.priorTargetSha256,
7156
+ actual: prior.data
7157
+ });
7158
+ }
7159
+ return ok({ absolutePath: fresh.data.absolutePath });
7160
+ }
7161
+ var PHASE_RANK = {
7162
+ locked: 0,
7163
+ taxonomy: 1,
7164
+ page: 2,
7165
+ verified: 3,
7166
+ "project-index": 4,
7167
+ unlocked: 5,
7168
+ event: 6,
7169
+ log: 7,
7170
+ complete: 8
7171
+ };
7172
+ function phaseAtLeast(current, needed) {
7173
+ if (!current || !isValidPhase(current)) return false;
7174
+ return PHASE_RANK[current] >= PHASE_RANK[needed];
7175
+ }
7176
+ async function runLockedStages(prepared, vault, deps, journalHome) {
7177
+ const state = emptyLockedState2();
7178
+ const home = journalHome;
7179
+ const revalidated = await revalidatePriorUnderLock(prepared, vault);
7180
+ if (!revalidated.ok) {
7181
+ return {
7182
+ ok: false,
7183
+ exitCode: errorExitCode3(revalidated.error),
7184
+ stage: "target",
7185
+ state,
7186
+ cause: revalidated
7187
+ };
7188
+ }
7189
+ let currentPhase = "locked";
7190
+ const existingJournal = readPublicationJournal(vault, prepared.operationId, home);
7191
+ if (!existingJournal.ok) {
7192
+ return {
7193
+ ok: false,
7194
+ exitCode: errorExitCode3(existingJournal.error),
7195
+ stage: "journal",
7196
+ state,
7197
+ cause: existingJournal
7198
+ };
7199
+ }
7200
+ if (!existingJournal.data) {
7201
+ const journal = createPublicationJournal({
7202
+ vaultPath: vault,
7203
+ operationId: prepared.operationId,
7204
+ identity: prepared.identity,
7205
+ home
7206
+ });
7207
+ if (!journal.ok) {
7208
+ return {
7209
+ ok: false,
7210
+ exitCode: errorExitCode3(journal.error),
7211
+ stage: "journal",
7212
+ state,
7213
+ cause: journal
7214
+ };
7215
+ }
7216
+ } else {
7217
+ currentPhase = existingJournal.data.phase;
7218
+ state.published = existingJournal.data.published || phaseAtLeast(currentPhase, "page");
7219
+ state.verified = existingJournal.data.verified || phaseAtLeast(currentPhase, "verified");
7220
+ for (const f of existingJournal.data.files_changed) state.changed.add(f);
7221
+ }
7222
+ const journalHook = await observeStage2(deps, "journal");
7223
+ if (journalHook) {
7224
+ return { ok: false, exitCode: ExitCode.WRITE_FAILED, stage: "journal", state, cause: journalHook };
7225
+ }
7226
+ const schemaPath = join29(vault, "SCHEMA.md");
7227
+ {
7228
+ let schemaText;
7229
+ try {
7230
+ schemaText = await readFile15(schemaPath, "utf8");
7231
+ } catch (error) {
7232
+ return {
7233
+ ok: false,
7234
+ exitCode: ExitCode.WRITE_FAILED,
7235
+ stage: "taxonomy",
7236
+ state,
7237
+ cause: err("WRITE_FAILED", { message: String(error) })
7238
+ };
7239
+ }
7240
+ const reconciled = reconcileTaxonomyDocument(schemaText, {
7241
+ tags: prepared.page.tags,
7242
+ comment: prepared.taxonomyComment
7243
+ });
7244
+ if (!reconciled.ok) {
7245
+ return {
7246
+ ok: false,
7247
+ exitCode: errorExitCode3(reconciled.error),
7248
+ stage: "taxonomy",
7249
+ state,
7250
+ cause: reconciled
7251
+ };
7252
+ }
7253
+ state.taxonomyAdded = reconciled.data.added;
7254
+ if (reconciled.data.changed) {
7255
+ const schemaWrite = await atomicWriteText(schemaPath, reconciled.data.text);
7256
+ if (!schemaWrite.ok) {
7257
+ return { ok: false, exitCode: ExitCode.WRITE_FAILED, stage: "taxonomy", state, cause: schemaWrite };
7258
+ }
7259
+ if (schemaWrite.data.changed) state.changed.add("SCHEMA.md");
7260
+ }
7261
+ if (!phaseAtLeast(currentPhase, "taxonomy")) {
7262
+ advancePublicationJournal({
7263
+ vaultPath: vault,
7264
+ operationId: prepared.operationId,
7265
+ identity: prepared.identity,
7266
+ nextPhase: "taxonomy",
7267
+ filesChanged: [...state.changed],
7268
+ home
7269
+ });
7270
+ currentPhase = "taxonomy";
7271
+ }
7272
+ const taxonomyHook = await observeStage2(deps, "taxonomy");
7273
+ if (taxonomyHook) {
7274
+ return { ok: false, exitCode: ExitCode.WRITE_FAILED, stage: "taxonomy", state, cause: taxonomyHook };
7275
+ }
7276
+ }
7277
+ if (!phaseAtLeast(currentPhase, "page")) {
7278
+ try {
7279
+ await mkdir10(dirname8(prepared.targetPath), { recursive: true });
7280
+ } catch (error) {
7281
+ return {
7282
+ ok: false,
7283
+ exitCode: ExitCode.WRITE_FAILED,
7284
+ stage: "page",
7285
+ state,
7286
+ cause: err("WRITE_FAILED", { message: String(error) })
7287
+ };
7288
+ }
7289
+ const pageWrite = await safeWritePage(prepared.targetPath, prepared.page.content);
7290
+ if (!pageWrite.ok) {
7291
+ return { ok: false, exitCode: errorExitCode3(pageWrite.error), stage: "page", state, cause: pageWrite };
7292
+ }
7293
+ state.pageChanged = pageWrite.data.changed;
7294
+ if (state.pageChanged) state.changed.add(prepared.page.target);
7295
+ state.published = true;
7296
+ advancePublicationJournal({
7297
+ vaultPath: vault,
7298
+ operationId: prepared.operationId,
7299
+ identity: prepared.identity,
7300
+ nextPhase: "page",
7301
+ published: true,
7302
+ filesChanged: [...state.changed],
7303
+ home
7304
+ });
7305
+ currentPhase = "page";
7306
+ const pageHook = await observeStage2(deps, "page");
7307
+ if (pageHook) {
7308
+ return { ok: false, exitCode: ExitCode.WRITE_FAILED, stage: "page", state, cause: pageHook };
7309
+ }
7310
+ } else {
7311
+ state.published = true;
7312
+ }
7313
+ if (!phaseAtLeast(currentPhase, "verified")) {
7314
+ let visible;
7315
+ let visibleSchema;
7316
+ try {
7317
+ [visible, visibleSchema] = await Promise.all([
7318
+ readFile15(prepared.targetPath, "utf8"),
7319
+ readFile15(schemaPath, "utf8")
7320
+ ]);
7321
+ } catch (error) {
7322
+ return {
7323
+ ok: false,
7324
+ exitCode: ExitCode.WRITE_FAILED,
7325
+ stage: "verify",
7326
+ state,
7327
+ cause: err("WRITE_FAILED", { message: String(error) })
7328
+ };
7329
+ }
7330
+ const visiblePage = prepareArchitecturePage(visible, prepared.page.target, prepared.page.project, {
7331
+ isNew: false
7332
+ });
7333
+ const visibleTaxonomy = extractTaxonomy(visibleSchema);
7334
+ if (!visiblePage.ok || visible !== prepared.page.content || sha256Hex(visible) !== prepared.draftSha256 || !visibleTaxonomy.ok || prepared.page.tags.some((tag) => !visibleTaxonomy.data.includes(tag))) {
7335
+ return {
7336
+ ok: false,
7337
+ exitCode: ExitCode.WRITE_FAILED,
7338
+ stage: "verify",
7339
+ state,
7340
+ cause: err("WRITE_FAILED", { message: "published bytes or taxonomy verification failed" })
7341
+ };
7342
+ }
7343
+ state.verified = true;
7344
+ advancePublicationJournal({
7345
+ vaultPath: vault,
7346
+ operationId: prepared.operationId,
7347
+ identity: prepared.identity,
7348
+ nextPhase: "verified",
7349
+ published: true,
7350
+ verified: true,
7351
+ filesChanged: [...state.changed],
7352
+ home
7353
+ });
7354
+ currentPhase = "verified";
7355
+ const verifyHook = await observeStage2(deps, "verify");
7356
+ if (verifyHook) {
7357
+ return { ok: false, exitCode: ExitCode.WRITE_FAILED, stage: "verify", state, cause: verifyHook };
7358
+ }
7359
+ } else {
7360
+ state.verified = true;
7361
+ }
7362
+ if (!phaseAtLeast(currentPhase, "project-index")) {
7363
+ const rendered = await renderProjectIndex(vault, prepared.page.project, { today: prepared.date });
7364
+ if (!rendered.ok) {
7365
+ return {
7366
+ ok: false,
7367
+ exitCode: errorExitCode3(rendered.error),
7368
+ stage: "project-index",
7369
+ state,
7370
+ cause: rendered
7371
+ };
7372
+ }
7373
+ const knowledgePath = join29(vault, rendered.data.index_path);
7374
+ try {
7375
+ await mkdir10(dirname8(knowledgePath), { recursive: true });
7376
+ } catch (error) {
7377
+ return {
7378
+ ok: false,
7379
+ exitCode: ExitCode.WRITE_FAILED,
7380
+ stage: "project-index",
7381
+ state,
7382
+ cause: err("WRITE_FAILED", { message: String(error) })
7383
+ };
7384
+ }
7385
+ const knowledgeWrite = await atomicWriteText(knowledgePath, rendered.data.text);
7386
+ if (!knowledgeWrite.ok) {
7387
+ return {
7388
+ ok: false,
7389
+ exitCode: ExitCode.WRITE_FAILED,
7390
+ stage: "project-index",
7391
+ state,
7392
+ cause: knowledgeWrite
7393
+ };
7394
+ }
7395
+ state.projectIndexChanged = knowledgeWrite.data.changed;
7396
+ if (state.projectIndexChanged) state.changed.add(rendered.data.index_path);
7397
+ try {
7398
+ const visibleIndex = await readFile15(knowledgePath, "utf8");
7399
+ if (visibleIndex !== rendered.data.text) {
7400
+ return {
7401
+ ok: false,
7402
+ exitCode: ExitCode.WRITE_FAILED,
7403
+ stage: "project-index",
7404
+ state,
7405
+ cause: err("WRITE_FAILED", { message: "project knowledge.md verification failed" })
7406
+ };
7407
+ }
7408
+ } catch (error) {
7409
+ return {
7410
+ ok: false,
7411
+ exitCode: ExitCode.WRITE_FAILED,
7412
+ stage: "project-index",
7413
+ state,
7414
+ cause: err("WRITE_FAILED", { message: String(error) })
7415
+ };
7416
+ }
7417
+ advancePublicationJournal({
7418
+ vaultPath: vault,
7419
+ operationId: prepared.operationId,
7420
+ identity: prepared.identity,
7421
+ nextPhase: "project-index",
7422
+ published: true,
7423
+ verified: true,
7424
+ filesChanged: [...state.changed],
7425
+ home
7426
+ });
7427
+ currentPhase = "project-index";
7428
+ const indexHook = await observeStage2(deps, "project-index");
7429
+ if (indexHook) {
7430
+ return {
7431
+ ok: false,
7432
+ exitCode: ExitCode.WRITE_FAILED,
7433
+ stage: "project-index",
7434
+ state,
7435
+ cause: indexHook
7436
+ };
7437
+ }
7438
+ }
7439
+ return { ok: true, data: state };
7440
+ }
7441
+ function renderProjectPublicationLog(prepared, added) {
7442
+ return [
7443
+ `## [${prepared.date}] project-page-publish | ${prepared.page.target}`,
7444
+ "",
7445
+ `- Published: [[${prepared.page.target.replace(/\.md$/, "")}]]`,
7446
+ `- Project: [[${prepared.page.project}]]`,
7447
+ `- Taxonomy: ${added.length > 0 ? `added ${added.join(", ")}` : "no additions"}`,
7448
+ ...prepared.logNote ? [`- Note: ${prepared.logNote}`] : []
7449
+ ].join("\n");
7450
+ }
7451
+ async function publishWithReceipt(prepared, vault, writeReceipt, deps, journalHome) {
7452
+ const canonicalVault = resolve5(vault);
7453
+ if (writeReceipt.mutation_vault !== canonicalVault) {
7454
+ return {
7455
+ exitCode: ExitCode.PREFLIGHT_FAILED,
7456
+ result: err("PREFLIGHT_FAILED", {
7457
+ reason: "mutation-vault-receipt-mismatch",
7458
+ expected: writeReceipt.mutation_vault,
7459
+ actual: canonicalVault
7460
+ })
7461
+ };
7462
+ }
7463
+ if (writeReceipt.base_oid) {
7464
+ const head = writeReceipt.git_vault ? git(writeReceipt.git_vault, ["rev-parse", "HEAD"]) : null;
7465
+ if (head !== writeReceipt.base_oid) {
7466
+ return {
7467
+ exitCode: ExitCode.PREFLIGHT_FAILED,
7468
+ result: err("PREFLIGHT_FAILED", {
7469
+ reason: "base-oid-drift",
7470
+ expected: writeReceipt.base_oid,
7471
+ actual: head,
7472
+ git_vault: writeReceipt.git_vault
7473
+ })
7474
+ };
7475
+ }
7476
+ }
7477
+ const aggregateMode = resolveRootAggregateMode();
7478
+ let lock;
7479
+ try {
7480
+ lock = acquireOwnedSyncLock(vault, {
7481
+ summary: `project-page publish ${prepared.page.target}`,
7482
+ ttlMinutes: 1
7483
+ });
7484
+ } catch (error) {
7485
+ return {
7486
+ exitCode: ExitCode.WRITE_FAILED,
7487
+ result: err("WRITE_FAILED", { stage: "lock", message: String(error) })
7488
+ };
7489
+ }
7490
+ if (!lock.ok) return { exitCode: errorExitCode3(lock.error), result: lock };
7491
+ let primary;
7492
+ let released;
7493
+ try {
7494
+ primary = await runLockedStages(prepared, vault, deps, journalHome);
7495
+ } catch (error) {
7496
+ primary = {
7497
+ ok: false,
7498
+ exitCode: ExitCode.WRITE_FAILED,
7499
+ stage: "taxonomy",
7500
+ state: emptyLockedState2(),
7501
+ cause: err("WRITE_FAILED", { message: `unexpected primary-stage failure: ${String(error)}` })
7502
+ };
7503
+ } finally {
7504
+ released = releaseOwnedSyncLock(lock.data);
7505
+ }
7506
+ const primaryState = primary?.ok ? primary.data : primary?.state;
7507
+ if (released === void 0 || !released.ok || !released.data.released) {
7508
+ return phaseFailure2(
7509
+ "unlock",
7510
+ prepared,
7511
+ primaryState?.published ?? false,
7512
+ primaryState?.verified ?? false,
7513
+ released && !released.ok ? released : err("WRITE_FAILED", { message: "lock release did not run" })
7514
+ );
7515
+ }
7516
+ {
7517
+ const journalNow = readPublicationJournal(vault, prepared.operationId, journalHome);
7518
+ const phase = journalNow.ok && journalNow.data ? journalNow.data.phase : void 0;
7519
+ if (!phaseAtLeast(phase, "unlocked")) {
7520
+ advancePublicationJournal({
7521
+ vaultPath: vault,
7522
+ operationId: prepared.operationId,
7523
+ identity: prepared.identity,
7524
+ nextPhase: "unlocked",
7525
+ published: primaryState?.published,
7526
+ verified: primaryState?.verified,
7527
+ filesChanged: primaryState ? [...primaryState.changed] : void 0,
7528
+ home: journalHome
7529
+ });
7530
+ }
7531
+ }
7532
+ const unlockHook = await observeStage2(deps, "unlock");
7533
+ if (unlockHook) {
7534
+ return phaseFailure2(
7535
+ "unlock",
7536
+ prepared,
7537
+ primaryState?.published ?? false,
7538
+ primaryState?.verified ?? false,
7539
+ unlockHook
7540
+ );
7541
+ }
7542
+ if (primary === void 0) {
7543
+ return phaseFailure2(
7544
+ "taxonomy",
7545
+ prepared,
7546
+ false,
7547
+ false,
7548
+ err("WRITE_FAILED", { message: "locked publication produced no result" })
7549
+ );
7550
+ }
7551
+ if (!primary.ok) {
7552
+ return phaseFailure2(
7553
+ primary.stage,
7554
+ prepared,
7555
+ primary.state.published,
7556
+ primary.state.verified,
7557
+ primary.cause,
7558
+ void 0,
7559
+ primary.exitCode
7560
+ );
7561
+ }
7562
+ const state = primary.data;
7563
+ const event = await writeLogEvent(vault, {
7564
+ schema: "skillwiki-log-event/v1",
7565
+ operation_id: prepared.operationId,
7566
+ occurred_at: `${prepared.date}T00:00:00.000Z`,
7567
+ host_id: writeReceipt.host_id ?? "standalone",
7568
+ actor: "skillwiki-cli",
7569
+ kind: "project-page-publish",
7570
+ target: prepared.page.target,
7571
+ note: prepared.logNote || `Published ${prepared.page.title}`,
7572
+ metadata: {
7573
+ page_type: prepared.page.type,
7574
+ project: prepared.page.project,
7575
+ tags: [...prepared.page.tags].sort(),
7576
+ draft_sha256: prepared.draftSha256
7577
+ // Do not embed mutable base_oid in identity-critical fields; receipt still carries it.
7578
+ }
7579
+ });
7580
+ if (!event.ok) {
7581
+ return phaseFailure2(
7582
+ "event",
7583
+ prepared,
7584
+ true,
7585
+ true,
7586
+ event,
7587
+ void 0,
7588
+ event.error === "EVENT_IDENTITY_COLLISION" ? ExitCode.WRITE_FAILED : errorExitCode3(event.error)
7589
+ );
7590
+ }
7591
+ if (event.data.created) state.changed.add(event.data.path);
7592
+ {
7593
+ const journalNow = readPublicationJournal(vault, prepared.operationId, journalHome);
7594
+ const phase = journalNow.ok && journalNow.data ? journalNow.data.phase : void 0;
7595
+ if (!phaseAtLeast(phase, "event")) {
7596
+ advancePublicationJournal({
7597
+ vaultPath: vault,
7598
+ operationId: prepared.operationId,
7599
+ identity: prepared.identity,
7600
+ nextPhase: "event",
7601
+ published: true,
7602
+ verified: true,
7603
+ filesChanged: [...state.changed],
7604
+ home: journalHome
7605
+ });
7606
+ }
7607
+ }
7608
+ const eventHook = await observeStage2(deps, "event");
7609
+ if (eventHook) return phaseFailure2("event", prepared, true, true, eventHook);
7610
+ let logAppended = false;
7611
+ if (aggregateMode === "dual") {
7612
+ const log = await runLogAppend({
7613
+ vault,
7614
+ content: renderProjectPublicationLog(prepared, state.taxonomyAdded),
7615
+ operationId: prepared.operationId,
7616
+ strictLock: true,
7617
+ recordLastOp: false,
7618
+ eventKind: "project-page-publish"
7619
+ });
7620
+ if (!log.result.ok) return phaseFailure2("log", prepared, true, true, log.result);
7621
+ if (log.exitCode !== ExitCode.OK) {
7622
+ return phaseFailure2(
7623
+ "log",
7624
+ prepared,
7625
+ true,
7626
+ true,
7627
+ err("WRITE_FAILED", { message: "log append returned inconsistent success state" })
7628
+ );
7629
+ }
7630
+ logAppended = log.result.data.appended;
7631
+ if (logAppended) state.changed.add("log.md");
7632
+ {
7633
+ const journalNow = readPublicationJournal(vault, prepared.operationId, journalHome);
7634
+ const phase = journalNow.ok && journalNow.data ? journalNow.data.phase : void 0;
7635
+ if (!phaseAtLeast(phase, "log")) {
7636
+ advancePublicationJournal({
7637
+ vaultPath: vault,
7638
+ operationId: prepared.operationId,
7639
+ identity: prepared.identity,
7640
+ nextPhase: "log",
7641
+ published: true,
7642
+ verified: true,
7643
+ filesChanged: [...state.changed],
7644
+ home: journalHome
7645
+ });
7646
+ }
7647
+ }
7648
+ const logHook = await observeStage2(deps, "log");
7649
+ if (logHook) return phaseFailure2("log", prepared, true, true, logHook);
7650
+ }
7651
+ const completed = completePublicationJournal({
7652
+ vaultPath: vault,
7653
+ operationId: prepared.operationId,
7654
+ identity: prepared.identity,
7655
+ filesChanged: [...state.changed],
7656
+ home: journalHome
7657
+ });
7658
+ if (!completed.ok) {
7659
+ if (completed.error !== "RECOVERY_EVIDENCE_MISSING") {
7660
+ return phaseFailure2("journal-cleanup", prepared, true, true, completed);
7661
+ }
7662
+ }
7663
+ const cleanupHook = await observeStage2(deps, "journal-cleanup");
7664
+ if (cleanupHook) return phaseFailure2("journal-cleanup", prepared, true, true, cleanupHook);
7665
+ return successReceipt2(prepared, {
7666
+ taxonomyAdded: state.taxonomyAdded,
7667
+ pageChanged: state.pageChanged,
7668
+ projectIndexChanged: state.projectIndexChanged,
7669
+ logAppended,
7670
+ filesChanged: [...state.changed],
7671
+ dryRun: false,
7672
+ receipt: writeReceipt
7673
+ });
7674
+ }
7675
+ async function publishPrepared(prepared, vault, deps, journalHome) {
7676
+ return runManagedWriteTransaction({
7677
+ vault,
7678
+ command: `project-page publish ${prepared.page.target}`,
7679
+ allowImmutableRecord: true,
7680
+ preflight: deps.preflight,
7681
+ mutate: async (receipt) => publishWithReceipt(prepared, vault, receipt, deps, journalHome)
7682
+ });
7683
+ }
7684
+ async function runProjectPagePublish(input, deps = DEFAULT_DEPS3) {
7685
+ if (input.approve && !input.write) {
7686
+ return {
7687
+ exitCode: ExitCode.USAGE,
7688
+ result: err("USAGE", { message: "--approve requires --write" })
7689
+ };
7690
+ }
7691
+ if (input.write && !input.approve) {
7692
+ return {
7693
+ exitCode: ExitCode.APPROVAL_REQUIRED,
7694
+ result: err("APPROVAL_REQUIRED", {
7695
+ message: "project-page publish --write requires --approve <token> from a prior dry-run",
7696
+ project: input.project,
7697
+ target: input.target
7698
+ })
7699
+ };
7700
+ }
7701
+ const prepared = await prepareProjectPagePublication(input);
7702
+ if (!prepared.ok) return { exitCode: errorExitCode3(prepared.error), result: prepared };
7703
+ if (!input.write) {
7704
+ return previewProjectPagePublication(prepared.data, input.vault);
7705
+ }
7706
+ const verified = verifyApprovalToken(input.approve, {
7707
+ publisher: "project-page",
7708
+ draft_sha256: prepared.data.draftSha256,
7709
+ target: prepared.data.page.target,
7710
+ project: prepared.data.page.project,
7711
+ log_note: prepared.data.logNote,
7712
+ prior_target_sha256: prepared.data.priorTargetSha256
7713
+ });
7714
+ if (!verified.ok) {
7715
+ return {
7716
+ exitCode: errorExitCode3(verified.error),
7717
+ result: err(verified.error, redactDetail2(verified.detail))
7718
+ };
7719
+ }
7720
+ return publishPrepared(prepared.data, input.vault, deps, input.journalHome);
7721
+ }
7722
+
7723
+ // src/commands/sync.ts
7724
+ import { existsSync as existsSync9 } from "fs";
7725
+ import { join as join30 } from "path";
7726
+ import { execFileSync as execFileSync3 } from "child_process";
7727
+
7728
+ // src/utils/vault-git-pathspec.ts
7729
+ var VAULT_GENERATED_COMMIT_PATHS = [
7730
+ ".skillwiki/last-op.json",
7731
+ ".skillwiki/memory",
7732
+ ".skillwiki/memory-topics.json"
7733
+ ];
7734
+ var VAULT_GENERATED_COMMIT_EXCLUDES = [
7735
+ ...VAULT_GENERATED_COMMIT_PATHS.map((path) => `:!${path}`)
7736
+ ];
7737
+ var VAULT_COMMIT_PATHSPEC = [".", ...VAULT_GENERATED_COMMIT_EXCLUDES];
7738
+ function stageVaultContentChanges(vault) {
7739
+ gitStrict(vault, ["add", "-A", "--", "."]);
7740
+ for (const generatedPath of VAULT_GENERATED_COMMIT_PATHS) {
7741
+ try {
7742
+ gitStrict(vault, ["reset", "HEAD", "--", generatedPath]);
7743
+ } catch (_e) {
7744
+ }
7745
+ }
7746
+ }
7747
+
7748
+ // src/commands/sync.ts
7749
+ function parseDirtyPaths(porcelain) {
7750
+ if (!porcelain) return [];
7751
+ return porcelain.split("\n").map((line) => line.trimEnd()).filter((line) => line.length >= 4).map((line) => {
7752
+ const payload = line.length >= 3 && line[2] === " " ? line.slice(3) : line.length >= 2 && line[1] === " " ? line.slice(2) : line;
7753
+ const arrow = payload.lastIndexOf(" -> ");
7754
+ return arrow >= 0 ? payload.slice(arrow + 4) : payload;
7755
+ }).filter((path) => path.length > 0);
7756
+ }
7757
+ function splitNonEmptyLines(text) {
7758
+ if (!text) return [];
7759
+ return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
7760
+ }
7761
+ function isTrackedNotePath(path) {
7762
+ if (!path.endsWith(".md")) return false;
7763
+ return !/^\.(skillwiki|claude|obsidian|antigravitycli|playwright-cli)\//.test(path);
7764
+ }
7765
+ function refHasPath(vault, ref, path) {
7766
+ try {
7767
+ execFileSync3("git", ["cat-file", "-e", `${ref}:${path}`], {
7768
+ cwd: vault,
7769
+ stdio: ["pipe", "pipe", "pipe"]
7770
+ });
7771
+ return true;
7772
+ } catch {
7773
+ return false;
7774
+ }
7775
+ }
7776
+ function runSyncStatus(input) {
7777
+ const vault = input.vault;
7778
+ const includeStashes = input.includeStashes ?? false;
7779
+ if (!existsSync9(join30(vault, ".git"))) {
7780
+ return {
7781
+ exitCode: ExitCode.VAULT_PATH_INVALID,
7782
+ result: ok({
7783
+ is_git_repo: false,
7784
+ dirty: 0,
7785
+ ahead: 0,
7786
+ behind: 0,
7787
+ unpromoted_note_paths: 0,
7788
+ unpromoted_note_examples: [],
7789
+ last_commit: "never",
7790
+ status: "not_a_repo",
7791
+ humanHint: "not a git repository"
7792
+ })
7793
+ };
7794
+ }
7795
+ enableGitLongPathsOnWindows(vault);
7796
+ const porcelain = git(vault, ["status", "--porcelain"]);
7797
+ const dirty = porcelain ? porcelain.split("\n").filter((l) => l.trim().length > 0).length : 0;
7798
+ const dirtyPaths = parseDirtyPaths(porcelain);
6084
7799
  const untrackedPaths = splitNonEmptyLines(git(vault, ["ls-files", "--others", "--exclude-standard"]));
6085
7800
  const revOutput = git(vault, ["rev-list", "--left-right", "--count", "origin/HEAD...HEAD"]);
6086
7801
  let ahead = 0;
@@ -6168,7 +7883,7 @@ function runSyncStatus(input) {
6168
7883
  }
6169
7884
  async function runSyncPush(input) {
6170
7885
  const vault = input.vault;
6171
- if (!existsSync8(join28(vault, ".git"))) {
7886
+ if (!existsSync9(join30(vault, ".git"))) {
6172
7887
  return {
6173
7888
  exitCode: ExitCode.VAULT_PATH_INVALID,
6174
7889
  result: err("NOT_A_GIT_REPO", { path: vault })
@@ -6328,7 +8043,7 @@ function enableGitLongPathsOnWindows(vault) {
6328
8043
  }
6329
8044
  async function runSyncPull(input) {
6330
8045
  const vault = input.vault;
6331
- if (!existsSync8(join28(vault, ".git"))) {
8046
+ if (!existsSync9(join30(vault, ".git"))) {
6332
8047
  return {
6333
8048
  exitCode: ExitCode.VAULT_PATH_INVALID,
6334
8049
  result: err("NOT_A_GIT_REPO", { path: vault })
@@ -6443,7 +8158,7 @@ function runSyncPeers(input) {
6443
8158
  }
6444
8159
  function runSyncLock(input) {
6445
8160
  const vault = input.vault;
6446
- if (!existsSync8(vault)) {
8161
+ if (!existsSync9(vault)) {
6447
8162
  return {
6448
8163
  exitCode: ExitCode.VAULT_PATH_INVALID,
6449
8164
  result: err("VAULT_PATH_INVALID", { path: vault })
@@ -6478,7 +8193,7 @@ function runSyncLock(input) {
6478
8193
  }
6479
8194
  function runSyncUnlock(input) {
6480
8195
  const vault = input.vault;
6481
- if (!existsSync8(vault)) {
8196
+ if (!existsSync9(vault)) {
6482
8197
  return {
6483
8198
  exitCode: ExitCode.VAULT_PATH_INVALID,
6484
8199
  result: err("VAULT_PATH_INVALID", { path: vault })
@@ -6555,11 +8270,11 @@ function runSyncJournalClearStale(input) {
6555
8270
  }
6556
8271
 
6557
8272
  // src/commands/snapshot-maintenance.ts
6558
- import { existsSync as existsSync9, mkdirSync as mkdirSync3, appendFileSync } from "fs";
8273
+ import { existsSync as existsSync10, mkdirSync as mkdirSync4, appendFileSync } from "fs";
6559
8274
  import { createHash as createHash5 } from "crypto";
6560
8275
  import { execSync as execSync3, spawn } from "child_process";
6561
8276
  import { platform as platform2 } from "os";
6562
- import { join as join29, resolve as resolvePath2 } from "path";
8277
+ import { join as join31, resolve as resolvePath2 } from "path";
6563
8278
  var MAINTENANCE_SCHEMA_VERSION = 1;
6564
8279
  var MAINTENANCE_COMMAND = "snapshot-maintenance journal clear-stale";
6565
8280
  function canonicalize(p) {
@@ -6622,7 +8337,7 @@ async function runSnapshotMaintenanceDryRun(input) {
6622
8337
  audit(makeAuditEvent(input, fleetLoad.hostId, now, "refusal", "MAINTENANCE_WRONG_WORKTREE"));
6623
8338
  return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_WRONG_WORKTREE", `requested path '${requested}' is not the configured snapshot worktree '${canonicalize(configuredWorktree)}'`) };
6624
8339
  }
6625
- if (!existsSync9(requested) || !existsSync9(join29(requested, ".git"))) {
8340
+ if (!existsSync10(requested) || !existsSync10(join31(requested, ".git"))) {
6626
8341
  audit(makeAuditEvent(input, fleetLoad.hostId, now, "refusal", "MAINTENANCE_MISSING_GIT_REPO"));
6627
8342
  return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_MISSING_GIT_REPO", `snapshot worktree is not a git repository: ${requested}`) };
6628
8343
  }
@@ -6702,14 +8417,14 @@ async function resolveLiveVault(input) {
6702
8417
  return resolved.ok ? canonicalize(resolved.data.path) : void 0;
6703
8418
  }
6704
8419
  function snapshotMaintenanceAuditLogPath(home) {
6705
- const stateDir = platform2() === "darwin" ? join29(home, "Library", "Application Support", "vault-sync") : join29(home, ".local", "state", "vault-sync");
6706
- return join29(stateDir, "snapshot-maintenance-audit.jsonl");
8420
+ const stateDir = platform2() === "darwin" ? join31(home, "Library", "Application Support", "vault-sync") : join31(home, ".local", "state", "vault-sync");
8421
+ return join31(stateDir, "snapshot-maintenance-audit.jsonl");
6707
8422
  }
6708
8423
  function defaultAuditSink(home) {
6709
8424
  return (event) => {
6710
8425
  try {
6711
8426
  const logPath = snapshotMaintenanceAuditLogPath(home);
6712
- mkdirSync3(join29(logPath, ".."), { recursive: true });
8427
+ mkdirSync4(join31(logPath, ".."), { recursive: true });
6713
8428
  appendFileSync(logPath, JSON.stringify(event) + "\n", { encoding: "utf8" });
6714
8429
  } catch {
6715
8430
  }
@@ -6869,8 +8584,8 @@ function releaseSnapshotFlock(handle) {
6869
8584
  }
6870
8585
 
6871
8586
  // src/commands/backup.ts
6872
- import { statSync as statSync2, readdirSync as readdirSync2, readFileSync as readFileSync13, mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "fs";
6873
- import { join as join30, relative as relative2, dirname as dirname6 } from "path";
8587
+ import { statSync as statSync2, readdirSync as readdirSync2, readFileSync as readFileSync15, mkdirSync as mkdirSync5, writeFileSync as writeFileSync6 } from "fs";
8588
+ import { join as join32, relative as relative3, dirname as dirname9 } from "path";
6874
8589
  import { PutObjectCommand, HeadObjectCommand, ListObjectsV2Command, GetObjectCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
6875
8590
 
6876
8591
  // src/utils/s3-client.ts
@@ -6894,11 +8609,11 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([".git", ".obsidian", "_archive", "node_
6894
8609
  function* walkMarkdown(dir, base) {
6895
8610
  for (const entry of readdirSync2(dir, { withFileTypes: true })) {
6896
8611
  if (SKIP_DIRS.has(entry.name)) continue;
6897
- const full = join30(dir, entry.name);
8612
+ const full = join32(dir, entry.name);
6898
8613
  if (entry.isDirectory()) {
6899
8614
  yield* walkMarkdown(full, base);
6900
8615
  } else if (entry.name.endsWith(".md")) {
6901
- yield relative2(base, full).replace(/\\/g, "/");
8616
+ yield relative3(base, full).replace(/\\/g, "/");
6902
8617
  }
6903
8618
  }
6904
8619
  }
@@ -6917,7 +8632,7 @@ async function runBackupSync(input) {
6917
8632
  let failed = 0;
6918
8633
  const files = [...walkMarkdown(input.vault, input.vault)];
6919
8634
  for (const relPath of files) {
6920
- const absPath = join30(input.vault, relPath);
8635
+ const absPath = join32(input.vault, relPath);
6921
8636
  const localStat = statSync2(absPath);
6922
8637
  let needsUpload = true;
6923
8638
  try {
@@ -6936,7 +8651,7 @@ async function runBackupSync(input) {
6936
8651
  continue;
6937
8652
  }
6938
8653
  try {
6939
- const body = readFileSync13(absPath);
8654
+ const body = readFileSync15(absPath);
6940
8655
  await client.send(new PutObjectCommand({ Bucket: input.bucket, Key: relPath, Body: body }));
6941
8656
  uploaded++;
6942
8657
  } catch {
@@ -6993,7 +8708,7 @@ async function runBackupRestore(input) {
6993
8708
  const objects = list.Contents ?? [];
6994
8709
  for (const obj of objects) {
6995
8710
  if (!obj.Key) continue;
6996
- const localPath = join30(target, obj.Key);
8711
+ const localPath = join32(target, obj.Key);
6997
8712
  try {
6998
8713
  const localStat = statSync2(localPath);
6999
8714
  if (obj.LastModified && localStat.mtime > obj.LastModified) {
@@ -7006,8 +8721,8 @@ async function runBackupRestore(input) {
7006
8721
  const resp = await client.send(new GetObjectCommand({ Bucket: input.bucket, Key: obj.Key }));
7007
8722
  const body = await resp.Body?.transformToByteArray();
7008
8723
  if (body) {
7009
- mkdirSync4(dirname6(localPath), { recursive: true });
7010
- writeFileSync5(localPath, Buffer.from(body));
8724
+ mkdirSync5(dirname9(localPath), { recursive: true });
8725
+ writeFileSync6(localPath, Buffer.from(body));
7011
8726
  downloaded++;
7012
8727
  }
7013
8728
  } catch {
@@ -7039,11 +8754,11 @@ async function runBackupRestore(input) {
7039
8754
  }
7040
8755
 
7041
8756
  // src/commands/status.ts
7042
- import { existsSync as existsSync10, statSync as statSync3 } from "fs";
7043
- import { readFile as readFile15 } from "fs/promises";
7044
- import { join as join31 } from "path";
8757
+ import { existsSync as existsSync11, statSync as statSync3 } from "fs";
8758
+ import { readFile as readFile16 } from "fs/promises";
8759
+ import { join as join33 } from "path";
7045
8760
  async function runStatus(input) {
7046
- if (!existsSync10(input.vault)) {
8761
+ if (!existsSync11(input.vault)) {
7047
8762
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
7048
8763
  }
7049
8764
  const scan = await scanVault(input.vault);
@@ -7068,7 +8783,7 @@ async function runStatus(input) {
7068
8783
  const compound = scan.data.compound.length;
7069
8784
  let schemaVersion = "v1";
7070
8785
  try {
7071
- const schemaContent = await readFile15(join31(input.vault, "SCHEMA.md"), "utf8");
8786
+ const schemaContent = await readFile16(join33(input.vault, "SCHEMA.md"), "utf8");
7072
8787
  const versionMatch = schemaContent.match(/version:\s*["']?([^"'\s\n]+)/i);
7073
8788
  if (versionMatch) schemaVersion = versionMatch[1];
7074
8789
  } catch {
@@ -7128,8 +8843,8 @@ async function runStatus(input) {
7128
8843
  }
7129
8844
 
7130
8845
  // src/commands/seed.ts
7131
- import { mkdir as mkdir10, writeFile as writeFile9, stat as stat4 } from "fs/promises";
7132
- import { join as join32 } from "path";
8846
+ import { mkdir as mkdir11, writeFile as writeFile8, stat as stat4 } from "fs/promises";
8847
+ import { join as join34 } from "path";
7133
8848
  var TODAY = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
7134
8849
  var EXAMPLE_PAGES = {
7135
8850
  "entities/example-project.md": `---
@@ -7198,30 +8913,30 @@ Real sources are immutable after ingestion \u2014 never edit them.
7198
8913
  `;
7199
8914
  async function runSeed(input) {
7200
8915
  try {
7201
- await stat4(join32(input.vault, "SCHEMA.md"));
8916
+ await stat4(join34(input.vault, "SCHEMA.md"));
7202
8917
  } catch {
7203
8918
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { root: input.vault, reason: "SCHEMA.md missing \u2014 run `skillwiki init` first" }) };
7204
8919
  }
7205
8920
  const created = [];
7206
8921
  const skipped = [];
7207
8922
  for (const [relPath, content] of Object.entries(EXAMPLE_PAGES)) {
7208
- const absPath = join32(input.vault, relPath);
8923
+ const absPath = join34(input.vault, relPath);
7209
8924
  try {
7210
8925
  await stat4(absPath);
7211
8926
  skipped.push(relPath);
7212
8927
  } catch {
7213
- await mkdir10(join32(absPath, ".."), { recursive: true });
7214
- await writeFile9(absPath, content, "utf8");
8928
+ await mkdir11(join34(absPath, ".."), { recursive: true });
8929
+ await writeFile8(absPath, content, "utf8");
7215
8930
  created.push(relPath);
7216
8931
  }
7217
8932
  }
7218
- const rawPath = join32(input.vault, "raw", "articles", "example-source.md");
8933
+ const rawPath = join34(input.vault, "raw", "articles", "example-source.md");
7219
8934
  try {
7220
8935
  await stat4(rawPath);
7221
8936
  skipped.push("raw/articles/example-source.md");
7222
8937
  } catch {
7223
- await mkdir10(join32(rawPath, ".."), { recursive: true });
7224
- await writeFile9(rawPath, EXAMPLE_RAW, "utf8");
8938
+ await mkdir11(join34(rawPath, ".."), { recursive: true });
8939
+ await writeFile8(rawPath, EXAMPLE_RAW, "utf8");
7225
8940
  created.push("raw/articles/example-source.md");
7226
8941
  }
7227
8942
  if (created.length > 0) {
@@ -7243,9 +8958,9 @@ async function runSeed(input) {
7243
8958
  }
7244
8959
 
7245
8960
  // src/commands/canvas.ts
7246
- import { readFile as readFile16, writeFile as writeFile10 } from "fs/promises";
7247
- import { existsSync as existsSync11 } from "fs";
7248
- import { join as join33 } from "path";
8961
+ import { readFile as readFile17, writeFile as writeFile9 } from "fs/promises";
8962
+ import { existsSync as existsSync12 } from "fs";
8963
+ import { join as join35 } from "path";
7249
8964
  var NODE_WIDTH = 240;
7250
8965
  var NODE_HEIGHT = 60;
7251
8966
  var COLUMN_SPACING = 400;
@@ -7323,8 +9038,8 @@ function buildCanvasEdges(adjacency) {
7323
9038
  return edges;
7324
9039
  }
7325
9040
  async function runCanvasGenerate(input) {
7326
- const graphPath = input.graphPath ?? join33(input.vault, ".skillwiki", "graph.json");
7327
- if (!existsSync11(graphPath)) {
9041
+ const graphPath = input.graphPath ?? join35(input.vault, ".skillwiki", "graph.json");
9042
+ if (!existsSync12(graphPath)) {
7328
9043
  return {
7329
9044
  exitCode: ExitCode.FILE_NOT_FOUND,
7330
9045
  result: err("FILE_NOT_FOUND", {
@@ -7335,7 +9050,7 @@ async function runCanvasGenerate(input) {
7335
9050
  }
7336
9051
  let raw;
7337
9052
  try {
7338
- raw = await readFile16(graphPath, "utf8");
9053
+ raw = await readFile17(graphPath, "utf8");
7339
9054
  } catch (e) {
7340
9055
  return {
7341
9056
  exitCode: ExitCode.FILE_NOT_FOUND,
@@ -7361,9 +9076,9 @@ async function runCanvasGenerate(input) {
7361
9076
  const nodes = buildCanvasNodes(paths);
7362
9077
  const edges = buildCanvasEdges(graph.adjacency);
7363
9078
  const canvas = { nodes, edges };
7364
- const outPath = join33(input.vault, "vault-graph.canvas");
9079
+ const outPath = join35(input.vault, "vault-graph.canvas");
7365
9080
  try {
7366
- await writeFile10(outPath, JSON.stringify(canvas, null, 2));
9081
+ await writeFile9(outPath, JSON.stringify(canvas, null, 2));
7367
9082
  } catch (e) {
7368
9083
  return {
7369
9084
  exitCode: ExitCode.WRITE_FAILED,
@@ -7382,11 +9097,252 @@ written: ${outPath}`
7382
9097
  };
7383
9098
  }
7384
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
+
7385
9341
  // src/commands/fleet-health.ts
7386
- import { existsSync as existsSync12, readFileSync as readFileSync14 } from "fs";
9342
+ import { existsSync as existsSync13, readFileSync as readFileSync16 } from "fs";
7387
9343
  import { execSync as nodeExecSync } from "child_process";
7388
9344
  import { hostname as nodeHostname, platform as nodePlatform } from "os";
7389
- import { join as join34 } from "path";
9345
+ import { join as join37 } from "path";
7390
9346
  var SSH_TIMEOUT_MS = 15e3;
7391
9347
  var TIMER_UNIT = "agent-memory-trends.timer";
7392
9348
  var SERVICE_UNIT = "agent-memory-trends.service";
@@ -7483,9 +9439,9 @@ function applyServiceFailedOverlay(run, serviceFailed) {
7483
9439
  function probeLocal(vaultPath, deps) {
7484
9440
  const latestPath = satelliteLatestRunPath(vaultPath);
7485
9441
  let parsed = null;
7486
- if (existsSync12(latestPath)) {
9442
+ if (existsSync13(latestPath)) {
7487
9443
  try {
7488
- const wire = readSatelliteLatestRunFromText(readFileSync14(latestPath, "utf8"));
9444
+ const wire = readSatelliteLatestRunFromText(readFileSync16(latestPath, "utf8"));
7489
9445
  if (wire) {
7490
9446
  parsed = {
7491
9447
  status: wire.status,
@@ -7616,7 +9572,7 @@ async function runFleetHealth(input) {
7616
9572
  const home = input.home ?? env.HOME ?? "";
7617
9573
  const osHostname = input.osHostname ?? env.HOSTNAME ?? nodeHostname();
7618
9574
  const vault = input.vault ?? env.WIKI_PATH;
7619
- const file = input.file ?? (vault ? join34(vault, FLEET_REL_PATH) : void 0);
9575
+ const file = input.file ?? (vault ? join37(vault, FLEET_REL_PATH) : void 0);
7620
9576
  if (!file) {
7621
9577
  return {
7622
9578
  exitCode: ExitCode.NO_VAULT_CONFIGURED,
@@ -7665,7 +9621,7 @@ async function runFleetHealth(input) {
7665
9621
  healthy
7666
9622
  });
7667
9623
  } else {
7668
- if (!hasDeclaredSshAccess(t.host, localHostId, t.sshAlias)) {
9624
+ if (!localHostId || !hasDeclaredSshAccess(t.host, localHostId, t.sshAlias)) {
7669
9625
  rows.push({
7670
9626
  host: t.id,
7671
9627
  timer: "unknown",
@@ -7700,15 +9656,15 @@ async function runFleetHealth(input) {
7700
9656
  }
7701
9657
 
7702
9658
  // src/utils/auto-commit.ts
7703
- import { existsSync as existsSync13 } from "fs";
7704
- import { join as join35 } from "path";
9659
+ import { existsSync as existsSync14 } from "fs";
9660
+ import { join as join38 } from "path";
7705
9661
  async function postCommit(vault, exitCode) {
7706
9662
  if (exitCode !== 0) return;
7707
9663
  const home = process.env.HOME ?? "";
7708
9664
  const dotenv = await parseDotenvFile(configPath(home));
7709
9665
  const autoCommit = process.env.AUTO_COMMIT ?? dotenv["AUTO_COMMIT"];
7710
9666
  if (autoCommit === "false") return;
7711
- if (!existsSync13(join35(vault, ".git"))) return;
9667
+ if (!existsSync14(join38(vault, ".git"))) return;
7712
9668
  const lastOps = readLastOp(vault);
7713
9669
  if (lastOps.length === 0) return;
7714
9670
  const porcelain = git(vault, ["status", "--porcelain", "--", ...VAULT_COMMIT_PATHSPEC]);
@@ -7732,9 +9688,9 @@ async function postCommit(vault, exitCode) {
7732
9688
  }
7733
9689
 
7734
9690
  // src/commands/write-preflight.ts
7735
- import { existsSync as existsSync14, readFileSync as readFileSync15, statSync as statSync4 } from "fs";
9691
+ import { existsSync as existsSync15, readFileSync as readFileSync17, statSync as statSync4 } from "fs";
7736
9692
  async function runWritePreflightCommand(input) {
7737
- if (!existsSync14(input.vault) || !statSync4(input.vault).isDirectory()) {
9693
+ if (!existsSync15(input.vault) || !statSync4(input.vault).isDirectory()) {
7738
9694
  return {
7739
9695
  exitCode: ExitCode.VAULT_PATH_INVALID,
7740
9696
  result: err(GateError.VAULT_PATH_INVALID, { path: input.vault })
@@ -7742,13 +9698,13 @@ async function runWritePreflightCommand(input) {
7742
9698
  }
7743
9699
  let priorText = input.priorArtifactText;
7744
9700
  if (input.priorArtifactFile) {
7745
- if (!existsSync14(input.priorArtifactFile)) {
9701
+ if (!existsSync15(input.priorArtifactFile)) {
7746
9702
  return {
7747
9703
  exitCode: ExitCode.FILE_NOT_FOUND,
7748
9704
  result: err("FILE_NOT_FOUND", { path: input.priorArtifactFile })
7749
9705
  };
7750
9706
  }
7751
- priorText = readFileSync15(input.priorArtifactFile, "utf8");
9707
+ priorText = readFileSync17(input.priorArtifactFile, "utf8");
7752
9708
  }
7753
9709
  const checkList = input.checks ? input.checks.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
7754
9710
  const result = runWritePreflight({
@@ -7862,7 +9818,7 @@ async function emitManagedVaultWrite(vault, command, mutate, opts) {
7862
9818
  if (dirty) {
7863
9819
  return emit(dirty, void 0, { postCommit: false });
7864
9820
  }
7865
- const { runManagedWriteTransaction: runManagedWriteTransaction2 } = await import("./managed-write-preflight-CTXX2MHQ.js");
9821
+ const { runManagedWriteTransaction: runManagedWriteTransaction2 } = await import("./managed-write-preflight-4LPVMP42.js");
7866
9822
  const run = await runManagedWriteTransaction2({
7867
9823
  vault,
7868
9824
  command,
@@ -7892,7 +9848,7 @@ program.command("validate <file>").description("validate vault page frontmatter
7892
9848
  emit(await runValidate({ file, apply: !!opts.apply, vault }), vault);
7893
9849
  });
7894
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) => {
7895
- const out = opts.out ?? join36(vault, ".skillwiki", "graph.json");
9851
+ const out = opts.out ?? join39(vault, ".skillwiki", "graph.json");
7896
9852
  return emitGuardedVaultWrite(vault, "graph build", () => runGraphBuild({ vault, out }));
7897
9853
  });
7898
9854
  var canvasCmd = program.command("canvas").description("manage Obsidian canvas files");
@@ -7906,10 +9862,76 @@ program.command("overlap [vault]").description("detect typed-knowledge pages tha
7906
9862
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
7907
9863
  else emit(await runOverlap({ vault: v.vault }), v.vault);
7908
9864
  });
7909
- 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) => {
9866
+ const v = await resolveVaultArg(vault, opts.wiki);
9867
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
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) => {
7910
9919
  const v = await resolveVaultArg(vault, opts.wiki);
7911
9920
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
7912
- else emit(await runQuery({ text, vault: v.vault, limit: opts.limit }), v.vault);
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
+ );
7913
9935
  });
7914
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({
7915
9937
  vault,
@@ -8014,7 +10036,7 @@ tagCmd.command("reconcile [vault]").description("preview or add prospective page
8014
10036
  );
8015
10037
  });
8016
10038
  var pageCmd = program.command("page").description("validate and publish typed vault pages");
8017
- pageCmd.command("publish <draft> [vault]").description("preview or publish an unpublished typed-page draft").requiredOption("--target <path>", "vault-relative typed page target").option("--log-note <text>", "single-line publication log note").option("--write", "publish SCHEMA, page, index, and log", false).option("--wiki <name>", "wiki profile name").action(async (draft, vault, opts) => {
10039
+ pageCmd.command("publish <draft> [vault]").description("preview or publish an unpublished typed-page draft").requiredOption("--target <path>", "vault-relative typed page target").option("--log-note <text>", "single-line publication log note").option("--write", "publish SCHEMA, page, index, and log", false).option("--approve <token>", "target-bound approval token from a prior dry-run").option("--wiki <name>", "wiki profile name").action(async (draft, vault, opts) => {
8018
10040
  const resolved = await resolveVaultArg(vault, opts.wiki);
8019
10041
  if (!resolved.ok) return emit({ exitCode: resolved.exitCode, result: resolved.payload });
8020
10042
  const input = {
@@ -8022,7 +10044,8 @@ pageCmd.command("publish <draft> [vault]").description("preview or publish an un
8022
10044
  draftPath: draft,
8023
10045
  target: opts.target,
8024
10046
  logNote: opts.logNote,
8025
- write: !!opts.write
10047
+ write: !!opts.write,
10048
+ approve: opts.approve
8026
10049
  };
8027
10050
  if (!opts.write) return emit(await runPagePublish(input), resolved.vault, { postCommit: false });
8028
10051
  return emitGuardedVaultWrite(
@@ -8032,6 +10055,27 @@ pageCmd.command("publish <draft> [vault]").description("preview or publish an un
8032
10055
  { postCommit: false }
8033
10056
  );
8034
10057
  });
10058
+ var projectPageCmd = program.command("project-page").description("publish project-local architecture pages");
10059
+ projectPageCmd.command("publish <draft> [vault]").description("preview or publish a Layer-3 architecture page under projects/{slug}/architecture/").requiredOption("--project <slug>", "project slug").requiredOption("--target <path>", "vault-relative architecture target").option("--log-note <text>", "single-line publication log note").option("--write", "publish SCHEMA, page, project knowledge index, and log", false).option("--approve <token>", "required with --write; token from a prior dry-run").option("--wiki <name>", "wiki profile name").action(async (draft, vault, opts) => {
10060
+ const resolved = await resolveVaultArg(vault, opts.wiki);
10061
+ if (!resolved.ok) return emit({ exitCode: resolved.exitCode, result: resolved.payload });
10062
+ const input = {
10063
+ vault: resolved.vault,
10064
+ draftPath: draft,
10065
+ project: opts.project,
10066
+ target: opts.target,
10067
+ logNote: opts.logNote,
10068
+ write: !!opts.write,
10069
+ approve: opts.approve
10070
+ };
10071
+ if (!opts.write) return emit(await runProjectPagePublish(input), resolved.vault, { postCommit: false });
10072
+ return emitGuardedVaultWrite(
10073
+ resolved.vault,
10074
+ "project-page publish",
10075
+ () => runProjectPagePublish(input),
10076
+ { postCommit: false }
10077
+ );
10078
+ });
8035
10079
  var indexCmd = program.command("index").description("root index projection and checks");
8036
10080
  indexCmd.command("rebuild [vault]").description("preview or write deterministic root index.md projection").option("--write", "write projected index.md", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
8037
10081
  const v = await resolveVaultArg(vault, opts.wiki);
@@ -8053,15 +10097,15 @@ program.command("topic-map-check [vault]").description("check whether a topic ma
8053
10097
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
8054
10098
  else emit(await runTopicMapCheck({ vault: v.vault, threshold: opts.threshold }), v.vault);
8055
10099
  });
8056
- 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) => {
8057
10101
  const v = await resolveVaultArg(vault, opts.wiki);
8058
10102
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
8059
- else if (opts.archive) return emitGuardedVaultWrite(
10103
+ else if (opts.archive && opts.apply) return emitManagedVaultWrite(
8060
10104
  v.vault,
8061
10105
  "stale --archive",
8062
- () => 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 })
8063
10107
  );
8064
- 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 });
8065
10109
  });
8066
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) => {
8067
10111
  const v = await resolveVaultArg(vault, opts.wiki);
@@ -8192,6 +10236,21 @@ projectionsCmd.command("repair-legacy [vault]").description("repair one supporte
8192
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) => {
8193
10237
  const v = await resolveVaultArg(vault, opts.wiki);
8194
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
+ );
8195
10254
  else if (opts.fix) return emitGuardedVaultWrite(
8196
10255
  v.vault,
8197
10256
  "lint --fix",
@@ -8203,8 +10262,7 @@ program.command("lint [vault]").description("run all vault health checks").optio
8203
10262
  logThreshold: opts.logThreshold,
8204
10263
  fix: true,
8205
10264
  only: opts.only,
8206
- summary: !!opts.summary,
8207
- examplesLimit: opts.summary ? opts.examples : void 0
10265
+ summary: false
8208
10266
  })
8209
10267
  );
8210
10268
  else if (opts.summary) emit(await runLint({
@@ -8271,10 +10329,10 @@ program.command("status [vault]").description("output vault diagnostics").option
8271
10329
  langEnvValue: process.env.WIKI_LANG
8272
10330
  }), v.vault);
8273
10331
  });
8274
- 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) => {
8275
10333
  const v = await resolveVaultArg(vault, opts.wiki);
8276
10334
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
8277
- else if (opts.cascade && !opts.apply) emit(await runArchive({
10335
+ else if (!opts.apply && (opts.cascade || String(page).startsWith("raw/"))) emit(await runArchive({
8278
10336
  vault: v.vault,
8279
10337
  page,
8280
10338
  cascade: true,
@@ -8291,6 +10349,7 @@ program.command("archive <page> [vault]").description("archive a typed-knowledge
8291
10349
  page,
8292
10350
  cascade: !!opts.cascade,
8293
10351
  apply: !!opts.apply,
10352
+ approve: opts.approve,
8294
10353
  remote: opts.remote,
8295
10354
  remoteDelete: !!opts.remoteDelete,
8296
10355
  maxRemoteDeletes: Number.parseInt(opts.maxRemoteDeletes, 10)
@@ -8323,15 +10382,16 @@ program.command("drift [vault]").description("detect content drift in raw source
8323
10382
  );
8324
10383
  else emit(await runDrift({ vault: v.vault, apply: false, newSince: opts.new }), v.vault);
8325
10384
  });
8326
- 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) => {
8327
10386
  const v = await resolveVaultArg(vault, opts.wiki);
8328
10387
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
8329
- else if (opts.apply || opts.manifestOut) return emitGuardedVaultWrite(
10388
+ else if (opts.apply || opts.manifestOut) return emitManagedVaultWrite(
8330
10389
  v.vault,
8331
10390
  opts.apply ? "dedup --apply" : "dedup --manifest-out",
8332
10391
  () => runDedup({
8333
10392
  vault: v.vault,
8334
10393
  apply: opts.apply,
10394
+ approve: opts.approve,
8335
10395
  canonicalPolicy: opts.canonicalPolicy,
8336
10396
  manifestOut: opts.manifestOut,
8337
10397
  manifestIn: opts.manifestIn,
@@ -8343,6 +10403,7 @@ program.command("dedup [vault]").description("detect duplicate raw sources by sh
8343
10403
  else emit(await runDedup({
8344
10404
  vault: v.vault,
8345
10405
  apply: opts.apply,
10406
+ approve: opts.approve,
8346
10407
  canonicalPolicy: opts.canonicalPolicy,
8347
10408
  manifestOut: opts.manifestOut,
8348
10409
  manifestIn: opts.manifestIn,