@saasontools/strauss-kb 0.1.6 → 0.1.8
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/README.md +111 -6
- package/dist/{chunk-V7TRZ2ER.js → chunk-MGPYUZOM.js} +32 -11
- package/dist/chunk-MGPYUZOM.js.map +1 -0
- package/dist/{chunk-PNSRTKYN.js → chunk-YJK7KGHN.js} +743 -131
- package/dist/chunk-YJK7KGHN.js.map +1 -0
- package/dist/{chunk-BVF7X5VO.js → chunk-YYX6CX6V.js} +5 -4
- package/dist/chunk-YYX6CX6V.js.map +1 -0
- package/dist/cli-main.cjs +757 -149
- package/dist/cli-main.cjs.map +1 -1
- package/dist/cli-main.js +2 -2
- package/dist/index.cjs +743 -97
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +181 -8
- package/dist/index.d.ts +181 -8
- package/dist/index.js +29 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp-main.cjs +730 -142
- package/dist/mcp-main.cjs.map +1 -1
- package/dist/mcp-main.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-BVF7X5VO.js.map +0 -1
- package/dist/chunk-PNSRTKYN.js.map +0 -1
- package/dist/chunk-V7TRZ2ER.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -35,6 +35,12 @@ __export(index_exports, {
|
|
|
35
35
|
CONTEXT_END: () => CONTEXT_END,
|
|
36
36
|
CONTEXT_PROFILES: () => CONTEXT_PROFILES,
|
|
37
37
|
DECISION_TYPE: () => DECISION_TYPE,
|
|
38
|
+
DEFAULT_AGING_DAYS: () => DEFAULT_AGING_DAYS,
|
|
39
|
+
DEFAULT_EXPIRING_DAYS: () => DEFAULT_EXPIRING_DAYS,
|
|
40
|
+
DEFAULT_LOAD_BUDGET: () => DEFAULT_LOAD_BUDGET,
|
|
41
|
+
DEFAULT_PACK_HOPS: () => DEFAULT_PACK_HOPS,
|
|
42
|
+
DEFAULT_PACK_MAX_NODES: () => DEFAULT_PACK_MAX_NODES,
|
|
43
|
+
DEFAULT_UNVERIFIED_DAYS: () => DEFAULT_UNVERIFIED_DAYS,
|
|
38
44
|
ErrorTypes: () => ErrorTypes,
|
|
39
45
|
Fault: () => Fault,
|
|
40
46
|
INDEX_FILE: () => INDEX_FILE,
|
|
@@ -43,12 +49,15 @@ __export(index_exports, {
|
|
|
43
49
|
KB_CONCEPT_ID_PATTERN: () => KB_CONCEPT_ID_PATTERN,
|
|
44
50
|
KB_CONFIDENCES: () => KB_CONFIDENCES,
|
|
45
51
|
KB_DIR: () => KB_DIR,
|
|
52
|
+
KB_DOCTOR_CHECKS: () => KB_DOCTOR_CHECKS,
|
|
53
|
+
KB_EDGE_KINDS: () => KB_EDGE_KINDS,
|
|
46
54
|
KB_MATERIALITIES: () => KB_MATERIALITIES,
|
|
47
55
|
KB_RECORD_STATUSES: () => KB_RECORD_STATUSES,
|
|
48
56
|
KB_RECORD_TYPES: () => KB_RECORD_TYPES,
|
|
49
57
|
KB_SLUG_PATTERN: () => KB_SLUG_PATTERN,
|
|
50
58
|
KbBaseFrozenError: () => KbBaseFrozenError,
|
|
51
59
|
KbInvalidConceptIdError: () => KbInvalidConceptIdError,
|
|
60
|
+
KbPackBudgetExceededError: () => KbPackBudgetExceededError,
|
|
52
61
|
KbPinsMalformedError: () => KbPinsMalformedError,
|
|
53
62
|
KbRecordAlreadyExistsError: () => KbRecordAlreadyExistsError,
|
|
54
63
|
KbRecordNotFoundError: () => KbRecordNotFoundError,
|
|
@@ -73,6 +82,8 @@ __export(index_exports, {
|
|
|
73
82
|
contextProfileBudgets: () => contextProfileBudgets,
|
|
74
83
|
createKbMcpServer: () => createKbMcpServer,
|
|
75
84
|
decisionInputSchema: () => decisionInputSchema,
|
|
85
|
+
doctor: () => doctor,
|
|
86
|
+
edgeNeighbours: () => edgeNeighbours,
|
|
76
87
|
indexIsStale: () => indexIsStale,
|
|
77
88
|
isKbRecordType: () => isKbRecordType,
|
|
78
89
|
isNoDecisionRecord: () => isNoDecisionRecord,
|
|
@@ -88,6 +99,8 @@ __export(index_exports, {
|
|
|
88
99
|
loadQmd: () => loadQmd,
|
|
89
100
|
matchToDiff: () => matchToDiff,
|
|
90
101
|
mergedContextBudgets: () => mergedContextBudgets,
|
|
102
|
+
neighbours: () => neighbours,
|
|
103
|
+
pack: () => pack,
|
|
91
104
|
parseLog: () => parseLog,
|
|
92
105
|
parseMarkdownWithFrontmatter: () => parseMarkdownWithFrontmatter,
|
|
93
106
|
pinBase: () => pinBase,
|
|
@@ -244,6 +257,7 @@ var Fault = /* @__PURE__ */ ((Fault2) => {
|
|
|
244
257
|
var ErrorTypes = /* @__PURE__ */ ((ErrorTypes2) => {
|
|
245
258
|
ErrorTypes2["KbRecordAlreadyExists"] = "KbRecordAlreadyExists";
|
|
246
259
|
ErrorTypes2["KbInvalidConceptId"] = "KbInvalidConceptId";
|
|
260
|
+
ErrorTypes2["KbPackBudgetExceeded"] = "KbPackBudgetExceeded";
|
|
247
261
|
ErrorTypes2["KbRecordNotFound"] = "KbRecordNotFound";
|
|
248
262
|
ErrorTypes2["KbSelfVerification"] = "KbSelfVerification";
|
|
249
263
|
ErrorTypes2["KbWriteConflict"] = "KbWriteConflict";
|
|
@@ -333,6 +347,27 @@ var KbSelfVerificationError = class extends BaseError {
|
|
|
333
347
|
actor;
|
|
334
348
|
generatedBy;
|
|
335
349
|
};
|
|
350
|
+
var KbPackBudgetExceededError = class extends BaseError {
|
|
351
|
+
constructor(recordCount, approxTokens2, budgetTokens, excluded) {
|
|
352
|
+
super({
|
|
353
|
+
message: `kb: a pack of ${recordCount} records is ~${approxTokens2} tokens against a budget of ${budgetTokens} \u2014 lower hops or maxNodes, or raise the budget`,
|
|
354
|
+
errorType: "KbPackBudgetExceeded" /* KbPackBudgetExceeded */,
|
|
355
|
+
code: 400,
|
|
356
|
+
fault: "User" /* User */,
|
|
357
|
+
retriable: false,
|
|
358
|
+
reportToUser: true,
|
|
359
|
+
details: { recordCount, approxTokens: approxTokens2, budgetTokens, excluded }
|
|
360
|
+
});
|
|
361
|
+
this.recordCount = recordCount;
|
|
362
|
+
this.approxTokens = approxTokens2;
|
|
363
|
+
this.budgetTokens = budgetTokens;
|
|
364
|
+
this.excluded = excluded;
|
|
365
|
+
}
|
|
366
|
+
recordCount;
|
|
367
|
+
approxTokens;
|
|
368
|
+
budgetTokens;
|
|
369
|
+
excluded;
|
|
370
|
+
};
|
|
336
371
|
var KbInvalidConceptIdError = class extends BaseError {
|
|
337
372
|
constructor(message, details) {
|
|
338
373
|
super({
|
|
@@ -585,41 +620,48 @@ async function loadQmd(logger) {
|
|
|
585
620
|
}
|
|
586
621
|
}
|
|
587
622
|
|
|
588
|
-
// src/
|
|
589
|
-
var
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
for (const
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
if (existing.depth > 0 && !existing.via.includes(edge)) {
|
|
608
|
-
existing.via.push(edge);
|
|
609
|
-
}
|
|
610
|
-
continue;
|
|
611
|
-
}
|
|
612
|
-
reached.set(record.conceptId, { record, depth, via: [edge] });
|
|
613
|
-
next.push(record);
|
|
614
|
-
}
|
|
623
|
+
// src/kb-edges.ts
|
|
624
|
+
var KB_EDGE_KINDS = [
|
|
625
|
+
"body-link",
|
|
626
|
+
"supersession",
|
|
627
|
+
"anchor",
|
|
628
|
+
"source"
|
|
629
|
+
];
|
|
630
|
+
var BODY_LINK_TARGET = new RegExp(
|
|
631
|
+
`\\]\\((${KB_CONCEPT_ID_PATTERN.source.replace(/^\^|\$$/g, "")})\\.md\\)`,
|
|
632
|
+
"g"
|
|
633
|
+
);
|
|
634
|
+
function neighbours(from, bundle, kinds = KB_EDGE_KINDS) {
|
|
635
|
+
const found = /* @__PURE__ */ new Map();
|
|
636
|
+
for (const kind of kinds) {
|
|
637
|
+
for (const record of edgeNeighbours(from, bundle, kind)) {
|
|
638
|
+
const existing = found.get(record.conceptId);
|
|
639
|
+
if (existing) {
|
|
640
|
+
if (!existing.via.includes(kind)) existing.via.push(kind);
|
|
641
|
+
continue;
|
|
615
642
|
}
|
|
643
|
+
found.set(record.conceptId, { record, via: [kind] });
|
|
616
644
|
}
|
|
617
|
-
frontier = next;
|
|
618
645
|
}
|
|
619
|
-
return [...
|
|
646
|
+
return [...found.values()];
|
|
620
647
|
}
|
|
621
|
-
function
|
|
622
|
-
switch (
|
|
648
|
+
function edgeNeighbours(from, bundle, kind) {
|
|
649
|
+
switch (kind) {
|
|
650
|
+
// A link whose target is not in the bundle is legal per compose.ts —
|
|
651
|
+
// records are routinely written before the ones they point at exist — so
|
|
652
|
+
// missing targets are skipped, never an error.
|
|
653
|
+
case "body-link": {
|
|
654
|
+
const targets = new Set(
|
|
655
|
+
[...from.body.matchAll(BODY_LINK_TARGET)].map((match) => match[1])
|
|
656
|
+
);
|
|
657
|
+
if (!targets.size) return [];
|
|
658
|
+
return bundle.filter(
|
|
659
|
+
(candidate) => candidate.conceptId !== from.conceptId && targets.has(candidate.conceptId)
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
// Both directions and both pointers: `supersede()` writes the pair, but a
|
|
663
|
+
// hand-edit can leave one side behind, and a walk trusting one pointer
|
|
664
|
+
// would miss a replacement the bundle openly declares.
|
|
623
665
|
case "supersession":
|
|
624
666
|
return bundle.filter(
|
|
625
667
|
(candidate) => candidate.conceptId !== from.conceptId && (candidate.conceptId === from.frontmatter.strauss_superseded_by || from.frontmatter.strauss_supersedes?.includes(
|
|
@@ -653,11 +695,129 @@ function anchorsTouch(left, right) {
|
|
|
653
695
|
if (!left.symbol || !right.symbol) return true;
|
|
654
696
|
return left.symbol === right.symbol;
|
|
655
697
|
}
|
|
698
|
+
|
|
699
|
+
// src/trace.ts
|
|
700
|
+
var TRACE_EDGES = ["supersession", "anchor", "source"];
|
|
701
|
+
function trace(seedId, bundle, options = {}) {
|
|
702
|
+
const edges = options.edges?.length ? options.edges : TRACE_EDGES;
|
|
703
|
+
const maxDepth = options.depth ?? 3;
|
|
704
|
+
const byId = new Map(bundle.map((record) => [record.conceptId, record]));
|
|
705
|
+
const seed = byId.get(seedId);
|
|
706
|
+
if (!seed) return [];
|
|
707
|
+
const reached = /* @__PURE__ */ new Map([
|
|
708
|
+
[seedId, { record: seed, depth: 0, via: [] }]
|
|
709
|
+
]);
|
|
710
|
+
let frontier = [seed];
|
|
711
|
+
for (let depth = 1; depth <= maxDepth && frontier.length; depth += 1) {
|
|
712
|
+
const next = [];
|
|
713
|
+
for (const from of frontier) {
|
|
714
|
+
for (const edge of edges) {
|
|
715
|
+
for (const record of edgeNeighbours(from, bundle, edge)) {
|
|
716
|
+
const existing = reached.get(record.conceptId);
|
|
717
|
+
if (existing) {
|
|
718
|
+
if (existing.depth > 0 && !existing.via.includes(edge)) {
|
|
719
|
+
existing.via.push(edge);
|
|
720
|
+
}
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
reached.set(record.conceptId, { record, depth, via: [edge] });
|
|
724
|
+
next.push(record);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
frontier = next;
|
|
729
|
+
}
|
|
730
|
+
return [...reached.values()].sort(byGeneratedAt);
|
|
731
|
+
}
|
|
656
732
|
function byGeneratedAt(left, right) {
|
|
657
733
|
const at = (step) => step.record.frontmatter.generated?.at ?? "";
|
|
658
734
|
return at(left).localeCompare(at(right)) || left.depth - right.depth;
|
|
659
735
|
}
|
|
660
736
|
|
|
737
|
+
// src/pack.ts
|
|
738
|
+
var DEFAULT_PACK_HOPS = 2;
|
|
739
|
+
var DEFAULT_PACK_MAX_NODES = 20;
|
|
740
|
+
var TYPE_PRIORITY = [
|
|
741
|
+
"decision",
|
|
742
|
+
"constraint",
|
|
743
|
+
"requirement",
|
|
744
|
+
...KB_RECORD_TYPES.filter(
|
|
745
|
+
(type) => !["decision", "constraint", "requirement"].includes(type)
|
|
746
|
+
)
|
|
747
|
+
];
|
|
748
|
+
function pack(bundle, rootId, options = {}) {
|
|
749
|
+
const hops = options.hops ?? DEFAULT_PACK_HOPS;
|
|
750
|
+
const maxNodes = options.maxNodes ?? DEFAULT_PACK_MAX_NODES;
|
|
751
|
+
const budgetTokens = options.budgetTokens ?? DEFAULT_LOAD_BUDGET;
|
|
752
|
+
const byId = new Map(bundle.map((record) => [record.conceptId, record]));
|
|
753
|
+
const root = byId.get(rootId);
|
|
754
|
+
if (!root) throw new KbRecordNotFoundError(rootId);
|
|
755
|
+
const reached = [{ record: root, depth: 0 }];
|
|
756
|
+
const seen = /* @__PURE__ */ new Set([rootId]);
|
|
757
|
+
let frontier = [root];
|
|
758
|
+
for (let depth = 1; frontier.length; depth += 1) {
|
|
759
|
+
const next = [];
|
|
760
|
+
for (const from of frontier) {
|
|
761
|
+
for (const { record } of neighbours(from, bundle)) {
|
|
762
|
+
if (seen.has(record.conceptId)) continue;
|
|
763
|
+
seen.add(record.conceptId);
|
|
764
|
+
reached.push({ record, depth });
|
|
765
|
+
next.push(record);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
frontier = next;
|
|
769
|
+
}
|
|
770
|
+
reached.sort(byRank);
|
|
771
|
+
const within = reached.filter((entry) => entry.depth <= hops);
|
|
772
|
+
const kept = within.slice(0, maxNodes);
|
|
773
|
+
const excluded = [
|
|
774
|
+
...within.slice(maxNodes),
|
|
775
|
+
...reached.filter((entry) => entry.depth > hops)
|
|
776
|
+
].map((entry) => entry.record.conceptId).sort();
|
|
777
|
+
const adjudicated = adjudicate(
|
|
778
|
+
kept.map((entry) => entry.record),
|
|
779
|
+
bundle
|
|
780
|
+
);
|
|
781
|
+
const superseded = adjudicated.filter((hit) => hit.standing === "superseded").map(stub);
|
|
782
|
+
const whole = adjudicated.filter((hit) => hit.standing !== "superseded");
|
|
783
|
+
const tokensLoaded = whole.reduce((total, hit) => total + estimateTokens(hit.record), 0) + superseded.reduce((total, entry) => total + estimateStubTokens(entry), 0);
|
|
784
|
+
const recordCount = adjudicated.length;
|
|
785
|
+
if (tokensLoaded > budgetTokens) {
|
|
786
|
+
throw new KbPackBudgetExceededError(
|
|
787
|
+
recordCount,
|
|
788
|
+
tokensLoaded,
|
|
789
|
+
budgetTokens,
|
|
790
|
+
excluded
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
return {
|
|
794
|
+
root: rootId,
|
|
795
|
+
records: whole.map((hit) => ({
|
|
796
|
+
conceptId: hit.record.conceptId,
|
|
797
|
+
title: hit.record.frontmatter.title ?? null,
|
|
798
|
+
standing: hit.standing,
|
|
799
|
+
supersededBy: hit.heads.map((head) => head.conceptId),
|
|
800
|
+
warnings: hit.warnings,
|
|
801
|
+
anchors: hit.record.frontmatter.strauss_anchors ?? [],
|
|
802
|
+
body: hit.record.body
|
|
803
|
+
})),
|
|
804
|
+
superseded,
|
|
805
|
+
excluded,
|
|
806
|
+
recordCount,
|
|
807
|
+
tokensLoaded,
|
|
808
|
+
budgetTokens
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
function byRank(left, right) {
|
|
812
|
+
return left.depth - right.depth || typeRank(left.record) - typeRank(right.record) || (left.record.frontmatter.title ?? "").localeCompare(
|
|
813
|
+
right.record.frontmatter.title ?? ""
|
|
814
|
+
) || left.record.conceptId.localeCompare(right.record.conceptId);
|
|
815
|
+
}
|
|
816
|
+
function typeRank(record) {
|
|
817
|
+
const index = TYPE_PRIORITY.indexOf(record.frontmatter.type);
|
|
818
|
+
return index === -1 ? TYPE_PRIORITY.length : index;
|
|
819
|
+
}
|
|
820
|
+
|
|
661
821
|
// src/kb-store.ts
|
|
662
822
|
var KB_DIR = (0, import_node_path2.join)(".strauss", "kb");
|
|
663
823
|
var STORE_OWNED = /* @__PURE__ */ new Set([INDEX_FILE, LOG_FILE, SEARCH_INDEX_FILE]);
|
|
@@ -947,6 +1107,10 @@ ${answer}
|
|
|
947
1107
|
async trace(bundlePath2, seedId, options = {}) {
|
|
948
1108
|
return trace(seedId, await this.list(bundlePath2), options);
|
|
949
1109
|
}
|
|
1110
|
+
/** A bounded neighbourhood around one record. See `pack.ts`. */
|
|
1111
|
+
async pack(bundlePath2, rootId, options = {}) {
|
|
1112
|
+
return pack(await this.list(bundlePath2), rootId, options);
|
|
1113
|
+
}
|
|
950
1114
|
/**
|
|
951
1115
|
* The stored index, rebuilt if it disagrees with the records.
|
|
952
1116
|
*
|
|
@@ -1969,6 +2133,258 @@ function validateBundle(records) {
|
|
|
1969
2133
|
return problems;
|
|
1970
2134
|
}
|
|
1971
2135
|
|
|
2136
|
+
// src/doctor.ts
|
|
2137
|
+
var DEFAULT_EXPIRING_DAYS = 30;
|
|
2138
|
+
var DEFAULT_UNVERIFIED_DAYS = 90;
|
|
2139
|
+
var DEFAULT_AGING_DAYS = 90;
|
|
2140
|
+
var KB_DOCTOR_CHECKS = [
|
|
2141
|
+
"expired",
|
|
2142
|
+
"expiring",
|
|
2143
|
+
"unverified",
|
|
2144
|
+
"aging",
|
|
2145
|
+
"orphaned",
|
|
2146
|
+
"broken-supersession",
|
|
2147
|
+
"superseded-but-cited"
|
|
2148
|
+
];
|
|
2149
|
+
var CHECK_HEADLINES = {
|
|
2150
|
+
expired: "past its stale_after date",
|
|
2151
|
+
expiring: "stale_after falls within the window",
|
|
2152
|
+
unverified: "nobody has ever confirmed it, and it is old enough to matter",
|
|
2153
|
+
aging: "still open or still proposed long after it was written",
|
|
2154
|
+
orphaned: "no other record links to it",
|
|
2155
|
+
"broken-supersession": "the supersession pointers do not resolve",
|
|
2156
|
+
"superseded-but-cited": "a live record's body links to one that no longer holds"
|
|
2157
|
+
};
|
|
2158
|
+
var DAY_MS = 864e5;
|
|
2159
|
+
function doctor(bundle, options = {}) {
|
|
2160
|
+
const thresholds = {
|
|
2161
|
+
expiringDays: options.expiringDays ?? DEFAULT_EXPIRING_DAYS,
|
|
2162
|
+
unverifiedDays: options.unverifiedDays ?? DEFAULT_UNVERIFIED_DAYS,
|
|
2163
|
+
agingDays: options.agingDays ?? DEFAULT_AGING_DAYS
|
|
2164
|
+
};
|
|
2165
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
2166
|
+
const adjudicated = adjudicate(bundle, bundle, now);
|
|
2167
|
+
const standings = new Map(
|
|
2168
|
+
adjudicated.map((hit) => [hit.record.conceptId, hit.standing])
|
|
2169
|
+
);
|
|
2170
|
+
const inForce = adjudicated.filter(
|
|
2171
|
+
(hit) => hit.standing !== "superseded" && hit.standing !== "rejected"
|
|
2172
|
+
);
|
|
2173
|
+
const groups = [
|
|
2174
|
+
group("expired", expired(inForce, now)),
|
|
2175
|
+
group("expiring", expiring(inForce, now, thresholds.expiringDays)),
|
|
2176
|
+
group("unverified", unverified(inForce, now, thresholds.unverifiedDays)),
|
|
2177
|
+
group("aging", aging(inForce, now, thresholds.agingDays)),
|
|
2178
|
+
group("orphaned", orphaned(bundle)),
|
|
2179
|
+
group("broken-supersession", brokenSupersession(bundle, adjudicated)),
|
|
2180
|
+
group("superseded-but-cited", supersededButCited(bundle, standings))
|
|
2181
|
+
];
|
|
2182
|
+
const counts = Object.fromEntries(
|
|
2183
|
+
groups.map((entry) => [entry.check, entry.count])
|
|
2184
|
+
);
|
|
2185
|
+
const findingCount = groups.reduce((total, entry) => total + entry.count, 0);
|
|
2186
|
+
return {
|
|
2187
|
+
recordCount: bundle.length,
|
|
2188
|
+
thresholds,
|
|
2189
|
+
counts,
|
|
2190
|
+
groups,
|
|
2191
|
+
findingCount,
|
|
2192
|
+
healthy: findingCount === 0
|
|
2193
|
+
};
|
|
2194
|
+
}
|
|
2195
|
+
function group(check, findings) {
|
|
2196
|
+
return {
|
|
2197
|
+
check,
|
|
2198
|
+
headline: CHECK_HEADLINES[check],
|
|
2199
|
+
count: findings.length,
|
|
2200
|
+
findings
|
|
2201
|
+
};
|
|
2202
|
+
}
|
|
2203
|
+
function expired(hits, now) {
|
|
2204
|
+
const findings = [];
|
|
2205
|
+
for (const hit of hits) {
|
|
2206
|
+
const raw = hit.record.frontmatter.stale_after;
|
|
2207
|
+
if (!raw) continue;
|
|
2208
|
+
const at = Date.parse(raw);
|
|
2209
|
+
if (Number.isNaN(at)) {
|
|
2210
|
+
findings.push(
|
|
2211
|
+
finding(hit.record, `stale_after "${raw}" is not a readable date`)
|
|
2212
|
+
);
|
|
2213
|
+
continue;
|
|
2214
|
+
}
|
|
2215
|
+
if (at < now.getTime()) {
|
|
2216
|
+
findings.push(
|
|
2217
|
+
finding(
|
|
2218
|
+
hit.record,
|
|
2219
|
+
`stale since ${raw} (${daysBetween(at, now.getTime())} days ago)`
|
|
2220
|
+
)
|
|
2221
|
+
);
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
return findings;
|
|
2225
|
+
}
|
|
2226
|
+
function expiring(hits, now, withinDays) {
|
|
2227
|
+
const horizon = now.getTime() + withinDays * DAY_MS;
|
|
2228
|
+
const findings = [];
|
|
2229
|
+
for (const hit of hits) {
|
|
2230
|
+
const raw = hit.record.frontmatter.stale_after;
|
|
2231
|
+
if (!raw) continue;
|
|
2232
|
+
const at = Date.parse(raw);
|
|
2233
|
+
if (Number.isNaN(at) || at < now.getTime() || at > horizon) continue;
|
|
2234
|
+
findings.push(
|
|
2235
|
+
finding(
|
|
2236
|
+
hit.record,
|
|
2237
|
+
`goes stale ${raw} (in ${daysBetween(now.getTime(), at)} days)`
|
|
2238
|
+
)
|
|
2239
|
+
);
|
|
2240
|
+
}
|
|
2241
|
+
return findings;
|
|
2242
|
+
}
|
|
2243
|
+
function unverified(hits, now, olderThanDays) {
|
|
2244
|
+
const findings = [];
|
|
2245
|
+
for (const hit of hits) {
|
|
2246
|
+
if (hit.record.frontmatter.verified?.length) continue;
|
|
2247
|
+
const age = ageInDays(hit.record, now);
|
|
2248
|
+
if (age === null || age <= olderThanDays) continue;
|
|
2249
|
+
findings.push(
|
|
2250
|
+
finding(hit.record, `never verified, written ${age} days ago`)
|
|
2251
|
+
);
|
|
2252
|
+
}
|
|
2253
|
+
return findings;
|
|
2254
|
+
}
|
|
2255
|
+
function aging(hits, now, olderThanDays) {
|
|
2256
|
+
const findings = [];
|
|
2257
|
+
for (const hit of hits) {
|
|
2258
|
+
const status = hit.record.frontmatter.strauss_status;
|
|
2259
|
+
if (status !== "open" && status !== "proposed") continue;
|
|
2260
|
+
const age = ageInDays(hit.record, now);
|
|
2261
|
+
if (age === null || age <= olderThanDays) continue;
|
|
2262
|
+
findings.push(
|
|
2263
|
+
finding(
|
|
2264
|
+
hit.record,
|
|
2265
|
+
status === "open" ? `open for ${age} days` : `proposed ${age} days ago and still unsettled`
|
|
2266
|
+
)
|
|
2267
|
+
);
|
|
2268
|
+
}
|
|
2269
|
+
return findings.sort(
|
|
2270
|
+
(left, right) => left.conceptId.localeCompare(right.conceptId)
|
|
2271
|
+
);
|
|
2272
|
+
}
|
|
2273
|
+
function orphaned(bundle) {
|
|
2274
|
+
const present = new Set(bundle.map((record) => record.conceptId));
|
|
2275
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
2276
|
+
for (const record of bundle) {
|
|
2277
|
+
for (const neighbour of edgeNeighbours(record, bundle, "body-link")) {
|
|
2278
|
+
referenced.add(neighbour.conceptId);
|
|
2279
|
+
}
|
|
2280
|
+
for (const replaced of record.frontmatter.strauss_supersedes ?? []) {
|
|
2281
|
+
referenced.add(replaced);
|
|
2282
|
+
}
|
|
2283
|
+
const replacement = record.frontmatter.strauss_superseded_by;
|
|
2284
|
+
if (replacement && present.has(replacement)) {
|
|
2285
|
+
referenced.add(record.conceptId);
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
return bundle.filter((record) => !referenced.has(record.conceptId)).map((record) => finding(record, "no other record links to it"));
|
|
2289
|
+
}
|
|
2290
|
+
var SUPERSESSION_CHECKS = /* @__PURE__ */ new Set([
|
|
2291
|
+
"superseded_by",
|
|
2292
|
+
"supersedes",
|
|
2293
|
+
"backlink"
|
|
2294
|
+
]);
|
|
2295
|
+
function brokenSupersession(bundle, adjudicated) {
|
|
2296
|
+
const byId = new Map(bundle.map((record) => [record.conceptId, record]));
|
|
2297
|
+
const findings = [];
|
|
2298
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2299
|
+
const add = (record, note) => {
|
|
2300
|
+
const key2 = `${record.conceptId}\0${note}`;
|
|
2301
|
+
if (seen.has(key2)) return;
|
|
2302
|
+
seen.add(key2);
|
|
2303
|
+
findings.push(finding(record, note));
|
|
2304
|
+
};
|
|
2305
|
+
for (const problem of validateBundle(bundle)) {
|
|
2306
|
+
if (!SUPERSESSION_CHECKS.has(problem.check)) continue;
|
|
2307
|
+
const record = byId.get(problem.conceptId);
|
|
2308
|
+
if (record) add(record, problem.note);
|
|
2309
|
+
}
|
|
2310
|
+
for (const record of bundle) {
|
|
2311
|
+
const replacement = record.frontmatter.strauss_superseded_by;
|
|
2312
|
+
if (!replacement) continue;
|
|
2313
|
+
if (!byId.has(replacement)) {
|
|
2314
|
+
add(record, `replacement ${replacement} is missing`);
|
|
2315
|
+
} else if (record.frontmatter.strauss_status !== "superseded") {
|
|
2316
|
+
add(
|
|
2317
|
+
record,
|
|
2318
|
+
`names ${replacement} as its replacement but is not marked superseded`
|
|
2319
|
+
);
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
for (const hit of adjudicated) {
|
|
2323
|
+
for (const warning of hit.warnings) {
|
|
2324
|
+
if (warning.kind === "broken-chain") {
|
|
2325
|
+
add(hit.record, `replacement ${warning.missing} is missing`);
|
|
2326
|
+
} else if (warning.kind === "chain-cycle") {
|
|
2327
|
+
add(
|
|
2328
|
+
hit.record,
|
|
2329
|
+
`supersession chain cycles through ${warning.through.join(" \u2192 ")}`
|
|
2330
|
+
);
|
|
2331
|
+
} else if (warning.kind === "forked-chain") {
|
|
2332
|
+
add(
|
|
2333
|
+
hit.record,
|
|
2334
|
+
`two records claim to replace it: ${warning.heads.join(", ")}`
|
|
2335
|
+
);
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
return findings.sort(
|
|
2340
|
+
(left, right) => left.conceptId.localeCompare(right.conceptId)
|
|
2341
|
+
);
|
|
2342
|
+
}
|
|
2343
|
+
function supersededButCited(bundle, standings) {
|
|
2344
|
+
const byId = new Map(bundle.map((record) => [record.conceptId, record]));
|
|
2345
|
+
const findings = [];
|
|
2346
|
+
for (const record of bundle) {
|
|
2347
|
+
const standing = standings.get(record.conceptId);
|
|
2348
|
+
if (standing === "superseded" || standing === "rejected") continue;
|
|
2349
|
+
for (const target of edgeNeighbours(record, bundle, "body-link")) {
|
|
2350
|
+
const targetStanding = standings.get(target.conceptId);
|
|
2351
|
+
if (targetStanding !== "superseded" && targetStanding !== "rejected") {
|
|
2352
|
+
continue;
|
|
2353
|
+
}
|
|
2354
|
+
if (replaces(record, target)) continue;
|
|
2355
|
+
const replacement = target.frontmatter.strauss_superseded_by;
|
|
2356
|
+
findings.push(
|
|
2357
|
+
finding(
|
|
2358
|
+
record,
|
|
2359
|
+
`cites ${targetStanding} ${target.conceptId}${targetStanding === "superseded" && replacement && byId.has(replacement) ? ` \u2014 replaced by ${replacement}` : ""}`
|
|
2360
|
+
)
|
|
2361
|
+
);
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
return findings;
|
|
2365
|
+
}
|
|
2366
|
+
function replaces(later, earlier) {
|
|
2367
|
+
return (later.frontmatter.strauss_supersedes ?? []).includes(earlier.conceptId) || earlier.frontmatter.strauss_superseded_by === later.conceptId;
|
|
2368
|
+
}
|
|
2369
|
+
function finding(record, note) {
|
|
2370
|
+
return {
|
|
2371
|
+
conceptId: record.conceptId,
|
|
2372
|
+
title: record.frontmatter.title ?? null,
|
|
2373
|
+
status: record.frontmatter.strauss_status,
|
|
2374
|
+
note
|
|
2375
|
+
};
|
|
2376
|
+
}
|
|
2377
|
+
function daysBetween(from, to) {
|
|
2378
|
+
return Math.max(0, Math.floor((to - from) / DAY_MS));
|
|
2379
|
+
}
|
|
2380
|
+
function ageInDays(record, now) {
|
|
2381
|
+
const at = record.frontmatter.generated?.at;
|
|
2382
|
+
if (!at) return null;
|
|
2383
|
+
const written = Date.parse(at);
|
|
2384
|
+
if (Number.isNaN(written)) return null;
|
|
2385
|
+
return daysBetween(written, now.getTime());
|
|
2386
|
+
}
|
|
2387
|
+
|
|
1972
2388
|
// src/decision-record.ts
|
|
1973
2389
|
var import_zod6 = require("zod");
|
|
1974
2390
|
var DECISION_TYPE = "decision";
|
|
@@ -2105,14 +2521,104 @@ var contextCommand = define({
|
|
|
2105
2521
|
}
|
|
2106
2522
|
});
|
|
2107
2523
|
|
|
2108
|
-
// src/commands/
|
|
2524
|
+
// src/commands/doctor.ts
|
|
2109
2525
|
var import_zod10 = require("zod");
|
|
2526
|
+
var days = (what, fallback) => import_zod10.z.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
|
|
2527
|
+
var doctorCommand = define({
|
|
2528
|
+
name: "doctor",
|
|
2529
|
+
tool: "kb_doctor",
|
|
2530
|
+
usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--strict]",
|
|
2531
|
+
description: "A health sweep over a whole base: what the calendar has already retired, what nobody ever confirmed, what has been open or proposed long enough that the status is now the answer, and what the graph has dropped on the floor. Read-only \u2014 it never writes, never supersedes, and never re-dates anything; every finding names a record for a person to repair. Seven checks, grouped and counted: expired (past `stale_after`), expiring (inside the window), unverified (an empty `verified[]` on a record old enough to matter), aging (still `open` or `proposed`), orphaned (no other record links to it), broken supersession (a chain that does not resolve), and superseded-but-cited (a live record whose body links to a record that no longer holds). Every group is reported even when empty, because a check that found nothing and a check that never ran look identical in a report that only lists findings.\n\nThis is the question no reader thinks to ask, which is why it needs a command: decay is invisible from inside a single record \u2014 a stale one reads exactly like a live one, and a question nobody answered reads exactly like one nobody asked. Reach for it when picking up a base someone else kept, before trusting a base you have not touched in months, or on a schedule; kb_validate is the narrower neighbour, checking only whether pointers between records agree.",
|
|
2532
|
+
input: import_zod10.z.object({
|
|
2533
|
+
bundlePath,
|
|
2534
|
+
expiringDays: days(
|
|
2535
|
+
"How far ahead `expiring` looks, in days.",
|
|
2536
|
+
DEFAULT_EXPIRING_DAYS
|
|
2537
|
+
),
|
|
2538
|
+
unverifiedDays: days(
|
|
2539
|
+
"How old an unconfirmed record must be before `unverified` reports it, in days.",
|
|
2540
|
+
DEFAULT_UNVERIFIED_DAYS
|
|
2541
|
+
),
|
|
2542
|
+
agingDays: days(
|
|
2543
|
+
"How long a record may stay `open` or `proposed` before `aging` reports it, in days.",
|
|
2544
|
+
DEFAULT_AGING_DAYS
|
|
2545
|
+
),
|
|
2546
|
+
strict: import_zod10.z.boolean().optional().describe(
|
|
2547
|
+
"Turn an expired record into a non-zero exit for the CLI. No effect on the report itself."
|
|
2548
|
+
)
|
|
2549
|
+
}),
|
|
2550
|
+
// Presence, not truthiness: `--expiring-days ""` is a caller who meant
|
|
2551
|
+
// something and mistyped it, and a falsy test would answer by quietly
|
|
2552
|
+
// sweeping at the default. Passed through as given, the schema rejects it
|
|
2553
|
+
// and says which field.
|
|
2554
|
+
fromArgv: (argv, path) => {
|
|
2555
|
+
const expiring2 = argvFlag(argv, "--expiring-days");
|
|
2556
|
+
const unverified2 = argvFlag(argv, "--unverified-days");
|
|
2557
|
+
const agingDays = argvFlag(argv, "--aging-days");
|
|
2558
|
+
return {
|
|
2559
|
+
bundlePath: path,
|
|
2560
|
+
...expiring2 !== void 0 ? { expiringDays: Number(expiring2) } : {},
|
|
2561
|
+
...unverified2 !== void 0 ? { unverifiedDays: Number(unverified2) } : {},
|
|
2562
|
+
...agingDays !== void 0 ? { agingDays: Number(agingDays) } : {},
|
|
2563
|
+
...argv.includes("--strict") ? { strict: true } : {}
|
|
2564
|
+
};
|
|
2565
|
+
},
|
|
2566
|
+
run: async ({ store, now }, { bundlePath: path, expiringDays, unverifiedDays, agingDays }) => {
|
|
2567
|
+
const checkedAt = now();
|
|
2568
|
+
const report = doctor(await store.list(path), {
|
|
2569
|
+
...expiringDays !== void 0 ? { expiringDays } : {},
|
|
2570
|
+
...unverifiedDays !== void 0 ? { unverifiedDays } : {},
|
|
2571
|
+
...agingDays !== void 0 ? { agingDays } : {},
|
|
2572
|
+
now: new Date(checkedAt)
|
|
2573
|
+
});
|
|
2574
|
+
return { bundlePath: path, checkedAt, ...report };
|
|
2575
|
+
},
|
|
2576
|
+
render: (result) => render(result),
|
|
2577
|
+
// Only expiry, and only under --strict. The other six checks report debt a
|
|
2578
|
+
// reader decides about; an expired record is the base asserting something it
|
|
2579
|
+
// already said it would stop standing behind, which is the one finding a
|
|
2580
|
+
// pipeline can act on without a judgment call.
|
|
2581
|
+
failsWhen: (result, input) => input.strict === true && result.counts.expired > 0
|
|
2582
|
+
});
|
|
2583
|
+
function render(result) {
|
|
2584
|
+
const { thresholds } = result;
|
|
2585
|
+
const lines = [
|
|
2586
|
+
`# KB Doctor \u2014 ${result.bundlePath}`,
|
|
2587
|
+
`records: ${result.recordCount}`,
|
|
2588
|
+
`thresholds: expiring within ${thresholds.expiringDays}d, unverified over ${thresholds.unverifiedDays}d, aging over ${thresholds.agingDays}d`,
|
|
2589
|
+
`checked: ${result.checkedAt}`,
|
|
2590
|
+
""
|
|
2591
|
+
];
|
|
2592
|
+
const width = Math.max(...result.groups.map((group2) => group2.check.length));
|
|
2593
|
+
for (const group2 of result.groups) {
|
|
2594
|
+
lines.push(
|
|
2595
|
+
` ${group2.check.padEnd(width)} ${String(group2.count).padStart(3)} ${group2.headline}`
|
|
2596
|
+
);
|
|
2597
|
+
}
|
|
2598
|
+
for (const group2 of result.groups) {
|
|
2599
|
+
if (!group2.count) continue;
|
|
2600
|
+
lines.push("", `## ${group2.check} (${group2.count})`);
|
|
2601
|
+
for (const found of group2.findings) {
|
|
2602
|
+
lines.push(
|
|
2603
|
+
`- ${found.conceptId}${found.title ? ` \u2014 ${found.title}` : ""}: ${found.note}`
|
|
2604
|
+
);
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
lines.push(
|
|
2608
|
+
"",
|
|
2609
|
+
result.healthy ? "Nothing to repair." : `${result.findingCount} finding${result.findingCount === 1 ? "" : "s"} across ${result.groups.filter((group2) => group2.count).length} of ${result.groups.length} checks.`
|
|
2610
|
+
);
|
|
2611
|
+
return lines.join("\n");
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2614
|
+
// src/commands/list.ts
|
|
2615
|
+
var import_zod11 = require("zod");
|
|
2110
2616
|
var listCommand = define({
|
|
2111
2617
|
name: "list",
|
|
2112
2618
|
tool: "kb_list",
|
|
2113
2619
|
usage: "list [type]",
|
|
2114
2620
|
description: "Every record, optionally narrowed to one type. Use kb_query when you have a question; this is for enumerating.",
|
|
2115
|
-
input:
|
|
2621
|
+
input: import_zod11.z.object({ bundlePath, type: import_zod11.z.enum(KB_RECORD_TYPES).optional() }),
|
|
2116
2622
|
fromArgv: (argv, path) => ({ bundlePath: path, type: argv[1] }),
|
|
2117
2623
|
run: async ({ store }, { bundlePath: path, type }) => (await store.list(path, type)).map((record) => ({
|
|
2118
2624
|
conceptId: record.conceptId,
|
|
@@ -2124,17 +2630,17 @@ var listCommand = define({
|
|
|
2124
2630
|
});
|
|
2125
2631
|
|
|
2126
2632
|
// src/commands/load.ts
|
|
2127
|
-
var
|
|
2633
|
+
var import_zod12 = require("zod");
|
|
2128
2634
|
var loadCommand = define({
|
|
2129
2635
|
name: "load",
|
|
2130
2636
|
tool: "kb_load",
|
|
2131
2637
|
usage: "load [type] [--budget N | --all]",
|
|
2132
2638
|
description: "Load the whole knowledge base at once, each record with its standing. Prefer this over searching: these bases run to a few thousand tokens, and a reader holding all of it has perfect recall and knows why it is asking, which no ranker does. Superseded records arrive under `superseded` as name, replacement and date only \u2014 their bodies no longer hold, and reading one later in a long session is the mistake this prevents; pass the id to kb_trace when you need the history. Rejected and unresolved records arrive whole: what was turned down, and what is still open, is the part a diff cannot show you. Refuses with a count rather than truncating when the base is too large \u2014 a truncated base is indistinguishable from a complete one, and would have you conclude something was never decided from a slice you did not know was a slice. Call at the point of use, not once per session: a base loaded early is summarised away by compaction, so if the visible context holds no records from this base and the question at hand is one it might govern, load before answering \u2014 never conclude nothing was decided from a context with no KB content in it. This tool (with kb_query and kb_trace) is the only supported way to read a base; a raw file read bypasses supersession resolution and returns replaced records as if current.\n\nThat refusal is the default guardrail, meant for an agent that would otherwise burn its whole context on one call. `all` bypasses it and loads everything regardless of size: a deliberate operator with the budget to spend, not something to reach for automatically. It is mutually exclusive with `budgetTokens`. When the reader does not need everything, kb_query or a narrower `type` filter is the better fit than either.",
|
|
2133
|
-
input:
|
|
2639
|
+
input: import_zod12.z.object({
|
|
2134
2640
|
bundlePath,
|
|
2135
|
-
type:
|
|
2136
|
-
budgetTokens:
|
|
2137
|
-
all:
|
|
2641
|
+
type: import_zod12.z.enum(KB_RECORD_TYPES).optional(),
|
|
2642
|
+
budgetTokens: import_zod12.z.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
|
|
2643
|
+
all: import_zod12.z.boolean().optional().describe(
|
|
2138
2644
|
"Load the entire base regardless of size. The deliberate-operator escape hatch; mutually exclusive with budgetTokens."
|
|
2139
2645
|
)
|
|
2140
2646
|
}).refine((value) => !(value.all && value.budgetTokens !== void 0), {
|
|
@@ -2172,25 +2678,25 @@ var loadCommand = define({
|
|
|
2172
2678
|
});
|
|
2173
2679
|
|
|
2174
2680
|
// src/commands/log.ts
|
|
2175
|
-
var
|
|
2681
|
+
var import_zod13 = require("zod");
|
|
2176
2682
|
var logCommand = define({
|
|
2177
2683
|
name: "log",
|
|
2178
2684
|
tool: "kb_log",
|
|
2179
2685
|
usage: "log",
|
|
2180
2686
|
description: "What touched what, and when. The only artifact here that cannot be reconstructed from the records, so malformed lines are reported rather than repaired.",
|
|
2181
|
-
input:
|
|
2687
|
+
input: import_zod13.z.object({ bundlePath }),
|
|
2182
2688
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
2183
2689
|
run: ({ store }, { bundlePath: path }) => store.readLog(path)
|
|
2184
2690
|
});
|
|
2185
2691
|
|
|
2186
2692
|
// src/commands/no-decision.ts
|
|
2187
|
-
var
|
|
2693
|
+
var import_zod14 = require("zod");
|
|
2188
2694
|
var noDecisionCommand = define({
|
|
2189
2695
|
name: "no-decision",
|
|
2190
2696
|
tool: "kb_no_decision",
|
|
2191
2697
|
usage: "no-decision <reason...>",
|
|
2192
2698
|
description: 'Claim in one sentence that there was nothing to decide. Gating on "did you write a decision?" rewards writing a junk one; gating on "did you answer?" does not, so silence has to be expressible. Idempotent \u2014 restating it is not a collision.',
|
|
2193
|
-
input:
|
|
2699
|
+
input: import_zod14.z.object({ bundlePath, reason: import_zod14.z.string().min(1) }),
|
|
2194
2700
|
fromArgv: (argv, path) => ({
|
|
2195
2701
|
bundlePath: path,
|
|
2196
2702
|
reason: argv.slice(1).join(" ").trim()
|
|
@@ -2206,23 +2712,123 @@ var noDecisionCommand = define({
|
|
|
2206
2712
|
}
|
|
2207
2713
|
});
|
|
2208
2714
|
|
|
2715
|
+
// src/commands/pack.ts
|
|
2716
|
+
var import_zod15 = require("zod");
|
|
2717
|
+
var packCommand = define({
|
|
2718
|
+
name: "pack",
|
|
2719
|
+
tool: "kb_pack",
|
|
2720
|
+
usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
|
|
2721
|
+
description: "The bounded neighbourhood around one record: everything within `hops` of the root, ranked and cut to `maxNodes`, with every cut record named under Excluded \u2014 a named gap is knowable, a silent one is not. Prefer this over kb_load when the base is too large to hold whole and the work centres on one record; prefer it over kb_query when the question needs the governed neighbourhood \u2014 what was settled and what binds near this record \u2014 rather than a lookup by wording. Superseded records arrive as name, replacement and date stubs exactly as kb_load emits them: their bodies no longer hold, and kb_trace has the history. Refuses outright rather than truncating when the pack would exceed its token budget \u2014 a partial pack is indistinguishable from a complete one \u2014 reporting the record count and every already-cut id so the caller can lower hops or maxNodes, or raise the budget. The header carries the bundle, root, budget and a timestamp; everything below the header is byte-identical across runs over an unchanged base, so two packs can be diffed and a changed byte means changed knowledge. This tool (with kb_load, kb_query and kb_trace) is the only supported way to read a base; a raw file read bypasses supersession resolution and returns replaced records as if current.",
|
|
2722
|
+
input: import_zod15.z.object({
|
|
2723
|
+
bundlePath,
|
|
2724
|
+
conceptId,
|
|
2725
|
+
hops: import_zod15.z.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
|
|
2726
|
+
maxNodes: import_zod15.z.number().int().positive().optional().describe(
|
|
2727
|
+
"How many records the pack may hold, root included. Defaults to 20."
|
|
2728
|
+
),
|
|
2729
|
+
budgetTokens: import_zod15.z.number().int().positive().optional().describe(
|
|
2730
|
+
"Approximate token ceiling over what is actually emitted. Defaults to 25000."
|
|
2731
|
+
)
|
|
2732
|
+
}),
|
|
2733
|
+
fromArgv: (argv, path) => {
|
|
2734
|
+
const hops = argvFlag(argv, "--hops");
|
|
2735
|
+
const maxNodes = argvFlag(argv, "--max-nodes");
|
|
2736
|
+
const budget = argvFlag(argv, "--budget");
|
|
2737
|
+
return {
|
|
2738
|
+
bundlePath: path,
|
|
2739
|
+
conceptId: argv[1],
|
|
2740
|
+
...hops ? { hops: Number(hops) } : {},
|
|
2741
|
+
...maxNodes ? { maxNodes: Number(maxNodes) } : {},
|
|
2742
|
+
...budget ? { budgetTokens: Number(budget) } : {}
|
|
2743
|
+
};
|
|
2744
|
+
},
|
|
2745
|
+
run: async ({ store, now }, { bundlePath: path, conceptId: root, hops, maxNodes, budgetTokens }) => {
|
|
2746
|
+
const result = await store.pack(path, root, {
|
|
2747
|
+
...hops !== void 0 ? { hops } : {},
|
|
2748
|
+
...maxNodes !== void 0 ? { maxNodes } : {},
|
|
2749
|
+
...budgetTokens !== void 0 ? { budgetTokens } : {}
|
|
2750
|
+
});
|
|
2751
|
+
return render2(result, path, now());
|
|
2752
|
+
}
|
|
2753
|
+
});
|
|
2754
|
+
function render2(result, bundle, at) {
|
|
2755
|
+
const lines = [
|
|
2756
|
+
`# KB Pack \u2014 ${result.root}`,
|
|
2757
|
+
`bundle: ${bundle}`,
|
|
2758
|
+
`budget: ~${result.tokensLoaded} of ${result.budgetTokens} tokens, ${result.recordCount} records`,
|
|
2759
|
+
`packed: ${at}`,
|
|
2760
|
+
"",
|
|
2761
|
+
`## Records (${result.records.length})`
|
|
2762
|
+
];
|
|
2763
|
+
for (const record of result.records) {
|
|
2764
|
+
lines.push(
|
|
2765
|
+
"",
|
|
2766
|
+
`### ${record.conceptId}${record.title ? ` \u2014 ${record.title}` : ""} [${record.standing}]`
|
|
2767
|
+
);
|
|
2768
|
+
if (record.warnings.length) {
|
|
2769
|
+
lines.push(`warnings: ${record.warnings.map(warningLabel).join("; ")}`);
|
|
2770
|
+
}
|
|
2771
|
+
if (record.anchors.length) {
|
|
2772
|
+
lines.push(
|
|
2773
|
+
`anchors: ${record.anchors.map(
|
|
2774
|
+
(anchor) => anchor.symbol ? `${anchor.file}#${anchor.symbol}` : anchor.file
|
|
2775
|
+
).join(", ")}`
|
|
2776
|
+
);
|
|
2777
|
+
}
|
|
2778
|
+
lines.push("", record.body.trimEnd());
|
|
2779
|
+
}
|
|
2780
|
+
if (result.superseded.length) {
|
|
2781
|
+
lines.push("", `## Superseded (${result.superseded.length})`);
|
|
2782
|
+
for (const entry of result.superseded) {
|
|
2783
|
+
lines.push(
|
|
2784
|
+
`- ${entry.conceptId} \u2192 ${entry.supersededBy.join(", ") || "(no surviving head)"}${entry.at ? ` (${entry.at})` : ""}`
|
|
2785
|
+
);
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
if (result.excluded.length) {
|
|
2789
|
+
lines.push("", `## Excluded (${result.excluded.length})`);
|
|
2790
|
+
for (const cut of result.excluded) lines.push(`- ${cut}`);
|
|
2791
|
+
}
|
|
2792
|
+
return lines.join("\n");
|
|
2793
|
+
}
|
|
2794
|
+
function warningLabel(warning) {
|
|
2795
|
+
switch (warning.kind) {
|
|
2796
|
+
case "superseded":
|
|
2797
|
+
return `superseded by ${warning.by.join(", ")}`;
|
|
2798
|
+
case "unsettled":
|
|
2799
|
+
return `unsettled (${warning.status})`;
|
|
2800
|
+
case "broken-chain":
|
|
2801
|
+
return `broken chain \u2014 ${warning.missing} is not in the bundle`;
|
|
2802
|
+
case "chain-cycle":
|
|
2803
|
+
return `chain cycle through ${warning.through.join(" \u2192 ")}`;
|
|
2804
|
+
case "forked-chain":
|
|
2805
|
+
return `forked chain \u2014 heads ${warning.heads.join(", ")}`;
|
|
2806
|
+
case "stale":
|
|
2807
|
+
return `stale since ${warning.staleAfter}`;
|
|
2808
|
+
case "unresolved-question":
|
|
2809
|
+
return "unresolved question";
|
|
2810
|
+
default:
|
|
2811
|
+
return warning.kind;
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2209
2815
|
// src/commands/pin.ts
|
|
2210
|
-
var
|
|
2816
|
+
var import_zod16 = require("zod");
|
|
2211
2817
|
var pinCommand = define({
|
|
2212
2818
|
name: "pin",
|
|
2213
2819
|
tool: "kb_pin",
|
|
2214
2820
|
usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
|
|
2215
2821
|
description: "Pin a base into a workspace pin manifest, so `context` surfaces it at every context birth. Three layers, nearest wins: the committed project manifest (.strauss/kb-pins.json, the default), `--local` (.strauss/kb-pins.local.json, personal and gitignored), and `--user` (~/.strauss/kb-pins.json, every workspace). Idempotent \u2014 re-pinning changes nothing unless --mode, --profiles, or --frozen/--unfreeze are given, which update just those fields. `--mode full` preloads the whole base into the block regardless of the full-under threshold; `--mode index` never upgrades. `--profiles` scopes the pin to named context profiles. `--frozen` marks the base concluded: write commands against it refuse and `context` labels it read-only. A path with no records yet succeeds with a warning; bases are routinely pinned before they are populated. Pins are workspace state: the pinned base itself is never touched.",
|
|
2216
|
-
input:
|
|
2822
|
+
input: import_zod16.z.object({
|
|
2217
2823
|
bundlePath,
|
|
2218
|
-
mode:
|
|
2824
|
+
mode: import_zod16.z.enum(["full", "index"]).optional().describe(
|
|
2219
2825
|
"full: always emit this base's records whole (still under the block budget); index: never upgrade. Absent: the profile's full-under threshold decides."
|
|
2220
2826
|
),
|
|
2221
|
-
profiles:
|
|
2222
|
-
layer:
|
|
2827
|
+
profiles: import_zod16.z.array(import_zod16.z.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
|
|
2828
|
+
layer: import_zod16.z.enum(["project", "local", "user"]).optional().describe(
|
|
2223
2829
|
"Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
|
|
2224
2830
|
),
|
|
2225
|
-
frozen:
|
|
2831
|
+
frozen: import_zod16.z.boolean().optional().describe(
|
|
2226
2832
|
"true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
|
|
2227
2833
|
)
|
|
2228
2834
|
}),
|
|
@@ -2251,29 +2857,29 @@ var pinCommand = define({
|
|
|
2251
2857
|
});
|
|
2252
2858
|
|
|
2253
2859
|
// src/commands/pins.ts
|
|
2254
|
-
var
|
|
2860
|
+
var import_zod17 = require("zod");
|
|
2255
2861
|
var pinsCommand = define({
|
|
2256
2862
|
name: "pins",
|
|
2257
2863
|
tool: "kb_pins",
|
|
2258
2864
|
usage: "pins",
|
|
2259
2865
|
description: "Every pinned base across the manifest layers, each with its layer and whether it currently resolves to readable records. Reads the workspace manifests rather than any one base, like kb_context.",
|
|
2260
|
-
input:
|
|
2866
|
+
input: import_zod17.z.object({}),
|
|
2261
2867
|
fromArgv: () => ({}),
|
|
2262
2868
|
run: ({ store }) => listPins(store, process.cwd())
|
|
2263
2869
|
});
|
|
2264
2870
|
|
|
2265
2871
|
// src/commands/query.ts
|
|
2266
|
-
var
|
|
2872
|
+
var import_zod18 = require("zod");
|
|
2267
2873
|
var queryCommand = define({
|
|
2268
2874
|
name: "query",
|
|
2269
2875
|
tool: "kb_query",
|
|
2270
2876
|
usage: "query <text...>",
|
|
2271
2877
|
description: "Search and return each match with its standing. Results are flagged, never filtered: a superseded record comes back alongside whatever replaced it, and a rejected one is marked as something explicitly not adopted. Prefer kb_load when the base fits its budget: on this package's measurements, a reader holding the whole base answered eight of nine questions whose wording appears in no record, where embedding search answered four. Never read record files directly \u2014 this tool (with kb_load and kb_trace) is the only supported way to read a base; a file read bypasses supersession resolution and returns replaced records as if current.",
|
|
2272
|
-
input:
|
|
2878
|
+
input: import_zod18.z.object({
|
|
2273
2879
|
bundlePath,
|
|
2274
|
-
text:
|
|
2275
|
-
type:
|
|
2276
|
-
includeNonCurrent:
|
|
2880
|
+
text: import_zod18.z.string().optional(),
|
|
2881
|
+
type: import_zod18.z.enum(KB_RECORD_TYPES).optional(),
|
|
2882
|
+
includeNonCurrent: import_zod18.z.boolean().optional()
|
|
2277
2883
|
}),
|
|
2278
2884
|
fromArgv: (argv, path) => ({
|
|
2279
2885
|
bundlePath: path,
|
|
@@ -2295,40 +2901,40 @@ var queryCommand = define({
|
|
|
2295
2901
|
});
|
|
2296
2902
|
|
|
2297
2903
|
// src/commands/read-index.ts
|
|
2298
|
-
var
|
|
2904
|
+
var import_zod19 = require("zod");
|
|
2299
2905
|
var readIndexCommand = define({
|
|
2300
2906
|
name: "index",
|
|
2301
2907
|
tool: "kb_index",
|
|
2302
2908
|
usage: "index",
|
|
2303
2909
|
description: "The index, rebuilt if it disagrees with the records. One call gives the whole shape of the base: title, type, status, and description per record. The cheap re-orientation call after compaction or deep in a long session \u2014 a few hundred tokens; call it (or kb_context, when bases are pinned) first, then kb_load or fetch by concept id.",
|
|
2304
|
-
input:
|
|
2910
|
+
input: import_zod19.z.object({ bundlePath }),
|
|
2305
2911
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
2306
2912
|
run: ({ store }, { bundlePath: path }) => store.readIndex(path)
|
|
2307
2913
|
});
|
|
2308
2914
|
|
|
2309
2915
|
// src/commands/schema.ts
|
|
2310
|
-
var
|
|
2916
|
+
var import_zod20 = require("zod");
|
|
2311
2917
|
var schemaCommand = define({
|
|
2312
2918
|
name: "schema",
|
|
2313
2919
|
tool: "kb_schema",
|
|
2314
2920
|
usage: "schema",
|
|
2315
2921
|
description: "JSON Schema for the frontmatter, the write input, and log entries \u2014 generated from the code that enforces them, so it cannot drift from what a write will accept.",
|
|
2316
|
-
input:
|
|
2922
|
+
input: import_zod20.z.object({}),
|
|
2317
2923
|
fromArgv: () => ({}),
|
|
2318
2924
|
run: () => Promise.resolve(kbJsonSchemas())
|
|
2319
2925
|
});
|
|
2320
2926
|
|
|
2321
2927
|
// src/commands/status.ts
|
|
2322
|
-
var
|
|
2928
|
+
var import_zod21 = require("zod");
|
|
2323
2929
|
var statusCommand = define({
|
|
2324
2930
|
name: "status",
|
|
2325
2931
|
tool: "kb_status",
|
|
2326
2932
|
usage: "status <concept-id> <status>",
|
|
2327
2933
|
description: "Move a record's status, leaving everything else alone. Uses a compare-and-swap, so a concurrent change fails loudly rather than being overwritten.",
|
|
2328
|
-
input:
|
|
2934
|
+
input: import_zod21.z.object({
|
|
2329
2935
|
bundlePath,
|
|
2330
2936
|
conceptId,
|
|
2331
|
-
status:
|
|
2937
|
+
status: import_zod21.z.enum(KB_RECORD_STATUSES)
|
|
2332
2938
|
}),
|
|
2333
2939
|
fromArgv: (argv, path) => ({
|
|
2334
2940
|
bundlePath: path,
|
|
@@ -2343,13 +2949,13 @@ var statusCommand = define({
|
|
|
2343
2949
|
});
|
|
2344
2950
|
|
|
2345
2951
|
// src/commands/supersede.ts
|
|
2346
|
-
var
|
|
2952
|
+
var import_zod22 = require("zod");
|
|
2347
2953
|
var supersedeCommand = define({
|
|
2348
2954
|
name: "supersede",
|
|
2349
2955
|
tool: "kb_supersede",
|
|
2350
2956
|
usage: "supersede <concept-id> <replacement-id>",
|
|
2351
2957
|
description: "Mark a record superseded by another, linking both directions. Use this rather than editing a record whose meaning changed \u2014 a record that quietly becomes something else invalidates every reference to it, and the earlier understanding is what a later trace needs.",
|
|
2352
|
-
input:
|
|
2958
|
+
input: import_zod22.z.object({ bundlePath, conceptId, replacementId: conceptId }),
|
|
2353
2959
|
fromArgv: (argv, path) => ({
|
|
2354
2960
|
bundlePath: path,
|
|
2355
2961
|
conceptId: argv[1],
|
|
@@ -2363,16 +2969,16 @@ var supersedeCommand = define({
|
|
|
2363
2969
|
});
|
|
2364
2970
|
|
|
2365
2971
|
// src/commands/sync-instructions.ts
|
|
2366
|
-
var
|
|
2972
|
+
var import_zod23 = require("zod");
|
|
2367
2973
|
var syncInstructionsCommand = define({
|
|
2368
2974
|
name: "sync-instructions",
|
|
2369
2975
|
usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
|
|
2370
2976
|
description: "Idempotently plant the `context` block between sentinel comments in an instruction file (AGENTS.md, CLAUDE.md), creating the block when absent and leaving everything outside the sentinels alone. CLI-only: this is file plumbing for runtimes whose instruction files are re-read where their conversations are not, not an agent capability \u2014 the capability is kb_context.",
|
|
2371
|
-
input:
|
|
2372
|
-
file:
|
|
2373
|
-
budgetTokens:
|
|
2374
|
-
fullUnderTokens:
|
|
2375
|
-
profile:
|
|
2977
|
+
input: import_zod23.z.object({
|
|
2978
|
+
file: import_zod23.z.string().min(1).describe("The instruction file to edit in place."),
|
|
2979
|
+
budgetTokens: import_zod23.z.number().int().positive().optional(),
|
|
2980
|
+
fullUnderTokens: import_zod23.z.number().int().positive().optional(),
|
|
2981
|
+
profile: import_zod23.z.string().optional()
|
|
2376
2982
|
}),
|
|
2377
2983
|
fromArgv: (argv) => {
|
|
2378
2984
|
const budget = argvFlag(argv, "--budget");
|
|
@@ -2398,17 +3004,17 @@ var syncInstructionsCommand = define({
|
|
|
2398
3004
|
});
|
|
2399
3005
|
|
|
2400
3006
|
// src/commands/trace.ts
|
|
2401
|
-
var
|
|
3007
|
+
var import_zod24 = require("zod");
|
|
2402
3008
|
var traceCommand = define({
|
|
2403
3009
|
name: "trace",
|
|
2404
3010
|
tool: "kb_trace",
|
|
2405
3011
|
usage: "trace <concept-id> [edges...]",
|
|
2406
3012
|
description: 'How a position was arrived at, as a timeline ordered by when each record was written. Deliberately includes rejected, draft, and superseded records \u2014 in a history those are the content, not noise. Follows supersession, shared code anchors, and shared sources. Use when the question is "why is this the way it is" rather than "what do we hold now". This tool (with kb_load and kb_query) is the only supported way to read a base; a raw file read bypasses supersession resolution and returns replaced records as if current.',
|
|
2407
|
-
input:
|
|
3013
|
+
input: import_zod24.z.object({
|
|
2408
3014
|
bundlePath,
|
|
2409
3015
|
conceptId,
|
|
2410
|
-
edges:
|
|
2411
|
-
depth:
|
|
3016
|
+
edges: import_zod24.z.array(import_zod24.z.enum(TRACE_EDGES)).optional(),
|
|
3017
|
+
depth: import_zod24.z.number().int().positive().optional()
|
|
2412
3018
|
}),
|
|
2413
3019
|
fromArgv: (argv, path) => ({
|
|
2414
3020
|
bundlePath: path,
|
|
@@ -2430,53 +3036,53 @@ var traceCommand = define({
|
|
|
2430
3036
|
});
|
|
2431
3037
|
|
|
2432
3038
|
// src/commands/types.ts
|
|
2433
|
-
var
|
|
3039
|
+
var import_zod25 = require("zod");
|
|
2434
3040
|
var typesCommand = define({
|
|
2435
3041
|
name: "types",
|
|
2436
3042
|
tool: "kb_types",
|
|
2437
3043
|
usage: "types",
|
|
2438
3044
|
description: "The twelve record types with their purpose, body sections, and starting status. Read this before writing rather than guessing headings \u2014 a section the type does not define is rejected.",
|
|
2439
|
-
input:
|
|
3045
|
+
input: import_zod25.z.object({}),
|
|
2440
3046
|
fromArgv: () => ({}),
|
|
2441
3047
|
run: () => Promise.resolve(RECORD_TYPES)
|
|
2442
3048
|
});
|
|
2443
3049
|
|
|
2444
3050
|
// src/commands/unpin.ts
|
|
2445
|
-
var
|
|
3051
|
+
var import_zod26 = require("zod");
|
|
2446
3052
|
var unpinCommand = define({
|
|
2447
3053
|
name: "unpin",
|
|
2448
3054
|
tool: "kb_unpin",
|
|
2449
3055
|
usage: "unpin [bundle-path]",
|
|
2450
3056
|
description: "Remove a base from every pin manifest layer that holds it \u2014 project, local, and user \u2014 because unpinned means gone, not still injected from another file. Reports which layers were touched.",
|
|
2451
|
-
input:
|
|
3057
|
+
input: import_zod26.z.object({ bundlePath }),
|
|
2452
3058
|
fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
|
|
2453
3059
|
run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
|
|
2454
3060
|
});
|
|
2455
3061
|
|
|
2456
3062
|
// src/commands/validate.ts
|
|
2457
|
-
var
|
|
3063
|
+
var import_zod27 = require("zod");
|
|
2458
3064
|
var validateCommand = define({
|
|
2459
3065
|
name: "validate",
|
|
2460
3066
|
tool: "kb_validate",
|
|
2461
3067
|
usage: "validate",
|
|
2462
3068
|
description: "Check pointers no single record can see: supersession links that disagree between the two records, and assumptions that cite sources. Per-record shape is enforced on every read, so a problem here means someone edited a file by hand.",
|
|
2463
|
-
input:
|
|
3069
|
+
input: import_zod27.z.object({ bundlePath }),
|
|
2464
3070
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
2465
3071
|
run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
|
|
2466
3072
|
failsWhen: (result) => Array.isArray(result) && result.length > 0
|
|
2467
3073
|
});
|
|
2468
3074
|
|
|
2469
3075
|
// src/commands/verify.ts
|
|
2470
|
-
var
|
|
3076
|
+
var import_zod28 = require("zod");
|
|
2471
3077
|
var verifyCommand = define({
|
|
2472
3078
|
name: "verify",
|
|
2473
3079
|
tool: "kb_verify",
|
|
2474
3080
|
usage: "verify <concept-id> --note <text>",
|
|
2475
3081
|
description: "Append one verified[] event \u2014 who checked the record, when, and what the check found. Appends only; prior events are never rewritten. A record's own generator is refused unless the actor is human: re-reading your own output is not an independent check.",
|
|
2476
|
-
input:
|
|
3082
|
+
input: import_zod28.z.object({
|
|
2477
3083
|
bundlePath,
|
|
2478
3084
|
conceptId,
|
|
2479
|
-
note:
|
|
3085
|
+
note: import_zod28.z.string().refine((s) => s.trim().length > 0, {
|
|
2480
3086
|
message: "note must say what the check found"
|
|
2481
3087
|
})
|
|
2482
3088
|
}),
|
|
@@ -2496,7 +3102,7 @@ var verifyCommand = define({
|
|
|
2496
3102
|
});
|
|
2497
3103
|
|
|
2498
3104
|
// src/commands/write.ts
|
|
2499
|
-
var
|
|
3105
|
+
var import_zod29 = require("zod");
|
|
2500
3106
|
var writeCommand = define({
|
|
2501
3107
|
name: "write",
|
|
2502
3108
|
tool: "kb_write",
|
|
@@ -2510,9 +3116,9 @@ var writeCommand = define({
|
|
|
2510
3116
|
"- Prefer a new record over overloading an existing one, and keep each short. A record nobody finishes reading is not durable memory.",
|
|
2511
3117
|
"- Records are never deleted; supersede instead, so the earlier reasoning stays inspectable."
|
|
2512
3118
|
].join("\n"),
|
|
2513
|
-
input:
|
|
3119
|
+
input: import_zod29.z.object({
|
|
2514
3120
|
bundlePath,
|
|
2515
|
-
type:
|
|
3121
|
+
type: import_zod29.z.enum(KB_RECORD_TYPES),
|
|
2516
3122
|
input: composeInputSchema
|
|
2517
3123
|
}),
|
|
2518
3124
|
fromArgv: async (argv, path, stdin) => ({
|
|
@@ -2536,7 +3142,7 @@ var writeCommand = define({
|
|
|
2536
3142
|
});
|
|
2537
3143
|
|
|
2538
3144
|
// src/commands/write-decision.ts
|
|
2539
|
-
var
|
|
3145
|
+
var import_zod30 = require("zod");
|
|
2540
3146
|
var writeDecisionCommand = define({
|
|
2541
3147
|
name: "write-decision",
|
|
2542
3148
|
tool: "kb_write_decision",
|
|
@@ -2549,7 +3155,7 @@ var writeDecisionCommand = define({
|
|
|
2549
3155
|
"- `alternative` is what you turned down and why, not a list of everything considered.",
|
|
2550
3156
|
"- A reference to material you read goes in `sources`; a reference to code goes in `anchors`; a reference to another record goes in `relatedConceptIds`."
|
|
2551
3157
|
].join("\n"),
|
|
2552
|
-
input:
|
|
3158
|
+
input: import_zod30.z.object({ bundlePath, input: decisionInputSchema }),
|
|
2553
3159
|
fromArgv: async (_argv, path, stdin) => ({
|
|
2554
3160
|
bundlePath: path,
|
|
2555
3161
|
input: JSON.parse(await stdin())
|
|
@@ -2579,12 +3185,14 @@ var KB_COMMANDS = [
|
|
|
2579
3185
|
answerCommand,
|
|
2580
3186
|
verifyCommand,
|
|
2581
3187
|
loadCommand,
|
|
3188
|
+
packCommand,
|
|
2582
3189
|
queryCommand,
|
|
2583
3190
|
traceCommand,
|
|
2584
3191
|
listCommand,
|
|
2585
3192
|
readIndexCommand,
|
|
2586
3193
|
logCommand,
|
|
2587
3194
|
validateCommand,
|
|
3195
|
+
doctorCommand,
|
|
2588
3196
|
schemaCommand,
|
|
2589
3197
|
pinCommand,
|
|
2590
3198
|
unpinCommand,
|
|
@@ -2600,8 +3208,13 @@ var KB_COMMANDS_BY_NAME = new Map(
|
|
|
2600
3208
|
// src/mcp.ts
|
|
2601
3209
|
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
2602
3210
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
3211
|
+
|
|
3212
|
+
// src/version.ts
|
|
3213
|
+
var VERSION = true ? "0.1.8" : "0.0.0-dev";
|
|
3214
|
+
|
|
3215
|
+
// src/mcp.ts
|
|
2603
3216
|
function createKbMcpServer() {
|
|
2604
|
-
const server = new import_mcp.McpServer({ name: "strauss-kb", version:
|
|
3217
|
+
const server = new import_mcp.McpServer({ name: "strauss-kb", version: VERSION });
|
|
2605
3218
|
const store = new KbStore({
|
|
2606
3219
|
warn: (entry) => process.stderr.write(`${JSON.stringify(entry)}
|
|
2607
3220
|
`)
|
|
@@ -2638,14 +3251,28 @@ async function runKbMcpServer() {
|
|
|
2638
3251
|
// src/cli.ts
|
|
2639
3252
|
var import_node_path7 = require("path");
|
|
2640
3253
|
async function runKbCli(argv) {
|
|
2641
|
-
const {
|
|
2642
|
-
const
|
|
3254
|
+
const { flags, literal } = takeLiteral(argv);
|
|
3255
|
+
const { bundle, rest: withFlags } = takeBundle(flags);
|
|
3256
|
+
const name = withFlags[0] ?? "";
|
|
2643
3257
|
if (!name || name === "-h" || name === "--help") {
|
|
2644
3258
|
process.stdout.write(usage());
|
|
2645
3259
|
return;
|
|
2646
3260
|
}
|
|
3261
|
+
if (name === "--version" || name === "-v") {
|
|
3262
|
+
process.stdout.write(`${VERSION}
|
|
3263
|
+
`);
|
|
3264
|
+
return;
|
|
3265
|
+
}
|
|
2647
3266
|
const command = KB_COMMANDS_BY_NAME.get(name);
|
|
2648
3267
|
if (!command) die(`unknown command ${name}`);
|
|
3268
|
+
const json = withFlags.includes("--json");
|
|
3269
|
+
if (json && !command.render) {
|
|
3270
|
+
die(`${name} takes no --json: its result is already the machine shape`);
|
|
3271
|
+
}
|
|
3272
|
+
const rest = [
|
|
3273
|
+
...json ? withFlags.filter((argument) => argument !== "--json") : withFlags,
|
|
3274
|
+
...literal
|
|
3275
|
+
];
|
|
2649
3276
|
const raw = await command.fromArgv(rest, bundle, readStdin);
|
|
2650
3277
|
const parsed = command.input.safeParse(raw);
|
|
2651
3278
|
if (!parsed.success) {
|
|
@@ -2665,13 +3292,16 @@ async function runKbCli(argv) {
|
|
|
2665
3292
|
},
|
|
2666
3293
|
parsed.data
|
|
2667
3294
|
);
|
|
2668
|
-
if (command.failsWhen?.(result)) process.exitCode = 1;
|
|
3295
|
+
if (command.failsWhen?.(result, parsed.data)) process.exitCode = 1;
|
|
2669
3296
|
if (result === "") return;
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
`
|
|
2673
|
-
|
|
2674
|
-
|
|
3297
|
+
const text = command.render && !json ? command.render(result) : typeof result === "string" ? result : JSON.stringify(result, null, 2);
|
|
3298
|
+
process.stdout.write(text.endsWith("\n") ? text : `${text}
|
|
3299
|
+
`);
|
|
3300
|
+
}
|
|
3301
|
+
function takeLiteral(argv) {
|
|
3302
|
+
const at = argv.indexOf("--");
|
|
3303
|
+
if (at === -1) return { flags: argv, literal: [] };
|
|
3304
|
+
return { flags: argv.slice(0, at), literal: argv.slice(at + 1) };
|
|
2675
3305
|
}
|
|
2676
3306
|
function takeBundle(argv) {
|
|
2677
3307
|
const at = argv.indexOf("--bundle");
|
|
@@ -2713,6 +3343,9 @@ function usage() {
|
|
|
2713
3343
|
),
|
|
2714
3344
|
"",
|
|
2715
3345
|
` --bundle PATH defaults to ./${KB_DIR}`,
|
|
3346
|
+
" --json the machine shape, where a command prints a table",
|
|
3347
|
+
" -- everything after it is text, not flags",
|
|
3348
|
+
" --version the installed package version",
|
|
2716
3349
|
" STRAUSS_KB_ACTOR names the writer in the log",
|
|
2717
3350
|
""
|
|
2718
3351
|
].join("\n");
|
|
@@ -2724,6 +3357,12 @@ function usage() {
|
|
|
2724
3357
|
CONTEXT_END,
|
|
2725
3358
|
CONTEXT_PROFILES,
|
|
2726
3359
|
DECISION_TYPE,
|
|
3360
|
+
DEFAULT_AGING_DAYS,
|
|
3361
|
+
DEFAULT_EXPIRING_DAYS,
|
|
3362
|
+
DEFAULT_LOAD_BUDGET,
|
|
3363
|
+
DEFAULT_PACK_HOPS,
|
|
3364
|
+
DEFAULT_PACK_MAX_NODES,
|
|
3365
|
+
DEFAULT_UNVERIFIED_DAYS,
|
|
2727
3366
|
ErrorTypes,
|
|
2728
3367
|
Fault,
|
|
2729
3368
|
INDEX_FILE,
|
|
@@ -2732,12 +3371,15 @@ function usage() {
|
|
|
2732
3371
|
KB_CONCEPT_ID_PATTERN,
|
|
2733
3372
|
KB_CONFIDENCES,
|
|
2734
3373
|
KB_DIR,
|
|
3374
|
+
KB_DOCTOR_CHECKS,
|
|
3375
|
+
KB_EDGE_KINDS,
|
|
2735
3376
|
KB_MATERIALITIES,
|
|
2736
3377
|
KB_RECORD_STATUSES,
|
|
2737
3378
|
KB_RECORD_TYPES,
|
|
2738
3379
|
KB_SLUG_PATTERN,
|
|
2739
3380
|
KbBaseFrozenError,
|
|
2740
3381
|
KbInvalidConceptIdError,
|
|
3382
|
+
KbPackBudgetExceededError,
|
|
2741
3383
|
KbPinsMalformedError,
|
|
2742
3384
|
KbRecordAlreadyExistsError,
|
|
2743
3385
|
KbRecordNotFoundError,
|
|
@@ -2762,6 +3404,8 @@ function usage() {
|
|
|
2762
3404
|
contextProfileBudgets,
|
|
2763
3405
|
createKbMcpServer,
|
|
2764
3406
|
decisionInputSchema,
|
|
3407
|
+
doctor,
|
|
3408
|
+
edgeNeighbours,
|
|
2765
3409
|
indexIsStale,
|
|
2766
3410
|
isKbRecordType,
|
|
2767
3411
|
isNoDecisionRecord,
|
|
@@ -2777,6 +3421,8 @@ function usage() {
|
|
|
2777
3421
|
loadQmd,
|
|
2778
3422
|
matchToDiff,
|
|
2779
3423
|
mergedContextBudgets,
|
|
3424
|
+
neighbours,
|
|
3425
|
+
pack,
|
|
2780
3426
|
parseLog,
|
|
2781
3427
|
parseMarkdownWithFrontmatter,
|
|
2782
3428
|
pinBase,
|