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