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/{chunk-I5JD3BQZ.js → chunk-54JVBNWK.js} +1 -1
- package/dist/{chunk-UNPZDCWN.js → chunk-7YDWC6D3.js} +12 -5
- package/dist/{chunk-65Q5UGND.js → chunk-BASWDOQB.js} +2 -2
- package/dist/{chunk-Y6KRDGI2.js → chunk-C2DKFJFA.js} +6 -1
- package/dist/{chunk-GNS2ZV5P.js → chunk-PQG26AGJ.js} +1 -1
- package/dist/cli.js +1788 -194
- package/dist/{index-projection-HAXDEM2F.js → index-projection-Q6WL76SO.js} +2 -2
- package/dist/{managed-write-preflight-CTXX2MHQ.js → managed-write-preflight-4LPVMP42.js} +3 -3
- package/dist/skillwiki-mcp.js +4 -4
- package/package.json +1 -1
- package/skills/.claude-plugin/plugin.json +1 -1
- package/skills/.codex-plugin/plugin.json +1 -1
- package/skills/package.json +1 -1
- package/skills/proj-decide/SKILL.md +35 -5
- package/skills/skills/proj-decide/SKILL.md +35 -5
- package/skills/skills/wiki-sync/SKILL.md +37 -51
- package/skills/wiki-sync/SKILL.md +37 -51
package/dist/cli.js
CHANGED
|
@@ -83,7 +83,7 @@ import {
|
|
|
83
83
|
upsertIndexEntry,
|
|
84
84
|
validateLogEvent,
|
|
85
85
|
writeLogEvent
|
|
86
|
-
} from "./chunk-
|
|
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-
|
|
108
|
+
} from "./chunk-54JVBNWK.js";
|
|
109
109
|
import {
|
|
110
110
|
runManagedWritePreflight,
|
|
111
111
|
runManagedWriteTransaction
|
|
112
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
150
|
+
} from "./chunk-C2DKFJFA.js";
|
|
149
151
|
|
|
150
152
|
// src/cli.ts
|
|
151
|
-
import { join as
|
|
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
|
|
1201
|
+
let sep3 = false;
|
|
1200
1202
|
for (const line of lines) {
|
|
1201
1203
|
if (line.startsWith("<<<<<<< ")) {
|
|
1202
1204
|
open2 = true;
|
|
1203
|
-
|
|
1205
|
+
sep3 = false;
|
|
1204
1206
|
continue;
|
|
1205
1207
|
}
|
|
1206
1208
|
if (line === "=======" && open2) {
|
|
1207
|
-
|
|
1209
|
+
sep3 = true;
|
|
1208
1210
|
continue;
|
|
1209
1211
|
}
|
|
1210
|
-
if (line.startsWith(">>>>>>> ") && open2 &&
|
|
1212
|
+
if (line.startsWith(">>>>>>> ") && open2 && sep3) return true;
|
|
1211
1213
|
if (line.startsWith(">>>>>>> ")) {
|
|
1212
1214
|
open2 = false;
|
|
1213
|
-
|
|
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-
|
|
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:
|
|
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-
|
|
3319
|
-
const before = await
|
|
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
|
|
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
|
|
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:
|
|
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((
|
|
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 =
|
|
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/
|
|
6009
|
-
import {
|
|
6010
|
-
import {
|
|
6011
|
-
import {
|
|
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/
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
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
|
-
|
|
6028
|
-
} catch
|
|
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/
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
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
|
|
6043
|
-
|
|
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
|
|
6047
|
-
|
|
6048
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
6062
|
-
|
|
6063
|
-
const
|
|
6064
|
-
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
|
|
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
|
-
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
|
|
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
|
-
|
|
6094
|
-
|
|
6095
|
-
|
|
6096
|
-
|
|
6097
|
-
|
|
6098
|
-
|
|
6099
|
-
|
|
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
|
|
6106
|
-
|
|
6107
|
-
|
|
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
|
-
|
|
6120
|
-
|
|
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
|
-
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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
|
|
6559
|
-
import { createHash as
|
|
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
|
|
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-" +
|
|
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 (!
|
|
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" ?
|
|
6706
|
-
return
|
|
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
|
-
|
|
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
|
|
6873
|
-
import { join as
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
7010
|
-
|
|
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
|
|
7043
|
-
import { readFile as
|
|
7044
|
-
import { join as
|
|
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 (!
|
|
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
|
|
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
|
|
7132
|
-
import { join as
|
|
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(
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
7247
|
-
import { existsSync as
|
|
7248
|
-
import { join as
|
|
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 ??
|
|
7327
|
-
if (!
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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 (
|
|
9058
|
+
if (existsSync13(latestPath)) {
|
|
7487
9059
|
try {
|
|
7488
|
-
const wire = readSatelliteLatestRunFromText(
|
|
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 ?
|
|
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
|
|
7704
|
-
import { join as
|
|
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 (!
|
|
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
|
|
9307
|
+
import { existsSync as existsSync15, readFileSync as readFileSync17, statSync as statSync4 } from "fs";
|
|
7736
9308
|
async function runWritePreflightCommand(input) {
|
|
7737
|
-
if (!
|
|
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 (!
|
|
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 =
|
|
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-
|
|
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 ??
|
|
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);
|