skillwiki 0.10.21 → 0.10.22

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.
package/dist/cli.js CHANGED
@@ -83,7 +83,7 @@ import {
83
83
  upsertIndexEntry,
84
84
  validateLogEvent,
85
85
  writeLogEvent
86
- } from "./chunk-UNPZDCWN.js";
86
+ } from "./chunk-7YDWC6D3.js";
87
87
  import {
88
88
  normalizeDistTag,
89
89
  readCache,
@@ -105,11 +105,11 @@ import {
105
105
  scanVault,
106
106
  splitFrontmatter,
107
107
  writeRootIndexProjection
108
- } from "./chunk-I5JD3BQZ.js";
108
+ } from "./chunk-54JVBNWK.js";
109
109
  import {
110
110
  runManagedWritePreflight,
111
111
  runManagedWriteTransaction
112
- } from "./chunk-65Q5UGND.js";
112
+ } from "./chunk-BASWDOQB.js";
113
113
  import {
114
114
  FLEET_REL_PATH,
115
115
  canSupersedeJournal,
@@ -137,18 +137,20 @@ import {
137
137
  snapshotterAliasForLocalHost,
138
138
  supersedeStaleReviewRequiredJournals,
139
139
  writeDotenv
140
- } from "./chunk-GNS2ZV5P.js";
140
+ } from "./chunk-PQG26AGJ.js";
141
141
  import {
142
142
  ExitCode,
143
143
  MetaSchema,
144
144
  RawSourceSchema,
145
+ TypedKnowledgeSchema,
146
+ detectSchema,
145
147
  err,
146
148
  isBlockedHost,
147
149
  ok
148
- } from "./chunk-Y6KRDGI2.js";
150
+ } from "./chunk-C2DKFJFA.js";
149
151
 
150
152
  // src/cli.ts
151
- import { join as join36 } from "path";
153
+ import { join as join38 } from "path";
152
154
  import { Command } from "commander";
153
155
 
154
156
  // src/utils/output.ts
@@ -1000,7 +1002,7 @@ import { mkdtempSync, writeFileSync, rmSync } from "fs";
1000
1002
  import { tmpdir } from "os";
1001
1003
  import { join as join7 } from "path";
1002
1004
  var ENTRY_RE = /^## \[/m;
