daftari 1.18.0 → 1.20.0
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/CHANGELOG.md +93 -0
- package/dist/curation/edges.d.ts +57 -0
- package/dist/curation/edges.d.ts.map +1 -0
- package/dist/curation/edges.js +410 -0
- package/dist/curation/edges.js.map +1 -0
- package/dist/curation/staged-actions.d.ts +0 -1
- package/dist/curation/staged-actions.d.ts.map +1 -1
- package/dist/curation/staged-actions.js +7 -11
- package/dist/curation/staged-actions.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/search/reindex.d.ts.map +1 -1
- package/dist/search/reindex.js +4 -0
- package/dist/search/reindex.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -0
- package/dist/server.js.map +1 -1
- package/dist/storage/index-db.d.ts +13 -0
- package/dist/storage/index-db.d.ts.map +1 -1
- package/dist/storage/index-db.js +43 -1
- package/dist/storage/index-db.js.map +1 -1
- package/dist/tools/edges.d.ts +17 -0
- package/dist/tools/edges.d.ts.map +1 -0
- package/dist/tools/edges.js +347 -0
- package/dist/tools/edges.js.map +1 -0
- package/dist/tools/staged-actions.d.ts +0 -1
- package/dist/tools/staged-actions.d.ts.map +1 -1
- package/dist/tools/staged-actions.js +92 -49
- package/dist/tools/staged-actions.js.map +1 -1
- package/dist/tools/write.d.ts +4 -1
- package/dist/tools/write.d.ts.map +1 -1
- package/dist/tools/write.js +522 -0
- package/dist/tools/write.js.map +1 -1
- package/package.json +1 -1
package/dist/tools/write.js
CHANGED
|
@@ -646,6 +646,425 @@ export async function vaultDeprecate(vaultRoot, args, access) {
|
|
|
646
646
|
});
|
|
647
647
|
}
|
|
648
648
|
// ---------------------------------------------------------------------------
|
|
649
|
+
// vault_set_confidence (§11.4)
|
|
650
|
+
// ---------------------------------------------------------------------------
|
|
651
|
+
// Changes only a document's `confidence`, leaving status and body untouched.
|
|
652
|
+
// A narrow tool so a calibration nudge never rides a full-document overwrite.
|
|
653
|
+
// A reason is mandatory — a confidence change is a claim about the document's
|
|
654
|
+
// trustworthiness and earns an audit trail.
|
|
655
|
+
export async function vaultSetConfidence(vaultRoot, args, access) {
|
|
656
|
+
const ready = requireIndexReady();
|
|
657
|
+
if (!ready.ok)
|
|
658
|
+
return ready;
|
|
659
|
+
const path = requireString(args, "path", "vault_set_confidence");
|
|
660
|
+
if (!path.ok)
|
|
661
|
+
return path;
|
|
662
|
+
const agent = requireString(args, "agent", "vault_set_confidence");
|
|
663
|
+
if (!agent.ok)
|
|
664
|
+
return agent;
|
|
665
|
+
const reason = requireString(args, "reason", "vault_set_confidence");
|
|
666
|
+
if (!reason.ok)
|
|
667
|
+
return reason;
|
|
668
|
+
const confidence = requireString(args, "confidence", "vault_set_confidence");
|
|
669
|
+
if (!confidence.ok)
|
|
670
|
+
return confidence;
|
|
671
|
+
if (!CONFIDENCES.includes(confidence.value)) {
|
|
672
|
+
return err(new Error(`vault_set_confidence: 'confidence' must be one of: ${CONFIDENCES.join(", ")}`));
|
|
673
|
+
}
|
|
674
|
+
const baseVersion = readBaseVersion(args, "vault_set_confidence");
|
|
675
|
+
if (!baseVersion.ok)
|
|
676
|
+
return baseVersion;
|
|
677
|
+
const resolved = resolveVaultPath(vaultRoot, path.value);
|
|
678
|
+
if (!resolved.ok)
|
|
679
|
+
return resolved;
|
|
680
|
+
const existing = await readFile(resolved.value);
|
|
681
|
+
if (!existing.ok) {
|
|
682
|
+
return err(new Error(`vault_set_confidence: document not found: ${path.value}`));
|
|
683
|
+
}
|
|
684
|
+
const parsed = parseDocument(existing.value);
|
|
685
|
+
if (!parsed.ok)
|
|
686
|
+
return parsed;
|
|
687
|
+
const config = loadConfig(vaultRoot);
|
|
688
|
+
if (!config.ok)
|
|
689
|
+
return config;
|
|
690
|
+
const extensions = config.value.schemaExtensions;
|
|
691
|
+
const oldFrontmatter = parsed.value.frontmatter;
|
|
692
|
+
if (access) {
|
|
693
|
+
const collection = collectionOf(path.value, oldFrontmatter);
|
|
694
|
+
if (!canWrite(access.role, collection)) {
|
|
695
|
+
return err(new Error(`access denied: role '${access.roleName}' cannot write to ` +
|
|
696
|
+
`collection '${collection}'`));
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
// No-op guard: a confidence already at the target would churn a commit for no
|
|
700
|
+
// change. Surface it as an error so a redundant staged confidence-up does not
|
|
701
|
+
// silently no-op (and a caller learns the value was already set). Compare
|
|
702
|
+
// against the *raw* on-disk value, not the validated frontmatter — the
|
|
703
|
+
// validator defaults a missing confidence to "low", so comparing the
|
|
704
|
+
// validated value would wrongly reject set_confidence(…, "low") on a doc that
|
|
705
|
+
// never declared one and never write the field (the trap vault_promote dodges
|
|
706
|
+
// the same way, via parsed.value.raw.confidence).
|
|
707
|
+
const rawConfidence = parsed.value.raw.confidence;
|
|
708
|
+
const currentConfidence = typeof rawConfidence === "string" && CONFIDENCES.includes(rawConfidence)
|
|
709
|
+
? rawConfidence
|
|
710
|
+
: undefined;
|
|
711
|
+
if (currentConfidence === confidence.value) {
|
|
712
|
+
return err(new Error(`vault_set_confidence: ${path.value} confidence is already '${confidence.value}'`));
|
|
713
|
+
}
|
|
714
|
+
const newFrontmatter = {
|
|
715
|
+
...oldFrontmatter,
|
|
716
|
+
confidence: confidence.value,
|
|
717
|
+
updated: todayISO(),
|
|
718
|
+
updated_by: agent.value,
|
|
719
|
+
};
|
|
720
|
+
return performWrite({
|
|
721
|
+
vaultRoot,
|
|
722
|
+
relPath: path.value,
|
|
723
|
+
absPath: resolved.value,
|
|
724
|
+
agent: agent.value,
|
|
725
|
+
tool: "vault_set_confidence",
|
|
726
|
+
action: "confidence-set",
|
|
727
|
+
fileText: serializeDocument(newFrontmatter, parsed.value.content, extensions, parsed.value.raw),
|
|
728
|
+
newFrontmatter,
|
|
729
|
+
oldFrontmatter,
|
|
730
|
+
validation: parsed.value.validation,
|
|
731
|
+
commitMessage: `vault_set_confidence: ${path.value} ${oldFrontmatter.confidence}→${confidence.value} ` +
|
|
732
|
+
`by ${agent.value} — ${reason.value}`,
|
|
733
|
+
autoCommit: config.value.autoCommit,
|
|
734
|
+
baseVersion: baseVersion.value,
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
// ---------------------------------------------------------------------------
|
|
738
|
+
// vault_supersede (§11.4)
|
|
739
|
+
// ---------------------------------------------------------------------------
|
|
740
|
+
// Marks a document explicitly superseded by a named successor. Distinct from
|
|
741
|
+
// vault_deprecate: deprecate sets status="deprecated" with an *optional*
|
|
742
|
+
// successor; supersede sets status="superseded" and *requires* a successor that
|
|
743
|
+
// must already exist. Permissive on source status in v1 (last-write-wins).
|
|
744
|
+
export async function vaultSupersede(vaultRoot, args, access) {
|
|
745
|
+
const ready = requireIndexReady();
|
|
746
|
+
if (!ready.ok)
|
|
747
|
+
return ready;
|
|
748
|
+
const oldPath = requireString(args, "old_path", "vault_supersede");
|
|
749
|
+
if (!oldPath.ok)
|
|
750
|
+
return oldPath;
|
|
751
|
+
const newPath = requireString(args, "new_path", "vault_supersede");
|
|
752
|
+
if (!newPath.ok)
|
|
753
|
+
return newPath;
|
|
754
|
+
const agent = requireString(args, "agent", "vault_supersede");
|
|
755
|
+
if (!agent.ok)
|
|
756
|
+
return agent;
|
|
757
|
+
const baseVersion = readBaseVersion(args, "vault_supersede");
|
|
758
|
+
if (!baseVersion.ok)
|
|
759
|
+
return baseVersion;
|
|
760
|
+
let reason;
|
|
761
|
+
if (args.reason !== undefined && args.reason !== null) {
|
|
762
|
+
if (typeof args.reason !== "string") {
|
|
763
|
+
return err(new Error("vault_supersede: 'reason' must be a string"));
|
|
764
|
+
}
|
|
765
|
+
const trimmed = args.reason.trim();
|
|
766
|
+
if (trimmed.length > 0)
|
|
767
|
+
reason = trimmed;
|
|
768
|
+
}
|
|
769
|
+
if (oldPath.value === newPath.value) {
|
|
770
|
+
return err(new Error("vault_supersede: a document cannot supersede itself"));
|
|
771
|
+
}
|
|
772
|
+
const resolvedOld = resolveVaultPath(vaultRoot, oldPath.value);
|
|
773
|
+
if (!resolvedOld.ok)
|
|
774
|
+
return resolvedOld;
|
|
775
|
+
const resolvedNew = resolveVaultPath(vaultRoot, newPath.value);
|
|
776
|
+
if (!resolvedNew.ok)
|
|
777
|
+
return resolvedNew;
|
|
778
|
+
const existing = await readFile(resolvedOld.value);
|
|
779
|
+
if (!existing.ok) {
|
|
780
|
+
return err(new Error(`vault_supersede: document not found: ${oldPath.value}`));
|
|
781
|
+
}
|
|
782
|
+
// The successor must be a real document — superseded_by must never dangle.
|
|
783
|
+
const successor = await readFile(resolvedNew.value);
|
|
784
|
+
if (!successor.ok) {
|
|
785
|
+
return err(new Error(`vault_supersede: successor not found: ${newPath.value}`));
|
|
786
|
+
}
|
|
787
|
+
const parsed = parseDocument(existing.value);
|
|
788
|
+
if (!parsed.ok)
|
|
789
|
+
return parsed;
|
|
790
|
+
const config = loadConfig(vaultRoot);
|
|
791
|
+
if (!config.ok)
|
|
792
|
+
return config;
|
|
793
|
+
const extensions = config.value.schemaExtensions;
|
|
794
|
+
const oldFrontmatter = parsed.value.frontmatter;
|
|
795
|
+
if (access) {
|
|
796
|
+
const collection = collectionOf(oldPath.value, oldFrontmatter);
|
|
797
|
+
if (!canWrite(access.role, collection)) {
|
|
798
|
+
return err(new Error(`access denied: role '${access.roleName}' cannot write to ` +
|
|
799
|
+
`collection '${collection}'`));
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
const newFrontmatter = {
|
|
803
|
+
...oldFrontmatter,
|
|
804
|
+
status: "superseded",
|
|
805
|
+
superseded_by: newPath.value,
|
|
806
|
+
updated: todayISO(),
|
|
807
|
+
updated_by: agent.value,
|
|
808
|
+
};
|
|
809
|
+
return performWrite({
|
|
810
|
+
vaultRoot,
|
|
811
|
+
relPath: oldPath.value,
|
|
812
|
+
absPath: resolvedOld.value,
|
|
813
|
+
agent: agent.value,
|
|
814
|
+
tool: "vault_supersede",
|
|
815
|
+
action: "supersede",
|
|
816
|
+
fileText: serializeDocument(newFrontmatter, parsed.value.content, extensions, parsed.value.raw),
|
|
817
|
+
newFrontmatter,
|
|
818
|
+
oldFrontmatter,
|
|
819
|
+
validation: parsed.value.validation,
|
|
820
|
+
commitMessage: `vault_supersede: ${oldPath.value} superseded by ${newPath.value} ` +
|
|
821
|
+
`by ${agent.value}${reason ? ` — ${reason}` : ""}`,
|
|
822
|
+
autoCommit: config.value.autoCommit,
|
|
823
|
+
baseVersion: baseVersion.value,
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
// Combines two source documents into a target and supersedes both sources to
|
|
827
|
+
// point at it. Mechanical, not generative: the merged body is supplied by the
|
|
828
|
+
// caller (a human at ratification, or the loop) — vault_merge never synthesizes
|
|
829
|
+
// prose (that would be an LLM call; the write layer is LLM-free).
|
|
830
|
+
//
|
|
831
|
+
// Unlike the single-file write tools this touches up to three files, so it does
|
|
832
|
+
// not use performWrite. It mirrors the backfill apply pattern: write each file,
|
|
833
|
+
// index each, then one git commit for the whole set (src/backfill/apply.ts). A
|
|
834
|
+
// source that *is* the target is written once (with the merged body), not also
|
|
835
|
+
// superseded. base_version optimistic concurrency is not offered for merge.
|
|
836
|
+
export async function vaultMerge(vaultRoot, args, access) {
|
|
837
|
+
const ready = requireIndexReady();
|
|
838
|
+
if (!ready.ok)
|
|
839
|
+
return ready;
|
|
840
|
+
const pathA = requireString(args, "path_a", "vault_merge");
|
|
841
|
+
if (!pathA.ok)
|
|
842
|
+
return pathA;
|
|
843
|
+
const pathB = requireString(args, "path_b", "vault_merge");
|
|
844
|
+
if (!pathB.ok)
|
|
845
|
+
return pathB;
|
|
846
|
+
const targetPath = requireString(args, "target_path", "vault_merge");
|
|
847
|
+
if (!targetPath.ok)
|
|
848
|
+
return targetPath;
|
|
849
|
+
const agent = requireString(args, "agent", "vault_merge");
|
|
850
|
+
if (!agent.ok)
|
|
851
|
+
return agent;
|
|
852
|
+
const body = args.body;
|
|
853
|
+
if (typeof body !== "string" || body.trim().length === 0) {
|
|
854
|
+
return err(new Error("vault_merge requires a non-empty string 'body' argument"));
|
|
855
|
+
}
|
|
856
|
+
if (args.frontmatter !== undefined && args.frontmatter !== null) {
|
|
857
|
+
if (typeof args.frontmatter !== "object") {
|
|
858
|
+
return err(new Error("vault_merge: 'frontmatter' must be an object"));
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
const frontmatterOverrides = args.frontmatter && typeof args.frontmatter === "object"
|
|
862
|
+
? args.frontmatter
|
|
863
|
+
: {};
|
|
864
|
+
// Resolve all three paths up front and run every identity check against the
|
|
865
|
+
// resolved absolute paths, never the raw caller strings: `pricing/a.md` and
|
|
866
|
+
// `./pricing/a.md` name the same file but differ as strings, which would
|
|
867
|
+
// otherwise defeat the path_a≠path_b guard and the source-is-target skip
|
|
868
|
+
// below (writing one file twice in a single commit and superseding it against
|
|
869
|
+
// itself).
|
|
870
|
+
const resolvedA = resolveVaultPath(vaultRoot, pathA.value);
|
|
871
|
+
if (!resolvedA.ok)
|
|
872
|
+
return resolvedA;
|
|
873
|
+
const resolvedB = resolveVaultPath(vaultRoot, pathB.value);
|
|
874
|
+
if (!resolvedB.ok)
|
|
875
|
+
return resolvedB;
|
|
876
|
+
const resolvedTarget = resolveVaultPath(vaultRoot, targetPath.value);
|
|
877
|
+
if (!resolvedTarget.ok)
|
|
878
|
+
return resolvedTarget;
|
|
879
|
+
if (resolvedA.value === resolvedB.value) {
|
|
880
|
+
return err(new Error("vault_merge: path_a and path_b must differ"));
|
|
881
|
+
}
|
|
882
|
+
const existingA = await readFile(resolvedA.value);
|
|
883
|
+
if (!existingA.ok)
|
|
884
|
+
return err(new Error(`vault_merge: document not found: ${pathA.value}`));
|
|
885
|
+
const existingB = await readFile(resolvedB.value);
|
|
886
|
+
if (!existingB.ok)
|
|
887
|
+
return err(new Error(`vault_merge: document not found: ${pathB.value}`));
|
|
888
|
+
const parsedA = parseDocument(existingA.value);
|
|
889
|
+
if (!parsedA.ok)
|
|
890
|
+
return parsedA;
|
|
891
|
+
const parsedB = parseDocument(existingB.value);
|
|
892
|
+
if (!parsedB.ok)
|
|
893
|
+
return parsedB;
|
|
894
|
+
const config = loadConfig(vaultRoot);
|
|
895
|
+
if (!config.ok)
|
|
896
|
+
return config;
|
|
897
|
+
const extensions = config.value.schemaExtensions;
|
|
898
|
+
// RBAC: a merge writes/mutates all three docs, so the caller needs write on
|
|
899
|
+
// each one's collection.
|
|
900
|
+
if (access) {
|
|
901
|
+
// The target's collection is the override, else path_a's *raw* collection
|
|
902
|
+
// (the value actually serialized into the target below — not the validated
|
|
903
|
+
// frontmatter, which may differ if path_a's raw collection is malformed),
|
|
904
|
+
// else the target path's top-level dir. Deriving it from the same source
|
|
905
|
+
// the write uses keeps the gate and the write in agreement.
|
|
906
|
+
const rawACollection = parsedA.value.raw.collection;
|
|
907
|
+
const collections = [
|
|
908
|
+
collectionOf(pathA.value, parsedA.value.frontmatter),
|
|
909
|
+
collectionOf(pathB.value, parsedB.value.frontmatter),
|
|
910
|
+
typeof frontmatterOverrides.collection === "string" && frontmatterOverrides.collection
|
|
911
|
+
? frontmatterOverrides.collection
|
|
912
|
+
: typeof rawACollection === "string" && rawACollection
|
|
913
|
+
? rawACollection
|
|
914
|
+
: (targetPath.value.split("/")[0] ?? ""),
|
|
915
|
+
];
|
|
916
|
+
for (const collection of collections) {
|
|
917
|
+
if (!canWrite(access.role, collection)) {
|
|
918
|
+
return err(new Error(`access denied: role '${access.roleName}' cannot write to ` +
|
|
919
|
+
`collection '${collection}'`));
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
// Target frontmatter: inherit path_a's raw frontmatter, apply caller
|
|
924
|
+
// overrides, stamp provenance/updated/updated_by. If the target file already
|
|
925
|
+
// exists, preserve its `created` (the vault_write update idiom); otherwise
|
|
926
|
+
// inherit path_a's. The supplied body replaces whatever was there.
|
|
927
|
+
const existingTarget = await readFile(resolvedTarget.value);
|
|
928
|
+
let targetOldFrontmatter = null;
|
|
929
|
+
let targetCreated = parsedA.value.frontmatter.created;
|
|
930
|
+
if (existingTarget.ok) {
|
|
931
|
+
const parsedTarget = parseDocument(existingTarget.value);
|
|
932
|
+
if (parsedTarget.ok) {
|
|
933
|
+
targetOldFrontmatter = parsedTarget.value.frontmatter;
|
|
934
|
+
targetCreated = parsedTarget.value.frontmatter.created;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
const targetRaw = {
|
|
938
|
+
...parsedA.value.raw,
|
|
939
|
+
provenance: "synthesized",
|
|
940
|
+
...frontmatterOverrides,
|
|
941
|
+
created: targetCreated,
|
|
942
|
+
updated: todayISO(),
|
|
943
|
+
updated_by: agent.value,
|
|
944
|
+
};
|
|
945
|
+
const { frontmatter: targetFm, report: targetReport } = validateFrontmatter(targetRaw, extensions);
|
|
946
|
+
if (!targetReport.valid) {
|
|
947
|
+
const summary = targetReport.issues.map((i) => `${i.field}: ${i.message}`).join("; ");
|
|
948
|
+
return err(new Error(`vault_merge: target frontmatter is invalid: ${summary}`));
|
|
949
|
+
}
|
|
950
|
+
const stampedTarget = {
|
|
951
|
+
...targetFm,
|
|
952
|
+
created: targetCreated,
|
|
953
|
+
updated: todayISO(),
|
|
954
|
+
updated_by: agent.value,
|
|
955
|
+
};
|
|
956
|
+
// Build the write set. The target is always written. Each source that is NOT
|
|
957
|
+
// the target is superseded to point at the target.
|
|
958
|
+
const writes = [];
|
|
959
|
+
writes.push({
|
|
960
|
+
relPath: targetPath.value,
|
|
961
|
+
absPath: resolvedTarget.value,
|
|
962
|
+
fileText: serializeDocument(stampedTarget, body, extensions, applyExtensionDefaults(targetRaw, extensions)),
|
|
963
|
+
newFrontmatter: stampedTarget,
|
|
964
|
+
oldFrontmatter: targetOldFrontmatter,
|
|
965
|
+
action: "merge",
|
|
966
|
+
});
|
|
967
|
+
for (const source of [
|
|
968
|
+
{ path: pathA.value, abs: resolvedA.value, parsed: parsedA.value },
|
|
969
|
+
{ path: pathB.value, abs: resolvedB.value, parsed: parsedB.value },
|
|
970
|
+
]) {
|
|
971
|
+
// Compare resolved absolute paths, not raw strings: a source that aliases
|
|
972
|
+
// the target (e.g. target `./pricing/a.md`, source `pricing/a.md`) is the
|
|
973
|
+
// fold-into-A case — write it once with the merged body, never supersede it.
|
|
974
|
+
if (source.abs === resolvedTarget.value)
|
|
975
|
+
continue;
|
|
976
|
+
const supersededFm = {
|
|
977
|
+
...source.parsed.frontmatter,
|
|
978
|
+
status: "superseded",
|
|
979
|
+
superseded_by: targetPath.value,
|
|
980
|
+
updated: todayISO(),
|
|
981
|
+
updated_by: agent.value,
|
|
982
|
+
};
|
|
983
|
+
writes.push({
|
|
984
|
+
relPath: source.path,
|
|
985
|
+
absPath: source.abs,
|
|
986
|
+
fileText: serializeDocument(supersededFm, source.parsed.content, extensions, source.parsed.raw),
|
|
987
|
+
newFrontmatter: supersededFm,
|
|
988
|
+
oldFrontmatter: source.parsed.frontmatter,
|
|
989
|
+
action: "supersede",
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
// Acquire file locks on every distinct path, in a deterministic sorted order
|
|
993
|
+
// (defensive against self-deadlock if two merges ever overlapped — the
|
|
994
|
+
// one-process invariant makes that theoretical). Release all in finally.
|
|
995
|
+
const lockDbResult = openLockDb(vaultRoot);
|
|
996
|
+
if (!lockDbResult.ok)
|
|
997
|
+
return lockDbResult;
|
|
998
|
+
const lockDb = lockDbResult.value;
|
|
999
|
+
const lockPaths = [...new Set(writes.map((w) => w.relPath))].sort();
|
|
1000
|
+
const held = [];
|
|
1001
|
+
try {
|
|
1002
|
+
for (const relPath of lockPaths) {
|
|
1003
|
+
const lock = acquireLock(lockDb, relPath, agent.value);
|
|
1004
|
+
if (!lock.ok)
|
|
1005
|
+
return lock;
|
|
1006
|
+
held.push(relPath);
|
|
1007
|
+
}
|
|
1008
|
+
// Write all files, then a single git commit. This is NOT crash-atomic on
|
|
1009
|
+
// the disk-write phase: like the single-file write path (performWrite), a
|
|
1010
|
+
// throw mid-loop — or a commit() failure after the files are on disk —
|
|
1011
|
+
// leaves the working tree dirty and uncommitted. For merge the dirty state
|
|
1012
|
+
// can be a *partial* merge (target written, a source not yet superseded),
|
|
1013
|
+
// which a later reindex/watcher would pick up. The git commit itself is
|
|
1014
|
+
// atomic, and the one-process invariant plus human-gated ratification keep
|
|
1015
|
+
// this window small; a full pre-write snapshot/rollback is deferred.
|
|
1016
|
+
for (const w of writes) {
|
|
1017
|
+
await mkdir(dirname(w.absPath), { recursive: true });
|
|
1018
|
+
await writeFile(w.absPath, w.fileText, "utf-8");
|
|
1019
|
+
}
|
|
1020
|
+
let commitHash = null;
|
|
1021
|
+
if (config.value.autoCommit) {
|
|
1022
|
+
const committed = await commit(vaultRoot, writes.map((w) => w.relPath), `vault_merge: ${pathA.value} + ${pathB.value} → ${targetPath.value} by ${agent.value}`, agent.value);
|
|
1023
|
+
if (!committed.ok)
|
|
1024
|
+
return committed;
|
|
1025
|
+
commitHash = committed.value.hash;
|
|
1026
|
+
}
|
|
1027
|
+
// Index each written doc and log provenance per file. Both are best-effort
|
|
1028
|
+
// (the index is a rebuildable cache; the log is advisory) so a failure here
|
|
1029
|
+
// does not unwind the durable commit. noteSelfWrite runs here, after the
|
|
1030
|
+
// index lands, so the fs.watch reactive indexer drops the redundant change
|
|
1031
|
+
// event for our own write (mirrors performWrite's ordering).
|
|
1032
|
+
let allIndexed = true;
|
|
1033
|
+
for (const w of writes) {
|
|
1034
|
+
const indexed = await indexDocument(vaultRoot, w.relPath);
|
|
1035
|
+
if (!indexed.ok)
|
|
1036
|
+
allIndexed = false;
|
|
1037
|
+
noteSelfWrite(w.absPath);
|
|
1038
|
+
await recordProvenance(vaultRoot, {
|
|
1039
|
+
tool: "vault_merge",
|
|
1040
|
+
file: w.relPath,
|
|
1041
|
+
agent: agent.value,
|
|
1042
|
+
action: w.action,
|
|
1043
|
+
frontmatter_diff: frontmatterDiff(w.oldFrontmatter, w.newFrontmatter),
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
return ok({
|
|
1047
|
+
path: targetPath.value,
|
|
1048
|
+
action: "merge",
|
|
1049
|
+
commit: commitHash,
|
|
1050
|
+
committed: config.value.autoCommit,
|
|
1051
|
+
status: stampedTarget.status,
|
|
1052
|
+
updated: stampedTarget.updated,
|
|
1053
|
+
validation: targetReport,
|
|
1054
|
+
indexUpdated: allIndexed,
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
catch (e) {
|
|
1058
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
1059
|
+
return err(new Error(`vault_merge failed: ${reason}`));
|
|
1060
|
+
}
|
|
1061
|
+
finally {
|
|
1062
|
+
for (const relPath of held)
|
|
1063
|
+
releaseLock(lockDb, relPath, agent.value);
|
|
1064
|
+
lockDb.close();
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
// ---------------------------------------------------------------------------
|
|
649
1068
|
// MCP tool definitions
|
|
650
1069
|
// ---------------------------------------------------------------------------
|
|
651
1070
|
const agentProperty = {
|
|
@@ -844,5 +1263,108 @@ export const writeTools = [
|
|
|
844
1263
|
},
|
|
845
1264
|
handler: (vaultRoot, args, access) => vaultDeprecate(vaultRoot, args, access),
|
|
846
1265
|
},
|
|
1266
|
+
{
|
|
1267
|
+
name: "vault_set_confidence",
|
|
1268
|
+
title: "Set a document's confidence",
|
|
1269
|
+
annotations: { destructiveHint: true },
|
|
1270
|
+
description: "Change only a document's confidence level (low | medium | high), leaving " +
|
|
1271
|
+
"its status and body untouched. A reason is required and recorded. Rejects " +
|
|
1272
|
+
"if the confidence is already at the target. Auto-commits.",
|
|
1273
|
+
inputSchema: {
|
|
1274
|
+
type: "object",
|
|
1275
|
+
properties: {
|
|
1276
|
+
path: {
|
|
1277
|
+
type: "string",
|
|
1278
|
+
description: "Vault-relative path of the document",
|
|
1279
|
+
},
|
|
1280
|
+
confidence: {
|
|
1281
|
+
type: "string",
|
|
1282
|
+
enum: [...CONFIDENCES],
|
|
1283
|
+
description: "The new confidence level",
|
|
1284
|
+
},
|
|
1285
|
+
reason: {
|
|
1286
|
+
type: "string",
|
|
1287
|
+
description: "Why the confidence is changing (recorded in the commit and provenance)",
|
|
1288
|
+
},
|
|
1289
|
+
agent: agentProperty,
|
|
1290
|
+
base_version: baseVersionProperty,
|
|
1291
|
+
},
|
|
1292
|
+
required: ["path", "confidence", "reason", "agent"],
|
|
1293
|
+
additionalProperties: false,
|
|
1294
|
+
},
|
|
1295
|
+
handler: (vaultRoot, args, access) => vaultSetConfidence(vaultRoot, args, access),
|
|
1296
|
+
},
|
|
1297
|
+
{
|
|
1298
|
+
name: "vault_supersede",
|
|
1299
|
+
title: "Supersede a document",
|
|
1300
|
+
annotations: { destructiveHint: true },
|
|
1301
|
+
description: "Mark a document superseded by a named successor. Sets status=superseded " +
|
|
1302
|
+
"and superseded_by; the successor must already exist. Distinct from " +
|
|
1303
|
+
"vault_deprecate (which sets status=deprecated with an optional " +
|
|
1304
|
+
"successor). Auto-commits.",
|
|
1305
|
+
inputSchema: {
|
|
1306
|
+
type: "object",
|
|
1307
|
+
properties: {
|
|
1308
|
+
old_path: {
|
|
1309
|
+
type: "string",
|
|
1310
|
+
description: "Vault-relative path of the document being superseded",
|
|
1311
|
+
},
|
|
1312
|
+
new_path: {
|
|
1313
|
+
type: "string",
|
|
1314
|
+
description: "Vault-relative path of the successor that replaces it (must exist)",
|
|
1315
|
+
},
|
|
1316
|
+
reason: {
|
|
1317
|
+
type: "string",
|
|
1318
|
+
description: "Optional reason recorded in the commit message",
|
|
1319
|
+
},
|
|
1320
|
+
agent: agentProperty,
|
|
1321
|
+
base_version: baseVersionProperty,
|
|
1322
|
+
},
|
|
1323
|
+
required: ["old_path", "new_path", "agent"],
|
|
1324
|
+
additionalProperties: false,
|
|
1325
|
+
},
|
|
1326
|
+
handler: (vaultRoot, args, access) => vaultSupersede(vaultRoot, args, access),
|
|
1327
|
+
},
|
|
1328
|
+
{
|
|
1329
|
+
name: "vault_merge",
|
|
1330
|
+
title: "Merge two documents into one",
|
|
1331
|
+
annotations: { destructiveHint: true },
|
|
1332
|
+
description: "Combine two source documents into a target and supersede both sources to " +
|
|
1333
|
+
"point at it, all in one commit. The merged body is supplied by the caller " +
|
|
1334
|
+
"— vault_merge does not synthesize prose. target_path may equal path_a (to " +
|
|
1335
|
+
"fold B into A) or be a new path. The target's frontmatter inherits " +
|
|
1336
|
+
"path_a's (provenance becomes 'synthesized') unless overridden. Auto-commits.",
|
|
1337
|
+
inputSchema: {
|
|
1338
|
+
type: "object",
|
|
1339
|
+
properties: {
|
|
1340
|
+
path_a: {
|
|
1341
|
+
type: "string",
|
|
1342
|
+
description: "Vault-relative path of the first source document",
|
|
1343
|
+
},
|
|
1344
|
+
path_b: {
|
|
1345
|
+
type: "string",
|
|
1346
|
+
description: "Vault-relative path of the second source document",
|
|
1347
|
+
},
|
|
1348
|
+
target_path: {
|
|
1349
|
+
type: "string",
|
|
1350
|
+
description: "Vault-relative path of the merge target (may equal path_a, or be new)",
|
|
1351
|
+
},
|
|
1352
|
+
body: {
|
|
1353
|
+
type: "string",
|
|
1354
|
+
description: "The merged markdown body for the target (no frontmatter)",
|
|
1355
|
+
},
|
|
1356
|
+
frontmatter: {
|
|
1357
|
+
type: "object",
|
|
1358
|
+
description: "Optional frontmatter overrides for the target. Defaults to path_a's " +
|
|
1359
|
+
"frontmatter with provenance set to 'synthesized'.",
|
|
1360
|
+
additionalProperties: true,
|
|
1361
|
+
},
|
|
1362
|
+
agent: agentProperty,
|
|
1363
|
+
},
|
|
1364
|
+
required: ["path_a", "path_b", "target_path", "body", "agent"],
|
|
1365
|
+
additionalProperties: false,
|
|
1366
|
+
},
|
|
1367
|
+
handler: (vaultRoot, args, access) => vaultMerge(vaultRoot, args, access),
|
|
1368
|
+
},
|
|
847
1369
|
];
|
|
848
1370
|
//# sourceMappingURL=write.js.map
|