1003
- var OPERATION_MARKER_RE = /<!--\s*skillwiki-(?:page-publish|log-op):([a-f0-9]{64})\s*-->/i;
1005
+ var OPERATION_MARKER_RE = /<!--\s*skillwiki-(?:page-publish|project-page-publish|log-op):([a-f0-9]{64})\s*-->/i;
1004
1006
  function splitLog(text) {
1005
1007
  const normalized = text.replace(/\r\n/g, "\n");
1006
1008
  const match = ENTRY_RE.exec(normalized);
@@ -1196,21 +1198,21 @@ function rollbackPath(vault, path, stage2, stage3) {
1196
1198
  function hasMarkers(path, text) {
1197
1199
  const lines = text.split(/\r?\n/);
1198
1200
  let open2 = false;
1199
- let sep2 = false;
1201
+ let sep3 = false;
1200
1202
  for (const line of lines) {
1201
1203
  if (line.startsWith("<<<<<<< ")) {
1202
1204
  open2 = true;
1203
- sep2 = false;
1205
+ sep3 = false;
1204
1206
  continue;
1205
1207
  }
1206
1208
  if (line === "=======" && open2) {
1207
- sep2 = true;
1209
+ sep3 = true;
1208
1210
  continue;
1209
1211
  }
1210
- if (line.startsWith(">>>>>>> ") && open2 && sep2) return true;
1212
+ if (line.startsWith(">>>>>>> ") && open2 && sep3) return true;
1211
1213
  if (line.startsWith(">>>>>>> ")) {
1212
1214
  open2 = false;
1213
- sep2 = false;
1215
+ sep3 = false;
1214
1216
  }
1215
1217
  }
1216
1218
  return false;
@@ -1322,10 +1324,12 @@ function marker(event) {
1322
1324
  return `<!-- skillwiki-log-event:${event.operation_id} -->`;
1323
1325
  }
1324
1326
  function bodyFor(event) {
1325
- if (event.kind === "page-publish") {
1327
+ if (event.kind === "page-publish" || event.kind === "project-page-publish") {
1326
1328
  const tax = Array.isArray(event.metadata.taxonomy_added) ? event.metadata.taxonomy_added : [];
1329
+ const project = typeof event.metadata.project === "string" ? event.metadata.project : void 0;
1327
1330
  return [
1328
1331
  `- Published: [[${event.target.replace(/\.md$/, "")}]]`,
1332
+ ...event.kind === "project-page-publish" && project ? [`- Project: [[${project}]]`] : [],
1329
1333
  `- Taxonomy: ${tax.length > 0 ? `added ${tax.join(", ")}` : "no additions"}`,
1330
1334
  event.note ? `- Note: ${event.note}` : ""
1331
1335
  ].filter(Boolean).join("\n");
@@ -1496,7 +1500,7 @@ function operationId(namespace, parts) {
1496
1500
 
1497
1501
  // src/commands/log-migrate-legacy.ts
1498
1502
  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;
1503
+ var PUBLISH_RE = /<!--\s*skillwiki-(?:page-publish|project-page-publish):([a-f0-9]{64})\s*-->/i;
1500
1504
  function splitLegacyBlocks(text) {
1501
1505
  const matches = [...text.matchAll(BLOCK_RE)];
1502
1506
  if (matches.length === 0) return [];
@@ -3204,7 +3208,7 @@ ${fmRewritten}
3204
3208
  await rename2(join18(input.vault, relPath), join18(input.vault, archivePath));
3205
3209
  let indexUpdated = false;
3206
3210
  if (!isRaw) {
3207
- const { renderRootIndex: renderRootIndex2, writeRootIndexProjection: writeRootIndexProjection2 } = await import("./index-projection-HAXDEM2F.js");
3211
+ const { renderRootIndex: renderRootIndex2, writeRootIndexProjection: writeRootIndexProjection2 } = await import("./index-projection-Q6WL76SO.js");
3208
3212
  const before = await readFile7(join18(input.vault, "index.md"), "utf8").catch(() => "");
3209
3213
  const fullTarget = relPath.replace(/\.md$/, "");
3210
3214
  const bare = fullTarget.split("/").pop() ?? fullTarget;
@@ -3313,10 +3317,10 @@ async function runRemove(input) {
3313
3317
  const tombstonePath = await writeDeleteIntent(input.vault, intent);
3314
3318
  await unlink2(join19(input.vault, relPath));
3315
3319
  if (relPath.endsWith(".md") && !relPath.startsWith("raw/")) {
3316
- const { readFile: readFile17 } = await import("fs/promises");
3320
+ const { readFile: readFile18 } = await import("fs/promises");
3317
3321
  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(() => "");
3322
+ const { renderRootIndex: renderRootIndex2, writeRootIndexProjection: writeRootIndexProjection2 } = await import("./index-projection-Q6WL76SO.js");
3323
+ const before = await readFile18(pathJoin(input.vault, "index.md"), "utf8").catch(() => "");
3320
3324
  const fullTarget = relPath.replace(/\.md$/, "");
3321
3325
  const bare = fullTarget.split("/").pop() ?? fullTarget;
3322
3326
  const hadIndexEntry = before.includes(`[[${fullTarget}]]`) || before.includes(`[[${bare}]]`);
@@ -4728,12 +4732,239 @@ function dateFromPath(path) {
4728
4732
  // src/commands/ingest.ts
4729
4733
  import { readFile as readFile13, open, unlink as unlink4, mkdir as mkdir9 } from "fs/promises";
4730
4734
  import { join as join26 } from "path";
4731
- import { createHash as createHash4 } from "crypto";
4735
+ import { createHash as createHash5 } from "crypto";
4732
4736
 
4733
4737
  // src/commands/page-publish.ts
4734
- import { realpathSync } from "fs";
4738
+ import { readFileSync as readFileSync12, realpathSync } from "fs";
4735
4739
  import { readFile as readFile12 } from "fs/promises";
4736
4740
  import { join as join25, resolve as resolve3 } from "path";
4741
+
4742
+ // src/utils/publication-approval.ts
4743
+ import { createHash as createHash4 } from "crypto";
4744
+ var APPROVAL_CONTRACT = "skillwiki-publication-approval-v1";
4745
+ var APPROVAL_TOKEN_VERSION = "swpub1";
4746
+ var SHA256_HEX = /^[0-9a-f]{64}$/;
4747
+ var PUBLISHERS = /* @__PURE__ */ new Set(["page", "project-page"]);
4748
+ function sha256Hex(bytes) {
4749
+ return createHash4("sha256").update(bytes).digest("hex");
4750
+ }
4751
+ function normalizeLogNote(note) {
4752
+ if (note === void 0) return "";
4753
+ return note.normalize("NFC").trim();
4754
+ }
4755
+ function isPlainObject2(value) {
4756
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4757
+ }
4758
+ function canonicalApprovalJson(payload) {
4759
+ const ordered = {
4760
+ contract: payload.contract,
4761
+ publisher: payload.publisher,
4762
+ draft_sha256: payload.draft_sha256,
4763
+ target: payload.target
4764
+ };
4765
+ if (payload.publisher === "project-page" && payload.project !== void 0) {
4766
+ ordered.project = payload.project;
4767
+ }
4768
+ ordered.log_note = payload.log_note;
4769
+ ordered.prior_target_sha256 = payload.prior_target_sha256;
4770
+ return JSON.stringify(ordered);
4771
+ }
4772
+ function base64UrlEncode(text) {
4773
+ return Buffer.from(text, "utf8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
4774
+ }
4775
+ function base64UrlDecode(segment) {
4776
+ if (!/^[A-Za-z0-9_-]+$/.test(segment)) {
4777
+ return err("APPROVAL_INVALID", { message: "payload is not base64url" });
4778
+ }
4779
+ const padded = segment + "=".repeat((4 - segment.length % 4) % 4);
4780
+ const b64 = padded.replace(/-/g, "+").replace(/_/g, "/");
4781
+ try {
4782
+ return ok(Buffer.from(b64, "base64").toString("utf8"));
4783
+ } catch (error) {
4784
+ return err("APPROVAL_INVALID", { message: `payload decode failed: ${String(error)}` });
4785
+ }
4786
+ }
4787
+ function validatePayloadShape(value) {
4788
+ if (!isPlainObject2(value)) {
4789
+ return err("APPROVAL_INVALID", { message: "payload must be a JSON object" });
4790
+ }
4791
+ if (value.contract !== APPROVAL_CONTRACT) {
4792
+ return err("APPROVAL_INVALID", { message: "unknown approval contract version" });
4793
+ }
4794
+ if (typeof value.publisher !== "string" || !PUBLISHERS.has(value.publisher)) {
4795
+ return err("APPROVAL_INVALID", { message: "unknown publisher kind" });
4796
+ }
4797
+ const publisher = value.publisher;
4798
+ if (typeof value.draft_sha256 !== "string" || !SHA256_HEX.test(value.draft_sha256)) {
4799
+ return err("APPROVAL_INVALID", { message: "draft_sha256 must be 64 lowercase hex chars" });
4800
+ }
4801
+ if (typeof value.target !== "string" || value.target.length === 0 || value.target.length > 500) {
4802
+ return err("APPROVAL_INVALID", { message: "invalid target" });
4803
+ }
4804
+ if (typeof value.log_note !== "string" || /[\r\n]/.test(value.log_note)) {
4805
+ return err("APPROVAL_INVALID", { message: "log_note must be a single line string" });
4806
+ }
4807
+ if (Buffer.byteLength(value.log_note, "utf8") > 500) {
4808
+ return err("APPROVAL_INVALID", { message: "log_note exceeds 500 UTF-8 bytes" });
4809
+ }
4810
+ const prior = value.prior_target_sha256;
4811
+ if (typeof prior !== "string" || prior !== "absent" && !SHA256_HEX.test(prior)) {
4812
+ return err("APPROVAL_INVALID", { message: "prior_target_sha256 must be hex or absent" });
4813
+ }
4814
+ if (publisher === "project-page") {
4815
+ if (typeof value.project !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(value.project)) {
4816
+ return err("APPROVAL_INVALID", { message: "project-page token requires a valid project slug" });
4817
+ }
4818
+ return ok({
4819
+ contract: APPROVAL_CONTRACT,
4820
+ publisher,
4821
+ draft_sha256: value.draft_sha256,
4822
+ target: value.target,
4823
+ project: value.project,
4824
+ log_note: value.log_note,
4825
+ prior_target_sha256: prior
4826
+ });
4827
+ }
4828
+ if (value.project !== void 0 && value.project !== null) {
4829
+ return err("APPROVAL_INVALID", { message: "page publisher token must omit project" });
4830
+ }
4831
+ return ok({
4832
+ contract: APPROVAL_CONTRACT,
4833
+ publisher,
4834
+ draft_sha256: value.draft_sha256,
4835
+ target: value.target,
4836
+ log_note: value.log_note,
4837
+ prior_target_sha256: prior
4838
+ });
4839
+ }
4840
+ function buildApprovalPayload(input) {
4841
+ const base = {
4842
+ contract: APPROVAL_CONTRACT,
4843
+ publisher: input.publisher,
4844
+ draft_sha256: input.draft_sha256,
4845
+ target: input.target,
4846
+ log_note: normalizeLogNote(input.log_note),
4847
+ prior_target_sha256: input.prior_target_sha256,
4848
+ ...input.publisher === "project-page" ? { project: input.project } : {}
4849
+ };
4850
+ return validatePayloadShape(base);
4851
+ }
4852
+ function encodeApprovalToken(payload) {
4853
+ const validated = validatePayloadShape(payload);
4854
+ if (!validated.ok) return validated;
4855
+ const json = canonicalApprovalJson(validated.data);
4856
+ const middle = base64UrlEncode(json);
4857
+ const checksum = sha256Hex(middle);
4858
+ return ok(`${APPROVAL_TOKEN_VERSION}.${middle}.${checksum}`);
4859
+ }
4860
+ function decodeApprovalToken(token) {
4861
+ if (typeof token !== "string" || token.length === 0) {
4862
+ return err("APPROVAL_INVALID", { message: "empty approval token" });
4863
+ }
4864
+ if (token.includes("\n") || token.includes("---\n") || token.includes("```")) {
4865
+ return err("APPROVAL_INVALID", { message: "token must not contain raw draft content" });
4866
+ }
4867
+ const parts = token.split(".");
4868
+ if (parts.length !== 3) {
4869
+ return err("APPROVAL_INVALID", { message: "token must have three segments" });
4870
+ }
4871
+ const [version, middle, checksum] = parts;
4872
+ if (version !== APPROVAL_TOKEN_VERSION) {
4873
+ return err("APPROVAL_INVALID", { message: "unknown token version" });
4874
+ }
4875
+ if (!SHA256_HEX.test(checksum)) {
4876
+ return err("APPROVAL_INVALID", { message: "checksum must be 64 lowercase hex chars" });
4877
+ }
4878
+ const expectedChecksum = sha256Hex(middle);
4879
+ if (expectedChecksum !== checksum) {
4880
+ return err("APPROVAL_INVALID", { message: "checksum mismatch" });
4881
+ }
4882
+ const decoded = base64UrlDecode(middle);
4883
+ if (!decoded.ok) return decoded;
4884
+ let parsed;
4885
+ try {
4886
+ parsed = JSON.parse(decoded.data);
4887
+ } catch {
4888
+ return err("APPROVAL_INVALID", { message: "payload is not valid JSON" });
4889
+ }
4890
+ const shaped = validatePayloadShape(parsed);
4891
+ if (!shaped.ok) return shaped;
4892
+ if (base64UrlEncode(canonicalApprovalJson(shaped.data)) !== middle) {
4893
+ return err("APPROVAL_INVALID", { message: "payload is not in canonical form" });
4894
+ }
4895
+ return shaped;
4896
+ }
4897
+ function verifyApprovalToken(token, expected) {
4898
+ const decoded = decodeApprovalToken(token);
4899
+ if (!decoded.ok) return decoded;
4900
+ const payload = decoded.data;
4901
+ if (payload.publisher !== expected.publisher) {
4902
+ return err("APPROVAL_MISMATCH", {
4903
+ field: "publisher",
4904
+ expected: expected.publisher,
4905
+ actual: payload.publisher
4906
+ });
4907
+ }
4908
+ if (payload.draft_sha256 !== expected.draft_sha256) {
4909
+ return err("APPROVAL_MISMATCH", {
4910
+ field: "draft_sha256",
4911
+ expected: expected.draft_sha256,
4912
+ actual: payload.draft_sha256
4913
+ });
4914
+ }
4915
+ if (payload.target !== expected.target) {
4916
+ return err("APPROVAL_MISMATCH", {
4917
+ field: "target",
4918
+ expected: expected.target,
4919
+ actual: payload.target
4920
+ });
4921
+ }
4922
+ if (payload.prior_target_sha256 !== expected.prior_target_sha256) {
4923
+ return err("APPROVAL_MISMATCH", {
4924
+ field: "prior_target_sha256",
4925
+ expected: expected.prior_target_sha256,
4926
+ actual: payload.prior_target_sha256
4927
+ });
4928
+ }
4929
+ const expectedNote = normalizeLogNote(expected.log_note);
4930
+ if (payload.log_note !== expectedNote) {
4931
+ return err("APPROVAL_MISMATCH", {
4932
+ field: "log_note",
4933
+ expected: expectedNote,
4934
+ actual: payload.log_note
4935
+ });
4936
+ }
4937
+ if (expected.publisher === "project-page") {
4938
+ if (payload.project !== expected.project) {
4939
+ return err("APPROVAL_MISMATCH", {
4940
+ field: "project",
4941
+ expected: expected.project,
4942
+ actual: payload.project
4943
+ });
4944
+ }
4945
+ } else if (payload.project !== void 0) {
4946
+ return err("APPROVAL_MISMATCH", {
4947
+ field: "project",
4948
+ expected: null,
4949
+ actual: payload.project
4950
+ });
4951
+ }
4952
+ return ok(payload);
4953
+ }
4954
+ function operationIdFromApproval(payload) {
4955
+ const namespace = `${payload.contract}:${payload.publisher}`;
4956
+ return operationId(namespace, [
4957
+ payload.draft_sha256,
4958
+ payload.target,
4959
+ payload.project ?? "",
4960
+ payload.log_note
4961
+ ]);
4962
+ }
4963
+ function redactApprovalTokens(text) {
4964
+ return text.replace(/swpub1\.[A-Za-z0-9_-]+\.[0-9a-f]{64}/g, "swpub1.[REDACTED]");
4965
+ }
4966
+
4967
+ // src/commands/page-publish.ts
4737
4968
  var DEFAULT_DEPS2 = {
4738
4969
  afterStage: async () => void 0,
4739
4970
  preflight: (input) => runManagedWritePreflight(input)
@@ -4758,6 +4989,16 @@ function errorExitCode(error) {
4758
4989
  return ExitCode.SYNC_LOCK_HELD;
4759
4990
  case "PREFLIGHT_FAILED":
4760
4991
  return ExitCode.PREFLIGHT_FAILED;
4992
+ case "APPROVAL_REQUIRED":
4993
+ return ExitCode.APPROVAL_REQUIRED;
4994
+ case "APPROVAL_INVALID":
4995
+ return ExitCode.APPROVAL_INVALID;
4996
+ case "APPROVAL_MISMATCH":
4997
+ return ExitCode.APPROVAL_MISMATCH;
4998
+ case "TARGET_DRIFT":
4999
+ return ExitCode.TARGET_DRIFT;
5000
+ case "USAGE":
5001
+ return ExitCode.USAGE;
4761
5002
  case "EVENT_IDENTITY_COLLISION":
4762
5003
  case "WRITE_FAILED":
4763
5004
  return ExitCode.WRITE_FAILED;
@@ -4765,9 +5006,6 @@ function errorExitCode(error) {
4765
5006
  return ExitCode.INVALID_FRONTMATTER;
4766
5007
  }
4767
5008
  }
4768
- function publicationId(target, content, logNote = "") {
4769
- return operationId("skillwiki-page-publish-v1", [target, content, logNote]);
4770
- }
4771
5009
  function resolveRootAggregateMode(env = process.env) {
4772
5010
  if (env.SKILLWIKI_ROOT_AGGREGATE_MODE === "source-only") return "source-only";
4773
5011
  return "dual";
@@ -4780,7 +5018,7 @@ function prepareFrozenPublication(input, source) {
4780
5018
  if (input.logNote !== void 0 && /[\r\n]/.test(input.logNote)) {
4781
5019
  return err("SCHEME_REJECTED", { message: "log note must be one line" });
4782
5020
  }
4783
- const logNote = input.logNote?.trim() || void 0;
5021
+ const logNote = normalizeLogNote(input.logNote) || void 0;
4784
5022
  if (logNote && Buffer.byteLength(logNote, "utf8") > 500) {
4785
5023
  return err("SCHEME_REJECTED", { message: "log note must be one line and at most 500 UTF-8 bytes" });
4786
5024
  }
@@ -4790,14 +5028,41 @@ function prepareFrozenPublication(input, source) {
4790
5028
  const date = (input.now ?? /* @__PURE__ */ new Date()).toISOString().slice(0, 10);
4791
5029
  const taxonomyComment = taxonomyCommentForPage(input.target, date);
4792
5030
  if (!taxonomyComment.ok) return taxonomyComment;
5031
+ const draftSha256 = sha256Hex(input.content);
5032
+ let priorTargetSha256 = input.priorTargetSha256;
5033
+ if (priorTargetSha256 === void 0) {
5034
+ try {
5035
+ const existing = readFileSync12(target.data.absolutePath, "utf8");
5036
+ priorTargetSha256 = sha256Hex(existing);
5037
+ } catch (error) {
5038
+ if (error.code === "ENOENT") priorTargetSha256 = "absent";
5039
+ else {
5040
+ return err("WRITE_FAILED", {
5041
+ path: target.data.absolutePath,
5042
+ message: String(error)
5043
+ });
5044
+ }
5045
+ }
5046
+ }
5047
+ const approvalPayload = buildApprovalPayload({
5048
+ publisher: "page",
5049
+ draft_sha256: draftSha256,
5050
+ target: input.target,
5051
+ log_note: logNote ?? "",
5052
+ prior_target_sha256: priorTargetSha256
5053
+ });
5054
+ if (!approvalPayload.ok) return approvalPayload;
4793
5055
  return ok({
4794
5056
  page: page.data,
4795
5057
  source,
4796
5058
  targetPath: target.data.absolutePath,
4797
5059
  logNote,
4798
- operationId: publicationId(input.target, input.content, logNote),
5060
+ operationId: operationIdFromApproval(approvalPayload.data),
4799
5061
  date,
4800
- taxonomyComment: taxonomyComment.data
5062
+ taxonomyComment: taxonomyComment.data,
5063
+ draftSha256,
5064
+ priorTargetSha256,
5065
+ approvalPayload: approvalPayload.data
4801
5066
  });
4802
5067
  }
4803
5068
  function preparePagePublicationFromContent(input) {
@@ -4944,7 +5209,7 @@ function redactDetail(detail) {
4944
5209
  if (detail === void 0) return void 0;
4945
5210
  try {
4946
5211
  const encoded = JSON.stringify(detail);
4947
- return JSON.parse(redactSensitiveContent(encoded).text);
5212
+ return JSON.parse(redactApprovalTokens(redactSensitiveContent(encoded).text));
4948
5213
  } catch {
4949
5214
  return { message: "unserializable error detail omitted" };
4950
5215
  }
@@ -4964,7 +5229,7 @@ function phaseFailure(stage, input, published, cause, context = {}, exitCode = E
4964
5229
  })
4965
5230
  };
4966
5231
  }
4967
- function successReceipt(input, taxonomyAdded, pageChanged, indexUpdated, logAppended, filesChanged, dryRun = false, receipt = null) {
5232
+ function successReceipt(input, taxonomyAdded, pageChanged, indexUpdated, logAppended, filesChanged, dryRun = false, receipt = null, approvalToken) {
4968
5233
  return {
4969
5234
  exitCode: ExitCode.OK,
4970
5235
  result: ok({
@@ -4980,6 +5245,9 @@ function successReceipt(input, taxonomyAdded, pageChanged, indexUpdated, logAppe
4980
5245
  files_changed: filesChanged,
4981
5246
  base_oid: receipt?.base_oid ?? null,
4982
5247
  write_mode: receipt?.mode ?? null,
5248
+ draft_sha256: input.draftSha256,
5249
+ target_before: input.priorTargetSha256,
5250
+ ...approvalToken ? { approval_token: approvalToken } : {},
4983
5251
  ...receipt ? {
4984
5252
  mutation_vault: receipt.mutation_vault,
4985
5253
  git_vault: receipt.git_vault,
@@ -5053,6 +5321,8 @@ async function previewPreparedPagePublication(input, vault) {
5053
5321
  ...index.data.changed ? ["index.md"] : [],
5054
5322
  ...logAppended ? ["log.md"] : []
5055
5323
  ];
5324
+ const token = encodeApprovalToken(input.approvalPayload);
5325
+ if (!token.ok) return { exitCode: errorExitCode(token.error), result: token };
5056
5326
  return successReceipt(
5057
5327
  input,
5058
5328
  reconciled.data.added,
@@ -5060,7 +5330,9 @@ async function previewPreparedPagePublication(input, vault) {
5060
5330
  index.data.changed,
5061
5331
  logAppended,
5062
5332
  filesChanged,
5063
- true
5333
+ true,
5334
+ null,
5335
+ token.data
5064
5336
  );
5065
5337
  }
5066
5338
  async function publishPreparedPageWithReceipt(input, vault, writeReceipt, deps = DEFAULT_DEPS2) {
@@ -5223,9 +5495,30 @@ async function publishPreparedPage(input, vault, deps = DEFAULT_DEPS2) {
5223
5495
  });
5224
5496
  }
5225
5497
  async function runPagePublish(input, deps = DEFAULT_DEPS2) {
5498
+ if (input.approve && !input.write) {
5499
+ return {
5500
+ exitCode: ExitCode.USAGE,
5501
+ result: err("USAGE", { message: "--approve requires --write" })
5502
+ };
5503
+ }
5226
5504
  const prepared = await preparePagePublication(input);
5227
5505
  if (!prepared.ok) return { exitCode: errorExitCode(prepared.error), result: prepared };
5228
5506
  if (!input.write) return previewPreparedPagePublication(prepared.data, input.vault);
5507
+ if (input.approve) {
5508
+ const verified = verifyApprovalToken(input.approve, {
5509
+ publisher: "page",
5510
+ draft_sha256: prepared.data.draftSha256,
5511
+ target: prepared.data.page.target,
5512
+ log_note: prepared.data.logNote ?? "",
5513
+ prior_target_sha256: prepared.data.priorTargetSha256
5514
+ });
5515
+ if (!verified.ok) {
5516
+ return {
5517
+ exitCode: errorExitCode(verified.error),
5518
+ result: err(verified.error, redactDetail(verified.detail))
5519
+ };
5520
+ }
5521
+ }
5229
5522
  return publishPreparedPage(prepared.data, input.vault, deps);
5230
5523
  }
5231
5524
 
@@ -5372,7 +5665,7 @@ async function acquireRawCaptureLock(path) {
5372
5665
  if (error.code !== "EEXIST") {
5373
5666
  return err("WRITE_FAILED", { path: lockPath, phase: "raw-lock", message: String(error) });
5374
5667
  }
5375
- await new Promise((resolve4) => setTimeout(resolve4, 10));
5668
+ await new Promise((resolve6) => setTimeout(resolve6, 10));
5376
5669
  }
5377
5670
  }
5378
5671
  return err("WRITE_FAILED", { path: lockPath, phase: "raw-lock", message: "raw capture lock held" });
@@ -5464,7 +5757,7 @@ async function runIngest(input, pagePublishDeps = defaultPagePublishDeps()) {
5464
5757
  })
5465
5758
  };
5466
5759
  }
5467
- const sha256 = createHash4("sha256").update(Buffer.from(sourceContent, "utf8")).digest("hex");
5760
+ const sha256 = createHash5("sha256").update(Buffer.from(sourceContent, "utf8")).digest("hex");
5468
5761
  const today = todayIso();
5469
5762
  const slug = slugify2(input.title);
5470
5763
  const tags = input.tags && input.tags.length > 0 ? input.tags : [];
@@ -6005,131 +6298,1410 @@ async function runTagReconcile(input) {
6005
6298
  };
6006
6299
  }
6007
6300
 
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";
6301
+ // src/commands/project-page-publish.ts
6302
+ import { realpathSync as realpathSync4 } from "fs";
6303
+ import { mkdir as mkdir10, readFile as readFile15 } from "fs/promises";
6304
+ import { dirname as dirname8, join as join29, resolve as resolve5 } from "path";
6012
6305
 
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) {
6306
+ // src/utils/architecture-page.ts
6307
+ import { lstatSync, realpathSync as realpathSync2 } from "fs";
6308
+ import { dirname as dirname6, posix as posix2, relative as relative2, resolve as resolve4, sep as sep2 } from "path";
6309
+ var PROJECT_SLUG_RE = /^[a-z0-9][a-z0-9-]*$/;
6310
+ var ARCH_TARGET_RE = /^projects\/([a-z0-9][a-z0-9-]*)\/architecture\/([a-z0-9][a-z0-9._-]*\.md)$/;
6311
+ function invalidFrontmatter(target, issues) {
6312
+ return err("INVALID_FRONTMATTER", {
6313
+ target,
6314
+ errors: issues.map((issue) => ({ path: issue.path.join("."), message: issue.message }))
6315
+ });
6316
+ }
6317
+ function validateProjectSlug(slug) {
6318
+ if (!PROJECT_SLUG_RE.test(slug)) {
6319
+ return err("VAULT_PATH_INVALID", { project: slug, message: "invalid project slug" });
6320
+ }
6321
+ return ok(slug);
6322
+ }
6323
+ function validateArchitectureTarget(target, project) {
6324
+ const slug = validateProjectSlug(project);
6325
+ if (!slug.ok) return slug;
6326
+ if (target.length === 0 || posix2.isAbsolute(target) || target.includes("\\") || posix2.normalize(target) !== target || target.split("/").some((segment) => segment === "" || segment === "." || segment === "..")) {
6327
+ return err("VAULT_PATH_INVALID", { target, message: "unsafe architecture target" });
6328
+ }
6329
+ const match = ARCH_TARGET_RE.exec(target);
6330
+ if (!match) {
6331
+ return err("VAULT_PATH_INVALID", {
6332
+ target,
6333
+ message: "target must be projects/{slug}/architecture/{filename}.md"
6334
+ });
6335
+ }
6336
+ if (match[1] !== project) {
6337
+ return err("VAULT_PATH_INVALID", {
6338
+ target,
6339
+ project,
6340
+ message: "project slug does not match architecture target path"
6341
+ });
6342
+ }
6343
+ return ok(target);
6344
+ }
6345
+ function assertArchitectureTargetInsideVault(vault, target, project) {
6346
+ const validated = validateArchitectureTarget(target, project);
6347
+ if (!validated.ok) return validated;
6348
+ let vaultReal;
6349
+ try {
6350
+ vaultReal = realpathSync2(vault);
6351
+ } catch {
6352
+ return err("VAULT_PATH_INVALID", { target, message: "vault realpath failed" });
6353
+ }
6354
+ const absolutePath = resolve4(vaultReal, target);
6355
+ const parent = dirname6(absolutePath);
6356
+ let parentReal;
6357
+ try {
6358
+ parentReal = realpathSync2(parent);
6359
+ } catch {
6360
+ return err("VAULT_PATH_INVALID", { target, message: "target parent realpath failed" });
6361
+ }
6362
+ const parentRelative = relative2(vaultReal, parentReal).split(sep2).join("/");
6363
+ if (parentRelative === ".." || parentRelative.startsWith("../")) {
6364
+ return err("VAULT_PATH_INVALID", { target, message: "target parent escapes vault" });
6365
+ }
6366
+ if (parentReal !== parent) {
6367
+ return err("VAULT_PATH_INVALID", { target, message: "target parent may not be a symlink alias" });
6368
+ }
6369
+ let existingRealPath;
6370
+ try {
6371
+ const targetStat = lstatSync(absolutePath);
6372
+ if (targetStat.isSymbolicLink()) {
6373
+ return err("VAULT_PATH_INVALID", { target, message: "target may not be a symlink" });
6374
+ }
6375
+ if (!targetStat.isFile()) {
6376
+ return err("VAULT_PATH_INVALID", { target, message: "existing target must be a regular file" });
6377
+ }
6026
6378
  try {
6027
- gitStrict(vault, ["reset", "HEAD", "--", generatedPath]);
6028
- } catch (_e) {
6379
+ existingRealPath = realpathSync2(absolutePath);
6380
+ } catch {
6381
+ return err("VAULT_PATH_INVALID", { target, message: "target realpath failed" });
6029
6382
  }
6383
+ } catch (error) {
6384
+ if (error.code !== "ENOENT") {
6385
+ return err("VAULT_PATH_INVALID", { target, message: "target lstat failed" });
6386
+ }
6387
+ }
6388
+ return ok({ absolutePath, existingRealPath });
6389
+ }
6390
+ function provenanceIncludesProject(provenanceProjects, project) {
6391
+ if (!provenanceProjects) return false;
6392
+ const wanted = `[[${project}]]`;
6393
+ return provenanceProjects.some((entry) => entry === wanted || entry === project);
6394
+ }
6395
+ function prepareArchitecturePage(content, target, project, opts = { isNew: true }) {
6396
+ const safeTarget = validateArchitectureTarget(target, project);
6397
+ if (!safeTarget.ok) return safeTarget;
6398
+ const sensitive = scanSensitiveContent(content, { file: target });
6399
+ if (sensitive.length > 0) {
6400
+ return err("SENSITIVE_CONTENT_DETECTED", { file: target, findings: sensitive });
6401
+ }
6402
+ const frontmatter = extractFrontmatter(content);
6403
+ if (!frontmatter.ok) return frontmatter;
6404
+ const detected = detectSchema(frontmatter.data);
6405
+ if (detected.schema !== "typed-knowledge") {
6406
+ return err("SCHEME_REJECTED", {
6407
+ target,
6408
+ message: "architecture pages must use typed-knowledge frontmatter"
6409
+ });
6410
+ }
6411
+ const parsed = TypedKnowledgeSchema.safeParse(frontmatter.data);
6412
+ if (!parsed.success) return invalidFrontmatter(target, parsed.error.issues);
6413
+ if (parsed.data.type !== "concept") {
6414
+ return err("SCHEME_REJECTED", {
6415
+ target,
6416
+ type: parsed.data.type,
6417
+ message: "architecture pages require type: concept"
6418
+ });
6419
+ }
6420
+ const provenance = parsed.data.provenance;
6421
+ if (provenance !== "project" && provenance !== "mixed") {
6422
+ return err("SCHEME_REJECTED", {
6423
+ target,
6424
+ provenance,
6425
+ message: "architecture pages require provenance: project or mixed"
6426
+ });
6427
+ }
6428
+ if (!provenanceIncludesProject(parsed.data.provenance_projects, project)) {
6429
+ return err("SCHEME_REJECTED", {
6430
+ target,
6431
+ project,
6432
+ message: "provenance_projects must include the target project"
6433
+ });
6434
+ }
6435
+ const tags = [...parsed.data.tags];
6436
+ if (opts.isNew && !tags.includes("adr")) {
6437
+ return err("SCHEME_REJECTED", {
6438
+ target,
6439
+ message: "newly created architecture pages require the adr tag"
6440
+ });
6030
6441
  }
6442
+ return ok({
6443
+ target,
6444
+ project,
6445
+ title: parsed.data.title,
6446
+ type: "concept",
6447
+ tags,
6448
+ content,
6449
+ isNew: opts.isNew
6450
+ });
6031
6451
  }
6032
6452
 
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);
6453
+ // src/utils/publication-operation-journal.ts
6454
+ import {
6455
+ chmodSync,
6456
+ existsSync as existsSync8,
6457
+ mkdirSync as mkdirSync3,
6458
+ readFileSync as readFileSync13,
6459
+ realpathSync as realpathSync3,
6460
+ renameSync as renameSync3,
6461
+ unlinkSync,
6462
+ writeFileSync as writeFileSync5
6463
+ } from "fs";
6464
+ import { homedir } from "os";
6465
+ import { dirname as dirname7, join as join28 } from "path";
6466
+ var PUBLICATION_JOURNAL_SCHEMA = "skillwiki-publication-operation-v1";
6467
+ var PHASE_ORDER2 = [
6468
+ "locked",
6469
+ "taxonomy",
6470
+ "page",
6471
+ "verified",
6472
+ "project-index",
6473
+ "unlocked",
6474
+ "event",
6475
+ "log",
6476
+ "complete"
6477
+ ];
6478
+ function phaseIndex2(phase) {
6479
+ return PHASE_ORDER2.indexOf(phase);
6041
6480
  }
6042
- function splitNonEmptyLines(text) {
6043
- if (!text) return [];
6044
- return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
6481
+ function isValidPhase(phase) {
6482
+ return PHASE_ORDER2.includes(phase);
6045
6483
  }
6046
- function isTrackedNotePath(path) {
6047
- if (!path.endsWith(".md")) return false;
6048
- return !/^\.(skillwiki|claude|obsidian|antigravitycli|playwright-cli)\//.test(path);
6484
+ function vaultIdentity(vaultPath) {
6485
+ let real;
6486
+ try {
6487
+ real = realpathSync3(vaultPath);
6488
+ } catch {
6489
+ real = vaultPath;
6490
+ }
6491
+ return sha256Hex(real).slice(0, 16);
6049
6492
  }
6050
- function refHasPath(vault, ref, path) {
6493
+ function publicationJournalRoot(home = process.env.HOME || homedir()) {
6494
+ return join28(home, ".skillwiki", "publication-operations");
6495
+ }
6496
+ function resolveJournalPaths(vaultPath, operationId2, home = process.env.HOME || homedir()) {
6497
+ const identity = vaultIdentity(vaultPath);
6498
+ const rootDir = publicationJournalRoot(home);
6499
+ const vaultDir = join28(rootDir, identity);
6500
+ return {
6501
+ rootDir,
6502
+ vaultDir,
6503
+ journalPath: join28(vaultDir, `${operationId2}.json`)
6504
+ };
6505
+ }
6506
+ function ensurePrivateDir(dir) {
6507
+ mkdirSync3(dir, { recursive: true, mode: 448 });
6051
6508
  try {
6052
- execFileSync3("git", ["cat-file", "-e", `${ref}:${path}`], {
6053
- cwd: vault,
6054
- stdio: ["pipe", "pipe", "pipe"]
6055
- });
6056
- return true;
6509
+ chmodSync(dir, 448);
6057
6510
  } catch {
6058
- return false;
6059
6511
  }
6060
6512
  }
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
- };
6513
+ function atomicWriteJson(path, body) {
6514
+ ensurePrivateDir(dirname7(path));
6515
+ const tmp = `${path}.tmp.${process.pid}.${Date.now()}`;
6516
+ writeFileSync5(tmp, body, { encoding: "utf8", mode: 384 });
6517
+ try {
6518
+ chmodSync(tmp, 384);
6519
+ } catch {
6079
6520
  }
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);
6084
- const untrackedPaths = splitNonEmptyLines(git(vault, ["ls-files", "--others", "--exclude-standard"]));
6085
- const revOutput = git(vault, ["rev-list", "--left-right", "--count", "origin/HEAD...HEAD"]);
6086
- let ahead = 0;
6087
- let behind = 0;
6088
- if (revOutput) {
6089
- const parts = revOutput.split(/\s+/);
6090
- behind = parseInt(parts[0], 10) || 0;
6091
- ahead = parseInt(parts[1], 10) || 0;
6521
+ renameSync3(tmp, path);
6522
+ try {
6523
+ chmodSync(path, 384);
6524
+ } catch {
6092
6525
  }
6093
- const tsRaw = git(vault, ["log", "-1", "--format=%ct"]);
6094
- let last_commit;
6095
- if (tsRaw) {
6096
- const ts = parseInt(tsRaw, 10);
6097
- if (!isNaN(ts) && ts > 0) {
6098
- last_commit = new Date(ts * 1e3).toISOString();
6099
- } else {
6100
- last_commit = "never";
6101
- }
6102
- } else {
6103
- last_commit = "never";
6526
+ }
6527
+ function identityEqual(a, b) {
6528
+ 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;
6529
+ }
6530
+ function validateRecord(value) {
6531
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
6532
+ return err("SCHEME_REJECTED", { message: "journal record must be an object" });
6104
6533
  }
6105
- const remoteRef = git(vault, ["rev-parse", "--verify", "origin/main"]) ? "origin/main" : git(vault, ["rev-parse", "--verify", "origin/HEAD"]) ? "origin/HEAD" : "HEAD";
6106
- const unpromotedNoteAll = Array.from(/* @__PURE__ */ new Set([...dirtyPaths, ...untrackedPaths])).filter(isTrackedNotePath).filter((path) => !refHasPath(vault, remoteRef, path));
6107
- const unpromotedNoteExamples = unpromotedNoteAll.slice(0, 5);
6108
- const unpromotedNotePaths = unpromotedNoteAll.length;
6109
- let status;
6110
- if (dirty > 0) {
6111
- status = "dirty";
6112
- } else if (ahead > 0) {
6113
- status = "ahead";
6114
- } else if (behind > 0) {
6115
- status = "behind";
6116
- } else {
6117
- status = "clean";
6534
+ const rec = value;
6535
+ if (rec.schema !== PUBLICATION_JOURNAL_SCHEMA) {
6536
+ return err("SCHEME_REJECTED", { message: "unknown journal schema" });
6118
6537
  }
6119
- const hintLines = [
6120
- `status: ${status}`,
6121
- `dirty: ${dirty}`,
6122
- `ahead: ${ahead}`,
6123
- `behind: ${behind}`,
6124
- `unpromoted_note_paths: ${unpromotedNotePaths}`,
6125
- `last_commit: ${last_commit}`
6126
- ];
6127
- const exitCode = status === "clean" ? ExitCode.OK : ExitCode.LINT_HAS_WARNINGS;
6128
- let stashes;
6129
- if (includeStashes) {
6130
- stashes = enumerateStashes(vault);
6538
+ if (typeof rec.operation_id !== "string" || !/^[0-9a-f]{64}$/.test(rec.operation_id)) {
6539
+ return err("SCHEME_REJECTED", { message: "invalid operation_id" });
6131
6540
  }
6132
- const output = {
6541
+ if (typeof rec.phase !== "string" || !isValidPhase(rec.phase)) {
6542
+ return err("SCHEME_REJECTED", { message: "invalid phase" });
6543
+ }
6544
+ if (typeof rec.vault_identity !== "string" || !/^[0-9a-f]{16}$/.test(rec.vault_identity)) {
6545
+ return err("SCHEME_REJECTED", { message: "invalid vault_identity" });
6546
+ }
6547
+ if (typeof rec.published !== "boolean" || typeof rec.verified !== "boolean") {
6548
+ return err("SCHEME_REJECTED", { message: "published/verified must be booleans" });
6549
+ }
6550
+ if (!Array.isArray(rec.files_changed) || !rec.files_changed.every((f) => typeof f === "string")) {
6551
+ return err("SCHEME_REJECTED", { message: "files_changed must be string[]" });
6552
+ }
6553
+ if (typeof rec.updated_at !== "string") {
6554
+ return err("SCHEME_REJECTED", { message: "updated_at must be a string" });
6555
+ }
6556
+ const id = rec.identity;
6557
+ if (typeof id !== "object" || id === null || Array.isArray(id)) {
6558
+ return err("SCHEME_REJECTED", { message: "identity must be an object" });
6559
+ }
6560
+ const identity = id;
6561
+ if (identity.publisher !== "page" && identity.publisher !== "project-page") {
6562
+ return err("SCHEME_REJECTED", { message: "invalid identity.publisher" });
6563
+ }
6564
+ for (const field of ["draft_sha256", "target", "log_note_sha256"]) {
6565
+ if (typeof identity[field] !== "string") {
6566
+ return err("SCHEME_REJECTED", { message: `invalid identity.${field}` });
6567
+ }
6568
+ }
6569
+ if (identity.prior_target_sha256 !== void 0 && typeof identity.prior_target_sha256 !== "string") {
6570
+ return err("SCHEME_REJECTED", { message: "invalid identity.prior_target_sha256" });
6571
+ }
6572
+ for (const forbidden of ["token", "approval_token", "page_body", "content", "log_note", "credential", "password"]) {
6573
+ if (forbidden in rec || forbidden in identity) {
6574
+ return err("SCHEME_REJECTED", { message: `journal must not contain ${forbidden}` });
6575
+ }
6576
+ }
6577
+ return ok({
6578
+ schema: PUBLICATION_JOURNAL_SCHEMA,
6579
+ operation_id: rec.operation_id,
6580
+ phase: rec.phase,
6581
+ vault_identity: rec.vault_identity,
6582
+ identity: {
6583
+ publisher: identity.publisher,
6584
+ draft_sha256: identity.draft_sha256,
6585
+ target: identity.target,
6586
+ ...typeof identity.project === "string" ? { project: identity.project } : {},
6587
+ log_note_sha256: identity.log_note_sha256,
6588
+ ...typeof identity.prior_target_sha256 === "string" ? { prior_target_sha256: identity.prior_target_sha256 } : {}
6589
+ },
6590
+ published: rec.published,
6591
+ verified: rec.verified,
6592
+ files_changed: [...rec.files_changed],
6593
+ updated_at: rec.updated_at
6594
+ });
6595
+ }
6596
+ function buildIdentitySummary(input) {
6597
+ return {
6598
+ publisher: input.publisher,
6599
+ draft_sha256: input.draft_sha256,
6600
+ target: input.target,
6601
+ ...input.publisher === "project-page" && input.project ? { project: input.project } : {},
6602
+ log_note_sha256: sha256Hex(input.log_note),
6603
+ prior_target_sha256: input.prior_target_sha256
6604
+ };
6605
+ }
6606
+ function readPublicationJournal(vaultPath, operationId2, home = process.env.HOME || homedir()) {
6607
+ const { journalPath: journalPath2 } = resolveJournalPaths(vaultPath, operationId2, home);
6608
+ if (!existsSync8(journalPath2)) return ok(null);
6609
+ let text;
6610
+ try {
6611
+ text = readFileSync13(journalPath2, "utf8");
6612
+ } catch (error) {
6613
+ return err("WRITE_FAILED", { message: `read journal failed: ${String(error)}` });
6614
+ }
6615
+ let parsed;
6616
+ try {
6617
+ parsed = JSON.parse(text);
6618
+ } catch {
6619
+ return err("SCHEME_REJECTED", { message: "malformed journal JSON" });
6620
+ }
6621
+ return validateRecord(parsed);
6622
+ }
6623
+ function createPublicationJournal(input) {
6624
+ const home = input.home ?? process.env.HOME ?? homedir();
6625
+ const paths = resolveJournalPaths(input.vaultPath, input.operationId, home);
6626
+ if (existsSync8(paths.journalPath)) {
6627
+ return err("WRITE_FAILED", { message: "journal already exists", path: paths.journalPath });
6628
+ }
6629
+ const record = {
6630
+ schema: PUBLICATION_JOURNAL_SCHEMA,
6631
+ operation_id: input.operationId,
6632
+ phase: "locked",
6633
+ vault_identity: vaultIdentity(input.vaultPath),
6634
+ identity: input.identity,
6635
+ published: false,
6636
+ verified: false,
6637
+ files_changed: [],
6638
+ updated_at: (input.now ?? /* @__PURE__ */ new Date()).toISOString()
6639
+ };
6640
+ const body = `${JSON.stringify(record, null, 2)}
6641
+ `;
6642
+ try {
6643
+ ensurePrivateDir(paths.vaultDir);
6644
+ atomicWriteJson(paths.journalPath, body);
6645
+ } catch (error) {
6646
+ return err("WRITE_FAILED", { message: `create journal failed: ${String(error)}` });
6647
+ }
6648
+ return ok(record);
6649
+ }
6650
+ function advancePublicationJournal(input) {
6651
+ const home = input.home ?? process.env.HOME ?? homedir();
6652
+ const existing = readPublicationJournal(input.vaultPath, input.operationId, home);
6653
+ if (!existing.ok) return existing;
6654
+ if (!existing.data) {
6655
+ return err("RECOVERY_EVIDENCE_MISSING", {
6656
+ operation_id: input.operationId,
6657
+ message: "publication journal not found"
6658
+ });
6659
+ }
6660
+ const current = existing.data;
6661
+ if (!identityEqual(current.identity, input.identity)) {
6662
+ return err("APPROVAL_MISMATCH", {
6663
+ message: "journal identity does not match approved payload",
6664
+ operation_id: input.operationId
6665
+ });
6666
+ }
6667
+ const from = phaseIndex2(current.phase);
6668
+ const to = phaseIndex2(input.nextPhase);
6669
+ if (to < from) {
6670
+ return err("SCHEME_REJECTED", {
6671
+ message: "phase regression refused",
6672
+ from: current.phase,
6673
+ to: input.nextPhase
6674
+ });
6675
+ }
6676
+ if (to === from && input.nextPhase !== current.phase) {
6677
+ return err("SCHEME_REJECTED", { message: "invalid phase transition" });
6678
+ }
6679
+ const record = {
6680
+ ...current,
6681
+ phase: input.nextPhase,
6682
+ published: input.published ?? current.published,
6683
+ verified: input.verified ?? current.verified,
6684
+ files_changed: input.filesChanged ? [.../* @__PURE__ */ new Set([...current.files_changed, ...input.filesChanged])].sort() : current.files_changed,
6685
+ updated_at: (input.now ?? /* @__PURE__ */ new Date()).toISOString()
6686
+ };
6687
+ const paths = resolveJournalPaths(input.vaultPath, input.operationId, home);
6688
+ try {
6689
+ atomicWriteJson(paths.journalPath, `${JSON.stringify(record, null, 2)}
6690
+ `);
6691
+ } catch (error) {
6692
+ return err("WRITE_FAILED", { message: `update journal failed: ${String(error)}` });
6693
+ }
6694
+ return ok(record);
6695
+ }
6696
+ function deletePublicationJournal(vaultPath, operationId2, home = process.env.HOME || homedir()) {
6697
+ const { journalPath: journalPath2 } = resolveJournalPaths(vaultPath, operationId2, home);
6698
+ if (!existsSync8(journalPath2)) return ok({ deleted: false });
6699
+ try {
6700
+ unlinkSync(journalPath2);
6701
+ return ok({ deleted: true });
6702
+ } catch (error) {
6703
+ return err("WRITE_FAILED", { message: `delete journal failed: ${String(error)}` });
6704
+ }
6705
+ }
6706
+ function completePublicationJournal(input) {
6707
+ const advanced = advancePublicationJournal({
6708
+ ...input,
6709
+ nextPhase: "complete",
6710
+ published: true,
6711
+ verified: true
6712
+ });
6713
+ if (!advanced.ok) return advanced;
6714
+ const deleted = deletePublicationJournal(
6715
+ input.vaultPath,
6716
+ input.operationId,
6717
+ input.home ?? process.env.HOME ?? homedir()
6718
+ );
6719
+ if (!deleted.ok) return deleted;
6720
+ return ok({ completed: true });
6721
+ }
6722
+
6723
+ // src/commands/project-page-publish.ts
6724
+ var DEFAULT_DEPS3 = {
6725
+ afterStage: async () => void 0,
6726
+ preflight: (input) => runManagedWritePreflight(input)
6727
+ };
6728
+ function errorExitCode3(error) {
6729
+ switch (error) {
6730
+ case "FILE_NOT_FOUND":
6731
+ return ExitCode.FILE_NOT_FOUND;
6732
+ case "MISSING_CLOSING_DELIMITER":
6733
+ return ExitCode.MISSING_CLOSING_DELIMITER;
6734
+ case "SCHEME_REJECTED":
6735
+ case "NO_TAXONOMY_BLOCK":
6736
+ return ExitCode.SCHEME_REJECTED;
6737
+ case "VAULT_PATH_INVALID":
6738
+ return ExitCode.VAULT_PATH_INVALID;
6739
+ case "SENSITIVE_CONTENT_DETECTED":
6740
+ return ExitCode.SENSITIVE_CONTENT_DETECTED;
6741
+ case "SYNC_LOCK_HELD":
6742
+ return ExitCode.SYNC_LOCK_HELD;
6743
+ case "PREFLIGHT_FAILED":
6744
+ return ExitCode.PREFLIGHT_FAILED;
6745
+ case "APPROVAL_REQUIRED":
6746
+ return ExitCode.APPROVAL_REQUIRED;
6747
+ case "APPROVAL_INVALID":
6748
+ return ExitCode.APPROVAL_INVALID;
6749
+ case "APPROVAL_MISMATCH":
6750
+ return ExitCode.APPROVAL_MISMATCH;
6751
+ case "TARGET_DRIFT":
6752
+ return ExitCode.TARGET_DRIFT;
6753
+ case "RECOVERY_EVIDENCE_MISSING":
6754
+ return ExitCode.RECOVERY_EVIDENCE_MISSING;
6755
+ case "PROJECT_NOT_FOUND":
6756
+ return ExitCode.PROJECT_NOT_FOUND;
6757
+ case "USAGE":
6758
+ return ExitCode.USAGE;
6759
+ case "EVENT_IDENTITY_COLLISION":
6760
+ case "WRITE_FAILED":
6761
+ return ExitCode.WRITE_FAILED;
6762
+ default:
6763
+ return ExitCode.INVALID_FRONTMATTER;
6764
+ }
6765
+ }
6766
+ function redactDetail2(detail) {
6767
+ if (detail === void 0) return void 0;
6768
+ try {
6769
+ const encoded = JSON.stringify(detail);
6770
+ const redacted = redactApprovalTokens(redactSensitiveContent(encoded).text);
6771
+ return JSON.parse(redacted);
6772
+ } catch {
6773
+ return { message: "unserializable error detail omitted" };
6774
+ }
6775
+ }
6776
+ async function observeStage2(deps, stage) {
6777
+ try {
6778
+ await deps.afterStage(stage);
6779
+ return void 0;
6780
+ } catch (error) {
6781
+ return err("WRITE_FAILED", {
6782
+ message: redactApprovalTokens(`stage hook failed at ${stage}: ${String(error)}`)
6783
+ });
6784
+ }
6785
+ }
6786
+ async function readPriorTargetSha(targetPath) {
6787
+ try {
6788
+ const text = await readFile15(targetPath, "utf8");
6789
+ return ok(sha256Hex(text));
6790
+ } catch (error) {
6791
+ if (error.code === "ENOENT") return ok("absent");
6792
+ return err("WRITE_FAILED", { path: targetPath, message: String(error) });
6793
+ }
6794
+ }
6795
+ function validateLogNote(logNote) {
6796
+ if (logNote !== void 0 && /[\r\n]/.test(logNote)) {
6797
+ return err("SCHEME_REJECTED", { message: "log note must be one line" });
6798
+ }
6799
+ const normalized = normalizeLogNote(logNote);
6800
+ if (normalized && Buffer.byteLength(normalized, "utf8") > 500) {
6801
+ return err("SCHEME_REJECTED", { message: "log note must be one line and at most 500 UTF-8 bytes" });
6802
+ }
6803
+ if (normalized && scanSensitiveContent(normalized, { file: "project-page-publish log note" }).length > 0) {
6804
+ return err("SENSITIVE_CONTENT_DETECTED", {
6805
+ message: "log note contains sensitive authentication material"
6806
+ });
6807
+ }
6808
+ return ok(normalized);
6809
+ }
6810
+ async function prepareProjectPagePublication(input) {
6811
+ let content;
6812
+ try {
6813
+ content = await readFile15(input.draftPath, "utf8");
6814
+ } catch (error) {
6815
+ return err("FILE_NOT_FOUND", { path: input.draftPath, message: String(error) });
6816
+ }
6817
+ let draftRealPath;
6818
+ try {
6819
+ draftRealPath = realpathSync4(input.draftPath);
6820
+ } catch (error) {
6821
+ return err("VAULT_PATH_INVALID", {
6822
+ path: input.draftPath,
6823
+ message: `draft realpath failed: ${String(error)}`
6824
+ });
6825
+ }
6826
+ const target = assertArchitectureTargetInsideVault(input.vault, input.target, input.project);
6827
+ if (!target.ok) return target;
6828
+ if (resolve5(input.draftPath) === target.data.absolutePath || target.data.existingRealPath !== void 0 && draftRealPath === target.data.existingRealPath) {
6829
+ return err("VAULT_PATH_INVALID", { message: "draft must not alias the final target" });
6830
+ }
6831
+ const isNew = target.data.existingRealPath === void 0;
6832
+ const page = prepareArchitecturePage(content, input.target, input.project, { isNew });
6833
+ if (!page.ok) return page;
6834
+ const logNote = validateLogNote(input.logNote);
6835
+ if (!logNote.ok) return logNote;
6836
+ const prior = await readPriorTargetSha(target.data.absolutePath);
6837
+ if (!prior.ok) return prior;
6838
+ const draftSha256 = sha256Hex(content);
6839
+ const approvalPayload = buildApprovalPayload({
6840
+ publisher: "project-page",
6841
+ draft_sha256: draftSha256,
6842
+ target: input.target,
6843
+ project: input.project,
6844
+ log_note: logNote.data,
6845
+ prior_target_sha256: prior.data
6846
+ });
6847
+ if (!approvalPayload.ok) return approvalPayload;
6848
+ const date = (input.now ?? /* @__PURE__ */ new Date()).toISOString().slice(0, 10);
6849
+ const taxonomyComment = taxonomyCommentForPage(input.target, date);
6850
+ if (!taxonomyComment.ok) return taxonomyComment;
6851
+ const identity = buildIdentitySummary({
6852
+ publisher: "project-page",
6853
+ draft_sha256: draftSha256,
6854
+ target: input.target,
6855
+ project: input.project,
6856
+ log_note: logNote.data,
6857
+ prior_target_sha256: prior.data
6858
+ });
6859
+ return ok({
6860
+ page: page.data,
6861
+ source: { kind: "file", realPath: draftRealPath },
6862
+ targetPath: target.data.absolutePath,
6863
+ logNote: logNote.data,
6864
+ draftSha256,
6865
+ priorTargetSha256: prior.data,
6866
+ approvalPayload: approvalPayload.data,
6867
+ operationId: operationIdFromApproval(approvalPayload.data),
6868
+ identity,
6869
+ date,
6870
+ taxonomyComment: taxonomyComment.data
6871
+ });
6872
+ }
6873
+ function successReceipt2(prepared, opts) {
6874
+ return {
6875
+ exitCode: ExitCode.OK,
6876
+ result: ok({
6877
+ approval_required: true,
6878
+ ...opts.approvalToken ? { approval_token: opts.approvalToken } : {},
6879
+ operation_id: prepared.operationId,
6880
+ project: prepared.page.project,
6881
+ target: prepared.page.target,
6882
+ page_type: prepared.page.type,
6883
+ tags: [...prepared.page.tags],
6884
+ taxonomy_added: [...opts.taxonomyAdded],
6885
+ page_changed: opts.pageChanged,
6886
+ project_index_changed: opts.projectIndexChanged,
6887
+ log_appended: opts.logAppended,
6888
+ dry_run: opts.dryRun,
6889
+ files_changed: opts.filesChanged,
6890
+ draft_sha256: prepared.draftSha256,
6891
+ target_before: prepared.priorTargetSha256,
6892
+ base_oid: opts.receipt?.base_oid ?? null,
6893
+ write_mode: opts.receipt?.mode ?? null,
6894
+ ...opts.receipt ? {
6895
+ mutation_vault: opts.receipt.mutation_vault,
6896
+ git_vault: opts.receipt.git_vault,
6897
+ convergence_source: opts.receipt.convergence_source
6898
+ } : {},
6899
+ ...opts.receipt?.convergence_vault ? { convergence_vault: opts.receipt.convergence_vault } : {},
6900
+ ...opts.receipt?.host_id ? { host_id: opts.receipt.host_id } : {},
6901
+ humanHint: opts.dryRun ? `dry run: would publish ${prepared.page.target} (${prepared.operationId.slice(0, 12)})` : `published ${prepared.page.target} (${prepared.operationId.slice(0, 12)})`
6902
+ })
6903
+ };
6904
+ }
6905
+ function phaseFailure2(stage, prepared, published, verified, cause, context = {}, exitCode = ExitCode.WRITE_FAILED) {
6906
+ return {
6907
+ exitCode: exitCode === ExitCode.WRITE_FAILED ? errorExitCode3(cause.error) || ExitCode.WRITE_FAILED : exitCode,
6908
+ result: err(cause.error === "EVENT_IDENTITY_COLLISION" ? "WRITE_FAILED" : cause.error, {
6909
+ ...context,
6910
+ stage,
6911
+ published,
6912
+ verified,
6913
+ target: prepared.page.target,
6914
+ operation_id: prepared.operationId,
6915
+ retry_safe: stage !== "target" && cause.error !== "TARGET_DRIFT",
6916
+ cause_error: cause.error,
6917
+ cause_detail: redactDetail2(cause.detail)
6918
+ })
6919
+ };
6920
+ }
6921
+ async function previewProjectPagePublication(prepared, vault) {
6922
+ const schemaPath = join29(vault, "SCHEMA.md");
6923
+ let schemaText;
6924
+ try {
6925
+ schemaText = await readFile15(schemaPath, "utf8");
6926
+ } catch (error) {
6927
+ return {
6928
+ exitCode: ExitCode.FILE_NOT_FOUND,
6929
+ result: err("FILE_NOT_FOUND", { path: schemaPath, message: String(error) })
6930
+ };
6931
+ }
6932
+ const reconciled = reconcileTaxonomyDocument(schemaText, {
6933
+ tags: prepared.page.tags,
6934
+ comment: prepared.taxonomyComment
6935
+ });
6936
+ if (!reconciled.ok) return { exitCode: errorExitCode3(reconciled.error), result: reconciled };
6937
+ const prior = await readPriorTargetSha(prepared.targetPath);
6938
+ if (!prior.ok) return { exitCode: errorExitCode3(prior.error), result: prior };
6939
+ const pageChanged = prior.data === "absent" || prior.data !== prepared.draftSha256;
6940
+ const projectIndex = await renderProjectIndex(vault, prepared.page.project, { today: prepared.date });
6941
+ if (!projectIndex.ok) {
6942
+ if (projectIndex.error !== "PROJECT_NOT_FOUND") {
6943
+ return { exitCode: errorExitCode3(projectIndex.error), result: projectIndex };
6944
+ }
6945
+ }
6946
+ let projectIndexChanged = true;
6947
+ if (projectIndex.ok) {
6948
+ const indexPath = join29(vault, projectIndex.data.index_path);
6949
+ try {
6950
+ const existing = await readFile15(indexPath, "utf8");
6951
+ projectIndexChanged = existing !== projectIndex.data.text || pageChanged;
6952
+ } catch {
6953
+ projectIndexChanged = true;
6954
+ }
6955
+ }
6956
+ const logPath = join29(vault, "log.md");
6957
+ let logAppended = true;
6958
+ try {
6959
+ const logText = await readFile15(logPath, "utf8");
6960
+ logAppended = !logText.includes(`<!-- skillwiki-project-page-publish:${prepared.operationId} -->`);
6961
+ } catch {
6962
+ return {
6963
+ exitCode: ExitCode.FILE_NOT_FOUND,
6964
+ result: err("FILE_NOT_FOUND", { path: logPath })
6965
+ };
6966
+ }
6967
+ const token = encodeApprovalToken(prepared.approvalPayload);
6968
+ if (!token.ok) return { exitCode: errorExitCode3(token.error), result: token };
6969
+ const filesChanged = [
6970
+ ...reconciled.data.changed ? ["SCHEMA.md"] : [],
6971
+ ...pageChanged ? [prepared.page.target] : [],
6972
+ ...projectIndexChanged ? [`projects/${prepared.page.project}/knowledge.md`] : [],
6973
+ ...logAppended ? ["log.md"] : []
6974
+ ];
6975
+ return successReceipt2(prepared, {
6976
+ taxonomyAdded: reconciled.data.added,
6977
+ pageChanged,
6978
+ projectIndexChanged,
6979
+ logAppended,
6980
+ filesChanged,
6981
+ dryRun: true,
6982
+ approvalToken: token.data
6983
+ });
6984
+ }
6985
+ function emptyLockedState2() {
6986
+ return {
6987
+ taxonomyAdded: [],
6988
+ pageChanged: false,
6989
+ projectIndexChanged: false,
6990
+ published: false,
6991
+ verified: false,
6992
+ changed: /* @__PURE__ */ new Set()
6993
+ };
6994
+ }
6995
+ async function revalidatePriorUnderLock(prepared, vault) {
6996
+ const fresh = assertArchitectureTargetInsideVault(vault, prepared.page.target, prepared.page.project);
6997
+ if (!fresh.ok) return fresh;
6998
+ if (fresh.data.absolutePath !== prepared.targetPath) {
6999
+ return err("VAULT_PATH_INVALID", { message: "target canonical path changed after preparation" });
7000
+ }
7001
+ if (prepared.source.kind === "file" && fresh.data.existingRealPath !== void 0 && fresh.data.existingRealPath === prepared.source.realPath) {
7002
+ return err("VAULT_PATH_INVALID", { message: "draft now aliases the final target" });
7003
+ }
7004
+ const prior = await readPriorTargetSha(fresh.data.absolutePath);
7005
+ if (!prior.ok) return prior;
7006
+ if (prior.data !== prepared.priorTargetSha256) {
7007
+ if (prior.data === prepared.draftSha256) {
7008
+ return ok({ absolutePath: fresh.data.absolutePath });
7009
+ }
7010
+ return err("TARGET_DRIFT", {
7011
+ message: "target changed since approval",
7012
+ expected_prior: prepared.priorTargetSha256,
7013
+ actual: prior.data
7014
+ });
7015
+ }
7016
+ return ok({ absolutePath: fresh.data.absolutePath });
7017
+ }
7018
+ var PHASE_RANK = {
7019
+ locked: 0,
7020
+ taxonomy: 1,
7021
+ page: 2,
7022
+ verified: 3,
7023
+ "project-index": 4,
7024
+ unlocked: 5,
7025
+ event: 6,
7026
+ log: 7,
7027
+ complete: 8
7028
+ };
7029
+ function phaseAtLeast(current, needed) {
7030
+ if (!current || !isValidPhase(current)) return false;
7031
+ return PHASE_RANK[current] >= PHASE_RANK[needed];
7032
+ }
7033
+ async function runLockedStages(prepared, vault, deps, journalHome) {
7034
+ const state = emptyLockedState2();
7035
+ const home = journalHome;
7036
+ const revalidated = await revalidatePriorUnderLock(prepared, vault);
7037
+ if (!revalidated.ok) {
7038
+ return {
7039
+ ok: false,
7040
+ exitCode: errorExitCode3(revalidated.error),
7041
+ stage: "target",
7042
+ state,
7043
+ cause: revalidated
7044
+ };
7045
+ }
7046
+ let currentPhase = "locked";
7047
+ const existingJournal = readPublicationJournal(vault, prepared.operationId, home);
7048
+ if (!existingJournal.ok) {
7049
+ return {
7050
+ ok: false,
7051
+ exitCode: errorExitCode3(existingJournal.error),
7052
+ stage: "journal",
7053
+ state,
7054
+ cause: existingJournal
7055
+ };
7056
+ }
7057
+ if (!existingJournal.data) {
7058
+ const journal = createPublicationJournal({
7059
+ vaultPath: vault,
7060
+ operationId: prepared.operationId,
7061
+ identity: prepared.identity,
7062
+ home
7063
+ });
7064
+ if (!journal.ok) {
7065
+ return {
7066
+ ok: false,
7067
+ exitCode: errorExitCode3(journal.error),
7068
+ stage: "journal",
7069
+ state,
7070
+ cause: journal
7071
+ };
7072
+ }
7073
+ } else {
7074
+ currentPhase = existingJournal.data.phase;
7075
+ state.published = existingJournal.data.published || phaseAtLeast(currentPhase, "page");
7076
+ state.verified = existingJournal.data.verified || phaseAtLeast(currentPhase, "verified");
7077
+ for (const f of existingJournal.data.files_changed) state.changed.add(f);
7078
+ }
7079
+ const journalHook = await observeStage2(deps, "journal");
7080
+ if (journalHook) {
7081
+ return { ok: false, exitCode: ExitCode.WRITE_FAILED, stage: "journal", state, cause: journalHook };
7082
+ }
7083
+ const schemaPath = join29(vault, "SCHEMA.md");
7084
+ {
7085
+ let schemaText;
7086
+ try {
7087
+ schemaText = await readFile15(schemaPath, "utf8");
7088
+ } catch (error) {
7089
+ return {
7090
+ ok: false,
7091
+ exitCode: ExitCode.WRITE_FAILED,
7092
+ stage: "taxonomy",
7093
+ state,
7094
+ cause: err("WRITE_FAILED", { message: String(error) })
7095
+ };
7096
+ }
7097
+ const reconciled = reconcileTaxonomyDocument(schemaText, {
7098
+ tags: prepared.page.tags,
7099
+ comment: prepared.taxonomyComment
7100
+ });
7101
+ if (!reconciled.ok) {
7102
+ return {
7103
+ ok: false,
7104
+ exitCode: errorExitCode3(reconciled.error),
7105
+ stage: "taxonomy",
7106
+ state,
7107
+ cause: reconciled
7108
+ };
7109
+ }
7110
+ state.taxonomyAdded = reconciled.data.added;
7111
+ if (reconciled.data.changed) {
7112
+ const schemaWrite = await atomicWriteText(schemaPath, reconciled.data.text);
7113
+ if (!schemaWrite.ok) {
7114
+ return { ok: false, exitCode: ExitCode.WRITE_FAILED, stage: "taxonomy", state, cause: schemaWrite };
7115
+ }
7116
+ if (schemaWrite.data.changed) state.changed.add("SCHEMA.md");
7117
+ }
7118
+ if (!phaseAtLeast(currentPhase, "taxonomy")) {
7119
+ advancePublicationJournal({
7120
+ vaultPath: vault,
7121
+ operationId: prepared.operationId,
7122
+ identity: prepared.identity,
7123
+ nextPhase: "taxonomy",
7124
+ filesChanged: [...state.changed],
7125
+ home
7126
+ });
7127
+ currentPhase = "taxonomy";
7128
+ }
7129
+ const taxonomyHook = await observeStage2(deps, "taxonomy");
7130
+ if (taxonomyHook) {
7131
+ return { ok: false, exitCode: ExitCode.WRITE_FAILED, stage: "taxonomy", state, cause: taxonomyHook };
7132
+ }
7133
+ }
7134
+ if (!phaseAtLeast(currentPhase, "page")) {
7135
+ try {
7136
+ await mkdir10(dirname8(prepared.targetPath), { recursive: true });
7137
+ } catch (error) {
7138
+ return {
7139
+ ok: false,
7140
+ exitCode: ExitCode.WRITE_FAILED,
7141
+ stage: "page",
7142
+ state,
7143
+ cause: err("WRITE_FAILED", { message: String(error) })
7144
+ };
7145
+ }
7146
+ const pageWrite = await safeWritePage(prepared.targetPath, prepared.page.content);
7147
+ if (!pageWrite.ok) {
7148
+ return { ok: false, exitCode: errorExitCode3(pageWrite.error), stage: "page", state, cause: pageWrite };
7149
+ }
7150
+ state.pageChanged = pageWrite.data.changed;
7151
+ if (state.pageChanged) state.changed.add(prepared.page.target);
7152
+ state.published = true;
7153
+ advancePublicationJournal({
7154
+ vaultPath: vault,
7155
+ operationId: prepared.operationId,
7156
+ identity: prepared.identity,
7157
+ nextPhase: "page",
7158
+ published: true,
7159
+ filesChanged: [...state.changed],
7160
+ home
7161
+ });
7162
+ currentPhase = "page";
7163
+ const pageHook = await observeStage2(deps, "page");
7164
+ if (pageHook) {
7165
+ return { ok: false, exitCode: ExitCode.WRITE_FAILED, stage: "page", state, cause: pageHook };
7166
+ }
7167
+ } else {
7168
+ state.published = true;
7169
+ }
7170
+ if (!phaseAtLeast(currentPhase, "verified")) {
7171
+ let visible;
7172
+ let visibleSchema;
7173
+ try {
7174
+ [visible, visibleSchema] = await Promise.all([
7175
+ readFile15(prepared.targetPath, "utf8"),
7176
+ readFile15(schemaPath, "utf8")
7177
+ ]);
7178
+ } catch (error) {
7179
+ return {
7180
+ ok: false,
7181
+ exitCode: ExitCode.WRITE_FAILED,
7182
+ stage: "verify",
7183
+ state,
7184
+ cause: err("WRITE_FAILED", { message: String(error) })
7185
+ };
7186
+ }
7187
+ const visiblePage = prepareArchitecturePage(visible, prepared.page.target, prepared.page.project, {
7188
+ isNew: false
7189
+ });
7190
+ const visibleTaxonomy = extractTaxonomy(visibleSchema);
7191
+ if (!visiblePage.ok || visible !== prepared.page.content || sha256Hex(visible) !== prepared.draftSha256 || !visibleTaxonomy.ok || prepared.page.tags.some((tag) => !visibleTaxonomy.data.includes(tag))) {
7192
+ return {
7193
+ ok: false,
7194
+ exitCode: ExitCode.WRITE_FAILED,
7195
+ stage: "verify",
7196
+ state,
7197
+ cause: err("WRITE_FAILED", { message: "published bytes or taxonomy verification failed" })
7198
+ };
7199
+ }
7200
+ state.verified = true;
7201
+ advancePublicationJournal({
7202
+ vaultPath: vault,
7203
+ operationId: prepared.operationId,
7204
+ identity: prepared.identity,
7205
+ nextPhase: "verified",
7206
+ published: true,
7207
+ verified: true,
7208
+ filesChanged: [...state.changed],
7209
+ home
7210
+ });
7211
+ currentPhase = "verified";
7212
+ const verifyHook = await observeStage2(deps, "verify");
7213
+ if (verifyHook) {
7214
+ return { ok: false, exitCode: ExitCode.WRITE_FAILED, stage: "verify", state, cause: verifyHook };
7215
+ }
7216
+ } else {
7217
+ state.verified = true;
7218
+ }
7219
+ if (!phaseAtLeast(currentPhase, "project-index")) {
7220
+ const rendered = await renderProjectIndex(vault, prepared.page.project, { today: prepared.date });
7221
+ if (!rendered.ok) {
7222
+ return {
7223
+ ok: false,
7224
+ exitCode: errorExitCode3(rendered.error),
7225
+ stage: "project-index",
7226
+ state,
7227
+ cause: rendered
7228
+ };
7229
+ }
7230
+ const knowledgePath = join29(vault, rendered.data.index_path);
7231
+ try {
7232
+ await mkdir10(dirname8(knowledgePath), { recursive: true });
7233
+ } catch (error) {
7234
+ return {
7235
+ ok: false,
7236
+ exitCode: ExitCode.WRITE_FAILED,
7237
+ stage: "project-index",
7238
+ state,
7239
+ cause: err("WRITE_FAILED", { message: String(error) })
7240
+ };
7241
+ }
7242
+ const knowledgeWrite = await atomicWriteText(knowledgePath, rendered.data.text);
7243
+ if (!knowledgeWrite.ok) {
7244
+ return {
7245
+ ok: false,
7246
+ exitCode: ExitCode.WRITE_FAILED,
7247
+ stage: "project-index",
7248
+ state,
7249
+ cause: knowledgeWrite
7250
+ };
7251
+ }
7252
+ state.projectIndexChanged = knowledgeWrite.data.changed;
7253
+ if (state.projectIndexChanged) state.changed.add(rendered.data.index_path);
7254
+ try {
7255
+ const visibleIndex = await readFile15(knowledgePath, "utf8");
7256
+ if (visibleIndex !== rendered.data.text) {
7257
+ return {
7258
+ ok: false,
7259
+ exitCode: ExitCode.WRITE_FAILED,
7260
+ stage: "project-index",
7261
+ state,
7262
+ cause: err("WRITE_FAILED", { message: "project knowledge.md verification failed" })
7263
+ };
7264
+ }
7265
+ } catch (error) {
7266
+ return {
7267
+ ok: false,
7268
+ exitCode: ExitCode.WRITE_FAILED,
7269
+ stage: "project-index",
7270
+ state,
7271
+ cause: err("WRITE_FAILED", { message: String(error) })
7272
+ };
7273
+ }
7274
+ advancePublicationJournal({
7275
+ vaultPath: vault,
7276
+ operationId: prepared.operationId,
7277
+ identity: prepared.identity,
7278
+ nextPhase: "project-index",
7279
+ published: true,
7280
+ verified: true,
7281
+ filesChanged: [...state.changed],
7282
+ home
7283
+ });
7284
+ currentPhase = "project-index";
7285
+ const indexHook = await observeStage2(deps, "project-index");
7286
+ if (indexHook) {
7287
+ return {
7288
+ ok: false,
7289
+ exitCode: ExitCode.WRITE_FAILED,
7290
+ stage: "project-index",
7291
+ state,
7292
+ cause: indexHook
7293
+ };
7294
+ }
7295
+ }
7296
+ return { ok: true, data: state };
7297
+ }
7298
+ function renderProjectPublicationLog(prepared, added) {
7299
+ return [
7300
+ `## [${prepared.date}] project-page-publish | ${prepared.page.target}`,
7301
+ "",
7302
+ `- Published: [[${prepared.page.target.replace(/\.md$/, "")}]]`,
7303
+ `- Project: [[${prepared.page.project}]]`,
7304
+ `- Taxonomy: ${added.length > 0 ? `added ${added.join(", ")}` : "no additions"}`,
7305
+ ...prepared.logNote ? [`- Note: ${prepared.logNote}`] : []
7306
+ ].join("\n");
7307
+ }
7308
+ async function publishWithReceipt(prepared, vault, writeReceipt, deps, journalHome) {
7309
+ const canonicalVault = resolve5(vault);
7310
+ if (writeReceipt.mutation_vault !== canonicalVault) {
7311
+ return {
7312
+ exitCode: ExitCode.PREFLIGHT_FAILED,
7313
+ result: err("PREFLIGHT_FAILED", {
7314
+ reason: "mutation-vault-receipt-mismatch",
7315
+ expected: writeReceipt.mutation_vault,
7316
+ actual: canonicalVault
7317
+ })
7318
+ };
7319
+ }
7320
+ if (writeReceipt.base_oid) {
7321
+ const head = writeReceipt.git_vault ? git(writeReceipt.git_vault, ["rev-parse", "HEAD"]) : null;
7322
+ if (head !== writeReceipt.base_oid) {
7323
+ return {
7324
+ exitCode: ExitCode.PREFLIGHT_FAILED,
7325
+ result: err("PREFLIGHT_FAILED", {
7326
+ reason: "base-oid-drift",
7327
+ expected: writeReceipt.base_oid,
7328
+ actual: head,
7329
+ git_vault: writeReceipt.git_vault
7330
+ })
7331
+ };
7332
+ }
7333
+ }
7334
+ const aggregateMode = resolveRootAggregateMode();
7335
+ let lock;
7336
+ try {
7337
+ lock = acquireOwnedSyncLock(vault, {
7338
+ summary: `project-page publish ${prepared.page.target}`,
7339
+ ttlMinutes: 1
7340
+ });
7341
+ } catch (error) {
7342
+ return {
7343
+ exitCode: ExitCode.WRITE_FAILED,
7344
+ result: err("WRITE_FAILED", { stage: "lock", message: String(error) })
7345
+ };
7346
+ }
7347
+ if (!lock.ok) return { exitCode: errorExitCode3(lock.error), result: lock };
7348
+ let primary;
7349
+ let released;
7350
+ try {
7351
+ primary = await runLockedStages(prepared, vault, deps, journalHome);
7352
+ } catch (error) {
7353
+ primary = {
7354
+ ok: false,
7355
+ exitCode: ExitCode.WRITE_FAILED,
7356
+ stage: "taxonomy",
7357
+ state: emptyLockedState2(),
7358
+ cause: err("WRITE_FAILED", { message: `unexpected primary-stage failure: ${String(error)}` })
7359
+ };
7360
+ } finally {
7361
+ released = releaseOwnedSyncLock(lock.data);
7362
+ }
7363
+ const primaryState = primary?.ok ? primary.data : primary?.state;
7364
+ if (released === void 0 || !released.ok || !released.data.released) {
7365
+ return phaseFailure2(
7366
+ "unlock",
7367
+ prepared,
7368
+ primaryState?.published ?? false,
7369
+ primaryState?.verified ?? false,
7370
+ released && !released.ok ? released : err("WRITE_FAILED", { message: "lock release did not run" })
7371
+ );
7372
+ }
7373
+ {
7374
+ const journalNow = readPublicationJournal(vault, prepared.operationId, journalHome);
7375
+ const phase = journalNow.ok && journalNow.data ? journalNow.data.phase : void 0;
7376
+ if (!phaseAtLeast(phase, "unlocked")) {
7377
+ advancePublicationJournal({
7378
+ vaultPath: vault,
7379
+ operationId: prepared.operationId,
7380
+ identity: prepared.identity,
7381
+ nextPhase: "unlocked",
7382
+ published: primaryState?.published,
7383
+ verified: primaryState?.verified,
7384
+ filesChanged: primaryState ? [...primaryState.changed] : void 0,
7385
+ home: journalHome
7386
+ });
7387
+ }
7388
+ }
7389
+ const unlockHook = await observeStage2(deps, "unlock");
7390
+ if (unlockHook) {
7391
+ return phaseFailure2(
7392
+ "unlock",
7393
+ prepared,
7394
+ primaryState?.published ?? false,
7395
+ primaryState?.verified ?? false,
7396
+ unlockHook
7397
+ );
7398
+ }
7399
+ if (primary === void 0) {
7400
+ return phaseFailure2(
7401
+ "taxonomy",
7402
+ prepared,
7403
+ false,
7404
+ false,
7405
+ err("WRITE_FAILED", { message: "locked publication produced no result" })
7406
+ );
7407
+ }
7408
+ if (!primary.ok) {
7409
+ return phaseFailure2(
7410
+ primary.stage,
7411
+ prepared,
7412
+ primary.state.published,
7413
+ primary.state.verified,
7414
+ primary.cause,
7415
+ void 0,
7416
+ primary.exitCode
7417
+ );
7418
+ }
7419
+ const state = primary.data;
7420
+ const event = await writeLogEvent(vault, {
7421
+ schema: "skillwiki-log-event/v1",
7422
+ operation_id: prepared.operationId,
7423
+ occurred_at: `${prepared.date}T00:00:00.000Z`,
7424
+ host_id: writeReceipt.host_id ?? "standalone",
7425
+ actor: "skillwiki-cli",
7426
+ kind: "project-page-publish",
7427
+ target: prepared.page.target,
7428
+ note: prepared.logNote || `Published ${prepared.page.title}`,
7429
+ metadata: {
7430
+ page_type: prepared.page.type,
7431
+ project: prepared.page.project,
7432
+ tags: [...prepared.page.tags].sort(),
7433
+ draft_sha256: prepared.draftSha256
7434
+ // Do not embed mutable base_oid in identity-critical fields; receipt still carries it.
7435
+ }
7436
+ });
7437
+ if (!event.ok) {
7438
+ return phaseFailure2(
7439
+ "event",
7440
+ prepared,
7441
+ true,
7442
+ true,
7443
+ event,
7444
+ void 0,
7445
+ event.error === "EVENT_IDENTITY_COLLISION" ? ExitCode.WRITE_FAILED : errorExitCode3(event.error)
7446
+ );
7447
+ }
7448
+ if (event.data.created) state.changed.add(event.data.path);
7449
+ {
7450
+ const journalNow = readPublicationJournal(vault, prepared.operationId, journalHome);
7451
+ const phase = journalNow.ok && journalNow.data ? journalNow.data.phase : void 0;
7452
+ if (!phaseAtLeast(phase, "event")) {
7453
+ advancePublicationJournal({
7454
+ vaultPath: vault,
7455
+ operationId: prepared.operationId,
7456
+ identity: prepared.identity,
7457
+ nextPhase: "event",
7458
+ published: true,
7459
+ verified: true,
7460
+ filesChanged: [...state.changed],
7461
+ home: journalHome
7462
+ });
7463
+ }
7464
+ }
7465
+ const eventHook = await observeStage2(deps, "event");
7466
+ if (eventHook) return phaseFailure2("event", prepared, true, true, eventHook);
7467
+ let logAppended = false;
7468
+ if (aggregateMode === "dual") {
7469
+ const log = await runLogAppend({
7470
+ vault,
7471
+ content: renderProjectPublicationLog(prepared, state.taxonomyAdded),
7472
+ operationId: prepared.operationId,
7473
+ strictLock: true,
7474
+ recordLastOp: false,
7475
+ eventKind: "project-page-publish"
7476
+ });
7477
+ if (!log.result.ok) return phaseFailure2("log", prepared, true, true, log.result);
7478
+ if (log.exitCode !== ExitCode.OK) {
7479
+ return phaseFailure2(
7480
+ "log",
7481
+ prepared,
7482
+ true,
7483
+ true,
7484
+ err("WRITE_FAILED", { message: "log append returned inconsistent success state" })
7485
+ );
7486
+ }
7487
+ logAppended = log.result.data.appended;
7488
+ if (logAppended) state.changed.add("log.md");
7489
+ {
7490
+ const journalNow = readPublicationJournal(vault, prepared.operationId, journalHome);
7491
+ const phase = journalNow.ok && journalNow.data ? journalNow.data.phase : void 0;
7492
+ if (!phaseAtLeast(phase, "log")) {
7493
+ advancePublicationJournal({
7494
+ vaultPath: vault,
7495
+ operationId: prepared.operationId,
7496
+ identity: prepared.identity,
7497
+ nextPhase: "log",
7498
+ published: true,
7499
+ verified: true,
7500
+ filesChanged: [...state.changed],
7501
+ home: journalHome
7502
+ });
7503
+ }
7504
+ }
7505
+ const logHook = await observeStage2(deps, "log");
7506
+ if (logHook) return phaseFailure2("log", prepared, true, true, logHook);
7507
+ }
7508
+ const completed = completePublicationJournal({
7509
+ vaultPath: vault,
7510
+ operationId: prepared.operationId,
7511
+ identity: prepared.identity,
7512
+ filesChanged: [...state.changed],
7513
+ home: journalHome
7514
+ });
7515
+ if (!completed.ok) {
7516
+ if (completed.error !== "RECOVERY_EVIDENCE_MISSING") {
7517
+ return phaseFailure2("journal-cleanup", prepared, true, true, completed);
7518
+ }
7519
+ }
7520
+ const cleanupHook = await observeStage2(deps, "journal-cleanup");
7521
+ if (cleanupHook) return phaseFailure2("journal-cleanup", prepared, true, true, cleanupHook);
7522
+ return successReceipt2(prepared, {
7523
+ taxonomyAdded: state.taxonomyAdded,
7524
+ pageChanged: state.pageChanged,
7525
+ projectIndexChanged: state.projectIndexChanged,
7526
+ logAppended,
7527
+ filesChanged: [...state.changed],
7528
+ dryRun: false,
7529
+ receipt: writeReceipt
7530
+ });
7531
+ }
7532
+ async function publishPrepared(prepared, vault, deps, journalHome) {
7533
+ return runManagedWriteTransaction({
7534
+ vault,
7535
+ command: `project-page publish ${prepared.page.target}`,
7536
+ allowImmutableRecord: true,
7537
+ preflight: deps.preflight,
7538
+ mutate: async (receipt) => publishWithReceipt(prepared, vault, receipt, deps, journalHome)
7539
+ });
7540
+ }
7541
+ async function runProjectPagePublish(input, deps = DEFAULT_DEPS3) {
7542
+ if (input.approve && !input.write) {
7543
+ return {
7544
+ exitCode: ExitCode.USAGE,
7545
+ result: err("USAGE", { message: "--approve requires --write" })
7546
+ };
7547
+ }
7548
+ if (input.write && !input.approve) {
7549
+ return {
7550
+ exitCode: ExitCode.APPROVAL_REQUIRED,
7551
+ result: err("APPROVAL_REQUIRED", {
7552
+ message: "project-page publish --write requires --approve <token> from a prior dry-run",
7553
+ project: input.project,
7554
+ target: input.target
7555
+ })
7556
+ };
7557
+ }
7558
+ const prepared = await prepareProjectPagePublication(input);
7559
+ if (!prepared.ok) return { exitCode: errorExitCode3(prepared.error), result: prepared };
7560
+ if (!input.write) {
7561
+ return previewProjectPagePublication(prepared.data, input.vault);
7562
+ }
7563
+ const verified = verifyApprovalToken(input.approve, {
7564
+ publisher: "project-page",
7565
+ draft_sha256: prepared.data.draftSha256,
7566
+ target: prepared.data.page.target,
7567
+ project: prepared.data.page.project,
7568
+ log_note: prepared.data.logNote,
7569
+ prior_target_sha256: prepared.data.priorTargetSha256
7570
+ });
7571
+ if (!verified.ok) {
7572
+ return {
7573
+ exitCode: errorExitCode3(verified.error),
7574
+ result: err(verified.error, redactDetail2(verified.detail))
7575
+ };
7576
+ }
7577
+ return publishPrepared(prepared.data, input.vault, deps, input.journalHome);
7578
+ }
7579
+
7580
+ // src/commands/sync.ts
7581
+ import { existsSync as existsSync9 } from "fs";
7582
+ import { join as join30 } from "path";
7583
+ import { execFileSync as execFileSync3 } from "child_process";
7584
+
7585
+ // src/utils/vault-git-pathspec.ts
7586
+ var VAULT_GENERATED_COMMIT_PATHS = [
7587
+ ".skillwiki/last-op.json",
7588
+ ".skillwiki/memory",
7589
+ ".skillwiki/memory-topics.json"
7590
+ ];
7591
+ var VAULT_GENERATED_COMMIT_EXCLUDES = [
7592
+ ...VAULT_GENERATED_COMMIT_PATHS.map((path) => `:!${path}`)
7593
+ ];
7594
+ var VAULT_COMMIT_PATHSPEC = [".", ...VAULT_GENERATED_COMMIT_EXCLUDES];
7595
+ function stageVaultContentChanges(vault) {
7596
+ gitStrict(vault, ["add", "-A", "--", "."]);
7597
+ for (const generatedPath of VAULT_GENERATED_COMMIT_PATHS) {
7598
+ try {
7599
+ gitStrict(vault, ["reset", "HEAD", "--", generatedPath]);
7600
+ } catch (_e) {
7601
+ }
7602
+ }
7603
+ }
7604
+
7605
+ // src/commands/sync.ts
7606
+ function parseDirtyPaths(porcelain) {
7607
+ if (!porcelain) return [];
7608
+ return porcelain.split("\n").map((line) => line.trimEnd()).filter((line) => line.length >= 4).map((line) => {
7609
+ const payload = line.length >= 3 && line[2] === " " ? line.slice(3) : line.length >= 2 && line[1] === " " ? line.slice(2) : line;
7610
+ const arrow = payload.lastIndexOf(" -> ");
7611
+ return arrow >= 0 ? payload.slice(arrow + 4) : payload;
7612
+ }).filter((path) => path.length > 0);
7613
+ }
7614
+ function splitNonEmptyLines(text) {
7615
+ if (!text) return [];
7616
+ return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
7617
+ }
7618
+ function isTrackedNotePath(path) {
7619
+ if (!path.endsWith(".md")) return false;
7620
+ return !/^\.(skillwiki|claude|obsidian|antigravitycli|playwright-cli)\//.test(path);
7621
+ }
7622
+ function refHasPath(vault, ref, path) {
7623
+ try {
7624
+ execFileSync3("git", ["cat-file", "-e", `${ref}:${path}`], {
7625
+ cwd: vault,
7626
+ stdio: ["pipe", "pipe", "pipe"]
7627
+ });
7628
+ return true;
7629
+ } catch {
7630
+ return false;
7631
+ }
7632
+ }
7633
+ function runSyncStatus(input) {
7634
+ const vault = input.vault;
7635
+ const includeStashes = input.includeStashes ?? false;
7636
+ if (!existsSync9(join30(vault, ".git"))) {
7637
+ return {
7638
+ exitCode: ExitCode.VAULT_PATH_INVALID,
7639
+ result: ok({
7640
+ is_git_repo: false,
7641
+ dirty: 0,
7642
+ ahead: 0,
7643
+ behind: 0,
7644
+ unpromoted_note_paths: 0,
7645
+ unpromoted_note_examples: [],
7646
+ last_commit: "never",
7647
+ status: "not_a_repo",
7648
+ humanHint: "not a git repository"
7649
+ })
7650
+ };
7651
+ }
7652
+ enableGitLongPathsOnWindows(vault);
7653
+ const porcelain = git(vault, ["status", "--porcelain"]);
7654
+ const dirty = porcelain ? porcelain.split("\n").filter((l) => l.trim().length > 0).length : 0;
7655
+ const dirtyPaths = parseDirtyPaths(porcelain);
7656
+ const untrackedPaths = splitNonEmptyLines(git(vault, ["ls-files", "--others", "--exclude-standard"]));
7657
+ const revOutput = git(vault, ["rev-list", "--left-right", "--count", "origin/HEAD...HEAD"]);
7658
+ let ahead = 0;
7659
+ let behind = 0;
7660
+ if (revOutput) {
7661
+ const parts = revOutput.split(/\s+/);
7662
+ behind = parseInt(parts[0], 10) || 0;
7663
+ ahead = parseInt(parts[1], 10) || 0;
7664
+ }
7665
+ const tsRaw = git(vault, ["log", "-1", "--format=%ct"]);
7666
+ let last_commit;
7667
+ if (tsRaw) {
7668
+ const ts = parseInt(tsRaw, 10);
7669
+ if (!isNaN(ts) && ts > 0) {
7670
+ last_commit = new Date(ts * 1e3).toISOString();
7671
+ } else {
7672
+ last_commit = "never";
7673
+ }
7674
+ } else {
7675
+ last_commit = "never";
7676
+ }
7677
+ const remoteRef = git(vault, ["rev-parse", "--verify", "origin/main"]) ? "origin/main" : git(vault, ["rev-parse", "--verify", "origin/HEAD"]) ? "origin/HEAD" : "HEAD";
7678
+ const unpromotedNoteAll = Array.from(/* @__PURE__ */ new Set([...dirtyPaths, ...untrackedPaths])).filter(isTrackedNotePath).filter((path) => !refHasPath(vault, remoteRef, path));
7679
+ const unpromotedNoteExamples = unpromotedNoteAll.slice(0, 5);
7680
+ const unpromotedNotePaths = unpromotedNoteAll.length;
7681
+ let status;
7682
+ if (dirty > 0) {
7683
+ status = "dirty";
7684
+ } else if (ahead > 0) {
7685
+ status = "ahead";
7686
+ } else if (behind > 0) {
7687
+ status = "behind";
7688
+ } else {
7689
+ status = "clean";
7690
+ }
7691
+ const hintLines = [
7692
+ `status: ${status}`,
7693
+ `dirty: ${dirty}`,
7694
+ `ahead: ${ahead}`,
7695
+ `behind: ${behind}`,
7696
+ `unpromoted_note_paths: ${unpromotedNotePaths}`,
7697
+ `last_commit: ${last_commit}`
7698
+ ];
7699
+ const exitCode = status === "clean" ? ExitCode.OK : ExitCode.LINT_HAS_WARNINGS;
7700
+ let stashes;
7701
+ if (includeStashes) {
7702
+ stashes = enumerateStashes(vault);
7703
+ }
7704
+ const output = {
6133
7705
  is_git_repo: true,
6134
7706
  dirty,
6135
7707
  ahead,
@@ -6168,7 +7740,7 @@ function runSyncStatus(input) {
6168
7740
  }
6169
7741
  async function runSyncPush(input) {
6170
7742
  const vault = input.vault;
6171
- if (!existsSync8(join28(vault, ".git"))) {
7743
+ if (!existsSync9(join30(vault, ".git"))) {
6172
7744
  return {
6173
7745
  exitCode: ExitCode.VAULT_PATH_INVALID,
6174
7746
  result: err("NOT_A_GIT_REPO", { path: vault })
@@ -6328,7 +7900,7 @@ function enableGitLongPathsOnWindows(vault) {
6328
7900
  }
6329
7901
  async function runSyncPull(input) {
6330
7902
  const vault = input.vault;
6331
- if (!existsSync8(join28(vault, ".git"))) {
7903
+ if (!existsSync9(join30(vault, ".git"))) {
6332
7904
  return {
6333
7905
  exitCode: ExitCode.VAULT_PATH_INVALID,
6334
7906
  result: err("NOT_A_GIT_REPO", { path: vault })
@@ -6443,7 +8015,7 @@ function runSyncPeers(input) {
6443
8015
  }
6444
8016
  function runSyncLock(input) {
6445
8017
  const vault = input.vault;
6446
- if (!existsSync8(vault)) {
8018
+ if (!existsSync9(vault)) {
6447
8019
  return {
6448
8020
  exitCode: ExitCode.VAULT_PATH_INVALID,
6449
8021
  result: err("VAULT_PATH_INVALID", { path: vault })
@@ -6478,7 +8050,7 @@ function runSyncLock(input) {
6478
8050
  }
6479
8051
  function runSyncUnlock(input) {
6480
8052
  const vault = input.vault;
6481
- if (!existsSync8(vault)) {
8053
+ if (!existsSync9(vault)) {
6482
8054
  return {
6483
8055
  exitCode: ExitCode.VAULT_PATH_INVALID,
6484
8056
  result: err("VAULT_PATH_INVALID", { path: vault })
@@ -6555,11 +8127,11 @@ function runSyncJournalClearStale(input) {
6555
8127
  }
6556
8128
 
6557
8129
  // src/commands/snapshot-maintenance.ts
6558
- import { existsSync as existsSync9, mkdirSync as mkdirSync3, appendFileSync } from "fs";
6559
- import { createHash as createHash5 } from "crypto";
8130
+ import { existsSync as existsSync10, mkdirSync as mkdirSync4, appendFileSync } from "fs";
8131
+ import { createHash as createHash6 } from "crypto";
6560
8132
  import { execSync as execSync3, spawn } from "child_process";
6561
8133
  import { platform as platform2 } from "os";
6562
- import { join as join29, resolve as resolvePath2 } from "path";
8134
+ import { join as join31, resolve as resolvePath2 } from "path";
6563
8135
  var MAINTENANCE_SCHEMA_VERSION = 1;
6564
8136
  var MAINTENANCE_COMMAND = "snapshot-maintenance journal clear-stale";
6565
8137
  function canonicalize(p) {
@@ -6581,7 +8153,7 @@ function computeApprovalId(plan) {
6581
8153
  targets,
6582
8154
  normalizeReason(plan.operator_reason)
6583
8155
  ].join("|");
6584
- return "smap1-" + createHash5("sha256").update(payload).digest("hex").slice(0, 32);
8156
+ return "smap1-" + createHash6("sha256").update(payload).digest("hex").slice(0, 32);
6585
8157
  }
6586
8158
  function normalizeReason(reason) {
6587
8159
  return reason.trim().replace(/\s+/g, " ");
@@ -6622,7 +8194,7 @@ async function runSnapshotMaintenanceDryRun(input) {
6622
8194
  audit(makeAuditEvent(input, fleetLoad.hostId, now, "refusal", "MAINTENANCE_WRONG_WORKTREE"));
6623
8195
  return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_WRONG_WORKTREE", `requested path '${requested}' is not the configured snapshot worktree '${canonicalize(configuredWorktree)}'`) };
6624
8196
  }
6625
- if (!existsSync9(requested) || !existsSync9(join29(requested, ".git"))) {
8197
+ if (!existsSync10(requested) || !existsSync10(join31(requested, ".git"))) {
6626
8198
  audit(makeAuditEvent(input, fleetLoad.hostId, now, "refusal", "MAINTENANCE_MISSING_GIT_REPO"));
6627
8199
  return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_MISSING_GIT_REPO", `snapshot worktree is not a git repository: ${requested}`) };
6628
8200
  }
@@ -6702,14 +8274,14 @@ async function resolveLiveVault(input) {
6702
8274
  return resolved.ok ? canonicalize(resolved.data.path) : void 0;
6703
8275
  }
6704
8276
  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");
8277
+ const stateDir = platform2() === "darwin" ? join31(home, "Library", "Application Support", "vault-sync") : join31(home, ".local", "state", "vault-sync");
8278
+ return join31(stateDir, "snapshot-maintenance-audit.jsonl");
6707
8279
  }
6708
8280
  function defaultAuditSink(home) {
6709
8281
  return (event) => {
6710
8282
  try {
6711
8283
  const logPath = snapshotMaintenanceAuditLogPath(home);
6712
- mkdirSync3(join29(logPath, ".."), { recursive: true });
8284
+ mkdirSync4(join31(logPath, ".."), { recursive: true });
6713
8285
  appendFileSync(logPath, JSON.stringify(event) + "\n", { encoding: "utf8" });
6714
8286
  } catch {
6715
8287
  }
@@ -6869,8 +8441,8 @@ function releaseSnapshotFlock(handle) {
6869
8441
  }
6870
8442
 
6871
8443
  // 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";
8444
+ import { statSync as statSync2, readdirSync as readdirSync2, readFileSync as readFileSync15, mkdirSync as mkdirSync5, writeFileSync as writeFileSync6 } from "fs";
8445
+ import { join as join32, relative as relative3, dirname as dirname9 } from "path";
6874
8446
  import { PutObjectCommand, HeadObjectCommand, ListObjectsV2Command, GetObjectCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
6875
8447
 
6876
8448
  // src/utils/s3-client.ts
@@ -6894,11 +8466,11 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([".git", ".obsidian", "_archive", "node_
6894
8466
  function* walkMarkdown(dir, base) {
6895
8467
  for (const entry of readdirSync2(dir, { withFileTypes: true })) {
6896
8468
  if (SKIP_DIRS.has(entry.name)) continue;
6897
- const full = join30(dir, entry.name);
8469
+ const full = join32(dir, entry.name);
6898
8470
  if (entry.isDirectory()) {
6899
8471
  yield* walkMarkdown(full, base);
6900
8472
  } else if (entry.name.endsWith(".md")) {
6901
- yield relative2(base, full).replace(/\\/g, "/");
8473
+ yield relative3(base, full).replace(/\\/g, "/");
6902
8474
  }
6903
8475
  }
6904
8476
  }
@@ -6917,7 +8489,7 @@ async function runBackupSync(input) {
6917
8489
  let failed = 0;
6918
8490
  const files = [...walkMarkdown(input.vault, input.vault)];
6919
8491
  for (const relPath of files) {
6920
- const absPath = join30(input.vault, relPath);
8492
+ const absPath = join32(input.vault, relPath);
6921
8493
  const localStat = statSync2(absPath);
6922
8494
  let needsUpload = true;
6923
8495
  try {
@@ -6936,7 +8508,7 @@ async function runBackupSync(input) {
6936
8508
  continue;
6937
8509
  }
6938
8510
  try {
6939
- const body = readFileSync13(absPath);
8511
+ const body = readFileSync15(absPath);
6940
8512
  await client.send(new PutObjectCommand({ Bucket: input.bucket, Key: relPath, Body: body }));
6941
8513
  uploaded++;
6942
8514
  } catch {
@@ -6993,7 +8565,7 @@ async function runBackupRestore(input) {
6993
8565
  const objects = list.Contents ?? [];
6994
8566
  for (const obj of objects) {
6995
8567
  if (!obj.Key) continue;
6996
- const localPath = join30(target, obj.Key);
8568
+ const localPath = join32(target, obj.Key);
6997
8569
  try {
6998
8570
  const localStat = statSync2(localPath);
6999
8571
  if (obj.LastModified && localStat.mtime > obj.LastModified) {
@@ -7006,8 +8578,8 @@ async function runBackupRestore(input) {
7006
8578
  const resp = await client.send(new GetObjectCommand({ Bucket: input.bucket, Key: obj.Key }));
7007
8579
  const body = await resp.Body?.transformToByteArray();
7008
8580
  if (body) {
7009
- mkdirSync4(dirname6(localPath), { recursive: true });
7010
- writeFileSync5(localPath, Buffer.from(body));
8581
+ mkdirSync5(dirname9(localPath), { recursive: true });
8582
+ writeFileSync6(localPath, Buffer.from(body));
7011
8583
  downloaded++;
7012
8584
  }
7013
8585
  } catch {
@@ -7039,11 +8611,11 @@ async function runBackupRestore(input) {
7039
8611
  }
7040
8612
 
7041
8613
  // 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";
8614
+ import { existsSync as existsSync11, statSync as statSync3 } from "fs";
8615
+ import { readFile as readFile16 } from "fs/promises";
8616
+ import { join as join33 } from "path";
7045
8617
  async function runStatus(input) {
7046
- if (!existsSync10(input.vault)) {
8618
+ if (!existsSync11(input.vault)) {
7047
8619
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
7048
8620
  }
7049
8621
  const scan = await scanVault(input.vault);
@@ -7068,7 +8640,7 @@ async function runStatus(input) {
7068
8640
  const compound = scan.data.compound.length;
7069
8641
  let schemaVersion = "v1";
7070
8642
  try {
7071
- const schemaContent = await readFile15(join31(input.vault, "SCHEMA.md"), "utf8");
8643
+ const schemaContent = await readFile16(join33(input.vault, "SCHEMA.md"), "utf8");
7072
8644
  const versionMatch = schemaContent.match(/version:\s*["']?([^"'\s\n]+)/i);
7073
8645
  if (versionMatch) schemaVersion = versionMatch[1];
7074
8646
  } catch {
@@ -7128,8 +8700,8 @@ async function runStatus(input) {
7128
8700
  }
7129
8701
 
7130
8702
  // 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";
8703
+ import { mkdir as mkdir11, writeFile as writeFile9, stat as stat4 } from "fs/promises";
8704
+ import { join as join34 } from "path";
7133
8705
  var TODAY = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
7134
8706
  var EXAMPLE_PAGES = {
7135
8707
  "entities/example-project.md": `---
@@ -7198,29 +8770,29 @@ Real sources are immutable after ingestion \u2014 never edit them.
7198
8770
  `;
7199
8771
  async function runSeed(input) {
7200
8772
  try {
7201
- await stat4(join32(input.vault, "SCHEMA.md"));
8773
+ await stat4(join34(input.vault, "SCHEMA.md"));
7202
8774
  } catch {
7203
8775
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { root: input.vault, reason: "SCHEMA.md missing \u2014 run `skillwiki init` first" }) };
7204
8776
  }
7205
8777
  const created = [];
7206
8778
  const skipped = [];
7207
8779
  for (const [relPath, content] of Object.entries(EXAMPLE_PAGES)) {
7208
- const absPath = join32(input.vault, relPath);
8780
+ const absPath = join34(input.vault, relPath);
7209
8781
  try {
7210
8782
  await stat4(absPath);
7211
8783
  skipped.push(relPath);
7212
8784
  } catch {
7213
- await mkdir10(join32(absPath, ".."), { recursive: true });
8785
+ await mkdir11(join34(absPath, ".."), { recursive: true });
7214
8786
  await writeFile9(absPath, content, "utf8");
7215
8787
  created.push(relPath);
7216
8788
  }
7217
8789
  }
7218
- const rawPath = join32(input.vault, "raw", "articles", "example-source.md");
8790
+ const rawPath = join34(input.vault, "raw", "articles", "example-source.md");
7219
8791
  try {
7220
8792
  await stat4(rawPath);
7221
8793
  skipped.push("raw/articles/example-source.md");
7222
8794
  } catch {
7223
- await mkdir10(join32(rawPath, ".."), { recursive: true });
8795
+ await mkdir11(join34(rawPath, ".."), { recursive: true });
7224
8796
  await writeFile9(rawPath, EXAMPLE_RAW, "utf8");
7225
8797
  created.push("raw/articles/example-source.md");
7226
8798
  }
@@ -7243,9 +8815,9 @@ async function runSeed(input) {
7243
8815
  }
7244
8816
 
7245
8817
  // 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";
8818
+ import { readFile as readFile17, writeFile as writeFile10 } from "fs/promises";
8819
+ import { existsSync as existsSync12 } from "fs";
8820
+ import { join as join35 } from "path";
7249
8821
  var NODE_WIDTH = 240;
7250
8822
  var NODE_HEIGHT = 60;
7251
8823
  var COLUMN_SPACING = 400;
@@ -7323,8 +8895,8 @@ function buildCanvasEdges(adjacency) {
7323
8895
  return edges;
7324
8896
  }
7325
8897
  async function runCanvasGenerate(input) {
7326
- const graphPath = input.graphPath ?? join33(input.vault, ".skillwiki", "graph.json");
7327
- if (!existsSync11(graphPath)) {
8898
+ const graphPath = input.graphPath ?? join35(input.vault, ".skillwiki", "graph.json");
8899
+ if (!existsSync12(graphPath)) {
7328
8900
  return {
7329
8901
  exitCode: ExitCode.FILE_NOT_FOUND,
7330
8902
  result: err("FILE_NOT_FOUND", {
@@ -7335,7 +8907,7 @@ async function runCanvasGenerate(input) {
7335
8907
  }
7336
8908
  let raw;
7337
8909
  try {
7338
- raw = await readFile16(graphPath, "utf8");
8910
+ raw = await readFile17(graphPath, "utf8");
7339
8911
  } catch (e) {
7340
8912
  return {
7341
8913
  exitCode: ExitCode.FILE_NOT_FOUND,
@@ -7361,7 +8933,7 @@ async function runCanvasGenerate(input) {
7361
8933
  const nodes = buildCanvasNodes(paths);
7362
8934
  const edges = buildCanvasEdges(graph.adjacency);
7363
8935
  const canvas = { nodes, edges };
7364
- const outPath = join33(input.vault, "vault-graph.canvas");
8936
+ const outPath = join35(input.vault, "vault-graph.canvas");
7365
8937
  try {
7366
8938
  await writeFile10(outPath, JSON.stringify(canvas, null, 2));
7367
8939
  } catch (e) {
@@ -7383,10 +8955,10 @@ written: ${outPath}`
7383
8955
  }
7384
8956
 
7385
8957
  // src/commands/fleet-health.ts
7386
- import { existsSync as existsSync12, readFileSync as readFileSync14 } from "fs";
8958
+ import { existsSync as existsSync13, readFileSync as readFileSync16 } from "fs";
7387
8959
  import { execSync as nodeExecSync } from "child_process";
7388
8960
  import { hostname as nodeHostname, platform as nodePlatform } from "os";
7389
- import { join as join34 } from "path";
8961
+ import { join as join36 } from "path";
7390
8962
  var SSH_TIMEOUT_MS = 15e3;
7391
8963
  var TIMER_UNIT = "agent-memory-trends.timer";
7392
8964
  var SERVICE_UNIT = "agent-memory-trends.service";
@@ -7483,9 +9055,9 @@ function applyServiceFailedOverlay(run, serviceFailed) {
7483
9055
  function probeLocal(vaultPath, deps) {
7484
9056
  const latestPath = satelliteLatestRunPath(vaultPath);
7485
9057
  let parsed = null;
7486
- if (existsSync12(latestPath)) {
9058
+ if (existsSync13(latestPath)) {
7487
9059
  try {
7488
- const wire = readSatelliteLatestRunFromText(readFileSync14(latestPath, "utf8"));
9060
+ const wire = readSatelliteLatestRunFromText(readFileSync16(latestPath, "utf8"));
7489
9061
  if (wire) {
7490
9062
  parsed = {
7491
9063
  status: wire.status,
@@ -7616,7 +9188,7 @@ async function runFleetHealth(input) {
7616
9188
  const home = input.home ?? env.HOME ?? "";
7617
9189
  const osHostname = input.osHostname ?? env.HOSTNAME ?? nodeHostname();
7618
9190
  const vault = input.vault ?? env.WIKI_PATH;
7619
- const file = input.file ?? (vault ? join34(vault, FLEET_REL_PATH) : void 0);
9191
+ const file = input.file ?? (vault ? join36(vault, FLEET_REL_PATH) : void 0);
7620
9192
  if (!file) {
7621
9193
  return {
7622
9194
  exitCode: ExitCode.NO_VAULT_CONFIGURED,
@@ -7700,15 +9272,15 @@ async function runFleetHealth(input) {
7700
9272
  }
7701
9273
 
7702
9274
  // src/utils/auto-commit.ts
7703
- import { existsSync as existsSync13 } from "fs";
7704
- import { join as join35 } from "path";
9275
+ import { existsSync as existsSync14 } from "fs";
9276
+ import { join as join37 } from "path";
7705
9277
  async function postCommit(vault, exitCode) {
7706
9278
  if (exitCode !== 0) return;
7707
9279
  const home = process.env.HOME ?? "";
7708
9280
  const dotenv = await parseDotenvFile(configPath(home));
7709
9281
  const autoCommit = process.env.AUTO_COMMIT ?? dotenv["AUTO_COMMIT"];
7710
9282
  if (autoCommit === "false") return;
7711
- if (!existsSync13(join35(vault, ".git"))) return;
9283
+ if (!existsSync14(join37(vault, ".git"))) return;
7712
9284
  const lastOps = readLastOp(vault);
7713
9285
  if (lastOps.length === 0) return;
7714
9286
  const porcelain = git(vault, ["status", "--porcelain", "--", ...VAULT_COMMIT_PATHSPEC]);
@@ -7732,9 +9304,9 @@ async function postCommit(vault, exitCode) {
7732
9304
  }
7733
9305
 
7734
9306
  // src/commands/write-preflight.ts
7735
- import { existsSync as existsSync14, readFileSync as readFileSync15, statSync as statSync4 } from "fs";
9307
+ import { existsSync as existsSync15, readFileSync as readFileSync17, statSync as statSync4 } from "fs";
7736
9308
  async function runWritePreflightCommand(input) {
7737
- if (!existsSync14(input.vault) || !statSync4(input.vault).isDirectory()) {
9309
+ if (!existsSync15(input.vault) || !statSync4(input.vault).isDirectory()) {
7738
9310
  return {
7739
9311
  exitCode: ExitCode.VAULT_PATH_INVALID,
7740
9312
  result: err(GateError.VAULT_PATH_INVALID, { path: input.vault })
@@ -7742,13 +9314,13 @@ async function runWritePreflightCommand(input) {
7742
9314
  }
7743
9315
  let priorText = input.priorArtifactText;
7744
9316
  if (input.priorArtifactFile) {
7745
- if (!existsSync14(input.priorArtifactFile)) {
9317
+ if (!existsSync15(input.priorArtifactFile)) {
7746
9318
  return {
7747
9319
  exitCode: ExitCode.FILE_NOT_FOUND,
7748
9320
  result: err("FILE_NOT_FOUND", { path: input.priorArtifactFile })
7749
9321
  };
7750
9322
  }
7751
- priorText = readFileSync15(input.priorArtifactFile, "utf8");
9323
+ priorText = readFileSync17(input.priorArtifactFile, "utf8");
7752
9324
  }
7753
9325
  const checkList = input.checks ? input.checks.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
7754
9326
  const result = runWritePreflight({
@@ -7862,7 +9434,7 @@ async function emitManagedVaultWrite(vault, command, mutate, opts) {
7862
9434
  if (dirty) {
7863
9435
  return emit(dirty, void 0, { postCommit: false });
7864
9436
  }
7865
- const { runManagedWriteTransaction: runManagedWriteTransaction2 } = await import("./managed-write-preflight-CTXX2MHQ.js");
9437
+ const { runManagedWriteTransaction: runManagedWriteTransaction2 } = await import("./managed-write-preflight-4LPVMP42.js");
7866
9438
  const run = await runManagedWriteTransaction2({
7867
9439
  vault,
7868
9440
  command,
@@ -7892,7 +9464,7 @@ program.command("validate <file>").description("validate vault page frontmatter
7892
9464
  emit(await runValidate({ file, apply: !!opts.apply, vault }), vault);
7893
9465
  });
7894
9466
  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");
9467
+ const out = opts.out ?? join38(vault, ".skillwiki", "graph.json");
7896
9468
  return emitGuardedVaultWrite(vault, "graph build", () => runGraphBuild({ vault, out }));
7897
9469
  });
7898
9470
  var canvasCmd = program.command("canvas").description("manage Obsidian canvas files");
@@ -8014,7 +9586,7 @@ tagCmd.command("reconcile [vault]").description("preview or add prospective page
8014
9586
  );
8015
9587
  });
8016
9588
  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) => {
9589
+ 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
9590
  const resolved = await resolveVaultArg(vault, opts.wiki);
8019
9591
  if (!resolved.ok) return emit({ exitCode: resolved.exitCode, result: resolved.payload });
8020
9592
  const input = {
@@ -8022,7 +9594,8 @@ pageCmd.command("publish <draft> [vault]").description("preview or publish an un
8022
9594
  draftPath: draft,
8023
9595
  target: opts.target,
8024
9596
  logNote: opts.logNote,
8025
- write: !!opts.write
9597
+ write: !!opts.write,
9598
+ approve: opts.approve
8026
9599
  };
8027
9600
  if (!opts.write) return emit(await runPagePublish(input), resolved.vault, { postCommit: false });
8028
9601
  return emitGuardedVaultWrite(
@@ -8032,6 +9605,27 @@ pageCmd.command("publish <draft> [vault]").description("preview or publish an un
8032
9605
  { postCommit: false }
8033
9606
  );
8034
9607
  });
9608
+ var projectPageCmd = program.command("project-page").description("publish project-local architecture pages");
9609
+ 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) => {
9610
+ const resolved = await resolveVaultArg(vault, opts.wiki);
9611
+ if (!resolved.ok) return emit({ exitCode: resolved.exitCode, result: resolved.payload });
9612
+ const input = {
9613
+ vault: resolved.vault,
9614
+ draftPath: draft,
9615
+ project: opts.project,
9616
+ target: opts.target,
9617
+ logNote: opts.logNote,
9618
+ write: !!opts.write,
9619
+ approve: opts.approve
9620
+ };
9621
+ if (!opts.write) return emit(await runProjectPagePublish(input), resolved.vault, { postCommit: false });
9622
+ return emitGuardedVaultWrite(
9623
+ resolved.vault,
9624
+ "project-page publish",
9625
+ () => runProjectPagePublish(input),
9626
+ { postCommit: false }
9627
+ );
9628
+ });
8035
9629
  var indexCmd = program.command("index").description("root index projection and checks");
8036
9630
  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
9631
  const v = await resolveVaultArg(vault, opts.wiki);