@saasontools/strauss-kb 0.1.3 → 0.1.5
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 +37 -2
- package/dist/{chunk-HYNAEAPM.js → chunk-FZIMFPGR.js} +88 -23
- package/dist/chunk-FZIMFPGR.js.map +1 -0
- package/dist/{chunk-QLTB77W4.js → chunk-KQMGKSPZ.js} +2 -2
- package/dist/{chunk-FSI4Q2FD.js → chunk-VOJ6D6OX.js} +2 -2
- package/dist/cli-main.cjs +87 -22
- package/dist/cli-main.cjs.map +1 -1
- package/dist/cli-main.js +2 -2
- package/dist/index.cjs +87 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +28 -3
- package/dist/index.d.ts +28 -3
- package/dist/index.js +3 -3
- package/dist/mcp-main.cjs +87 -22
- package/dist/mcp-main.cjs.map +1 -1
- package/dist/mcp-main.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-HYNAEAPM.js.map +0 -1
- /package/dist/{chunk-QLTB77W4.js.map → chunk-KQMGKSPZ.js.map} +0 -0
- /package/dist/{chunk-FSI4Q2FD.js.map → chunk-VOJ6D6OX.js.map} +0 -0
package/dist/cli-main.js
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -270,7 +270,7 @@ var KbRecordAlreadyExistsError = class extends BaseError {
|
|
|
270
270
|
fault: "User" /* User */,
|
|
271
271
|
retriable: false,
|
|
272
272
|
reportToUser: true,
|
|
273
|
-
details: { conceptId: conceptId2 }
|
|
273
|
+
details: { conceptId: conceptId2, action: "refused" }
|
|
274
274
|
});
|
|
275
275
|
this.conceptId = conceptId2;
|
|
276
276
|
}
|
|
@@ -675,13 +675,27 @@ var KbStore = class {
|
|
|
675
675
|
conceptId: conceptId2,
|
|
676
676
|
by: actor
|
|
677
677
|
});
|
|
678
|
+
const targets = new Set(frontmatter.strauss_supersedes ?? []);
|
|
679
|
+
targets.delete(conceptId2);
|
|
680
|
+
const supersededIds = [];
|
|
681
|
+
for (const old of targets) {
|
|
682
|
+
if (await this.markSupersededRetrying(bundlePath2, old, conceptId2, actor)) {
|
|
683
|
+
supersededIds.push(old);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
678
686
|
this.logger.info?.({
|
|
679
687
|
operation: "kb.write",
|
|
680
688
|
bundlePath: root,
|
|
681
689
|
conceptId: conceptId2,
|
|
682
690
|
anchors: frontmatter.strauss_anchors?.length ?? 0
|
|
683
691
|
});
|
|
684
|
-
return {
|
|
692
|
+
return {
|
|
693
|
+
conceptId: conceptId2,
|
|
694
|
+
frontmatter,
|
|
695
|
+
body: input.body,
|
|
696
|
+
action: supersededIds.length ? "superseded-prior" : "created",
|
|
697
|
+
supersededIds
|
|
698
|
+
};
|
|
685
699
|
}
|
|
686
700
|
/** One record by concept id, or null when it does not exist. */
|
|
687
701
|
async read(bundlePath2, conceptId2) {
|
|
@@ -745,15 +759,11 @@ var KbStore = class {
|
|
|
745
759
|
async supersede(bundlePath2, conceptId2, replacementId, actor = "unknown") {
|
|
746
760
|
const replacement = await this.read(bundlePath2, replacementId);
|
|
747
761
|
if (!replacement) throw new KbRecordNotFoundError(replacementId);
|
|
748
|
-
const superseded = await this.
|
|
762
|
+
const superseded = await this.markSuperseded(
|
|
749
763
|
bundlePath2,
|
|
750
764
|
conceptId2,
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
strauss_status: "superseded",
|
|
754
|
-
strauss_superseded_by: replacementId
|
|
755
|
-
}),
|
|
756
|
-
{ operation: "supersede", by: actor, target: replacementId }
|
|
765
|
+
replacementId,
|
|
766
|
+
actor
|
|
757
767
|
);
|
|
758
768
|
await this.mutate(
|
|
759
769
|
bundlePath2,
|
|
@@ -842,6 +852,10 @@ ${answer}
|
|
|
842
852
|
* Refuses rather than truncates when the base is too large. A truncated base
|
|
843
853
|
* is indistinguishable from a complete one, so a caller would answer "that
|
|
844
854
|
* was never decided" from a slice it did not know was a slice.
|
|
855
|
+
*
|
|
856
|
+
* That refusal is the default guardrail. `all` bypasses it outright and
|
|
857
|
+
* always hands back the whole bundle: an explicit, never-accidental escape
|
|
858
|
+
* hatch for an operator who has the budget to spend, not a wider default.
|
|
845
859
|
*/
|
|
846
860
|
async load(bundlePath2, options = {}) {
|
|
847
861
|
const budgetTokens = options.budgetTokens ?? DEFAULT_LOAD_BUDGET;
|
|
@@ -851,7 +865,7 @@ ${answer}
|
|
|
851
865
|
const records = adjudicated.filter((hit) => hit.standing !== "superseded");
|
|
852
866
|
const superseded = adjudicated.filter((hit) => hit.standing === "superseded").map(stub);
|
|
853
867
|
const approxTokens2 = records.reduce((total, hit) => total + estimateTokens(hit.record), 0) + superseded.reduce((total, entry) => total + estimateStubTokens(entry), 0);
|
|
854
|
-
if (approxTokens2 > budgetTokens) {
|
|
868
|
+
if (!options.all && approxTokens2 > budgetTokens) {
|
|
855
869
|
return {
|
|
856
870
|
loaded: false,
|
|
857
871
|
recordCount: wanted.length,
|
|
@@ -862,8 +876,8 @@ ${answer}
|
|
|
862
876
|
return {
|
|
863
877
|
loaded: true,
|
|
864
878
|
recordCount: wanted.length,
|
|
865
|
-
|
|
866
|
-
budgetTokens,
|
|
879
|
+
tokensLoaded: approxTokens2,
|
|
880
|
+
budgetTokens: options.all ? null : budgetTokens,
|
|
867
881
|
records,
|
|
868
882
|
superseded
|
|
869
883
|
};
|
|
@@ -917,6 +931,42 @@ ${answer}
|
|
|
917
931
|
}
|
|
918
932
|
return result;
|
|
919
933
|
}
|
|
934
|
+
/**
|
|
935
|
+
* `markSuperseded`, tolerant of the two ways it legitimately doesn't land:
|
|
936
|
+
* a missing target (a broken link, legal per compose.ts) or a CAS conflict
|
|
937
|
+
* from a concurrent writer touching the same target. A conflict is retried
|
|
938
|
+
* a bounded number of times — each attempt re-reads the target fresh — and
|
|
939
|
+
* on the last, `false` reports "not marked" rather than throwing: the
|
|
940
|
+
* caller's own record is already published, so failing here would leave
|
|
941
|
+
* that publish unreported instead of undone. kb_validate's existing
|
|
942
|
+
* "not marked superseded" check is what surfaces the residue.
|
|
943
|
+
*/
|
|
944
|
+
async markSupersededRetrying(bundlePath2, conceptId2, replacementId, actor, retries = 3) {
|
|
945
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
946
|
+
try {
|
|
947
|
+
await this.markSuperseded(bundlePath2, conceptId2, replacementId, actor);
|
|
948
|
+
return true;
|
|
949
|
+
} catch (error) {
|
|
950
|
+
if (error instanceof KbRecordNotFoundError) return false;
|
|
951
|
+
if (!(error instanceof KbWriteConflictError)) throw error;
|
|
952
|
+
if (attempt === retries) return false;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
return false;
|
|
956
|
+
}
|
|
957
|
+
/** The one-directional half of `supersede`: marks `conceptId` superseded. */
|
|
958
|
+
async markSuperseded(bundlePath2, conceptId2, replacementId, actor) {
|
|
959
|
+
return this.mutate(
|
|
960
|
+
bundlePath2,
|
|
961
|
+
conceptId2,
|
|
962
|
+
(frontmatter) => ({
|
|
963
|
+
...frontmatter,
|
|
964
|
+
strauss_status: "superseded",
|
|
965
|
+
strauss_superseded_by: replacementId
|
|
966
|
+
}),
|
|
967
|
+
{ operation: "supersede", by: actor, target: replacementId }
|
|
968
|
+
);
|
|
969
|
+
}
|
|
920
970
|
async mutate(bundlePath2, conceptId2, change, entry, changeBody = (body) => body) {
|
|
921
971
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
922
972
|
const before = await (0, import_promises2.readFile)(target, "utf8").catch(() => null);
|
|
@@ -1133,7 +1183,7 @@ var composeInputSchema = import_zod3.z.object({
|
|
|
1133
1183
|
/** Concept ids this record relates to; rendered as body links. */
|
|
1134
1184
|
relatedConceptIds: import_zod3.z.array(kbConceptIdSchema).optional(),
|
|
1135
1185
|
/** Concept ids this record replaces. The store settles the backlinks. */
|
|
1136
|
-
supersedes: import_zod3.z.array(kbConceptIdSchema).optional(),
|
|
1186
|
+
supersedes: import_zod3.z.array(kbConceptIdSchema).max(32).optional(),
|
|
1137
1187
|
materiality: import_zod3.z.enum(KB_MATERIALITIES).optional(),
|
|
1138
1188
|
confidence: import_zod3.z.enum(KB_CONFIDENCES).optional(),
|
|
1139
1189
|
owner: import_zod3.z.string().min(1).optional()
|
|
@@ -2012,25 +2062,32 @@ var import_zod11 = require("zod");
|
|
|
2012
2062
|
var loadCommand = define({
|
|
2013
2063
|
name: "load",
|
|
2014
2064
|
tool: "kb_load",
|
|
2015
|
-
usage: "load [type] [--budget N]",
|
|
2016
|
-
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.",
|
|
2065
|
+
usage: "load [type] [--budget N | --all]",
|
|
2066
|
+
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.",
|
|
2017
2067
|
input: import_zod11.z.object({
|
|
2018
2068
|
bundlePath,
|
|
2019
2069
|
type: import_zod11.z.enum(KB_RECORD_TYPES).optional(),
|
|
2020
|
-
budgetTokens: import_zod11.z.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000.")
|
|
2070
|
+
budgetTokens: import_zod11.z.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
|
|
2071
|
+
all: import_zod11.z.boolean().optional().describe(
|
|
2072
|
+
"Load the entire base regardless of size. The deliberate-operator escape hatch; mutually exclusive with budgetTokens."
|
|
2073
|
+
)
|
|
2074
|
+
}).refine((value) => !(value.all && value.budgetTokens !== void 0), {
|
|
2075
|
+
message: "all and budgetTokens are mutually exclusive: pass a ceiling or none, not both."
|
|
2021
2076
|
}),
|
|
2022
2077
|
fromArgv: (argv, path) => {
|
|
2023
2078
|
const budget = argvFlag(argv, "--budget");
|
|
2024
2079
|
return {
|
|
2025
2080
|
bundlePath: path,
|
|
2026
|
-
...argv[1] && argv[1]
|
|
2027
|
-
...budget ? { budgetTokens: Number(budget) } : {}
|
|
2081
|
+
...argv[1] && !argv[1].startsWith("--") ? { type: argv[1] } : {},
|
|
2082
|
+
...budget ? { budgetTokens: Number(budget) } : {},
|
|
2083
|
+
...argv.includes("--all") ? { all: true } : {}
|
|
2028
2084
|
};
|
|
2029
2085
|
},
|
|
2030
|
-
run: async ({ store }, { bundlePath: path, type, budgetTokens }) => {
|
|
2086
|
+
run: async ({ store }, { bundlePath: path, type, budgetTokens, all }) => {
|
|
2031
2087
|
const result = await store.load(path, {
|
|
2032
2088
|
...type ? { type } : {},
|
|
2033
|
-
...budgetTokens ? { budgetTokens } : {}
|
|
2089
|
+
...budgetTokens ? { budgetTokens } : {},
|
|
2090
|
+
...all ? { all } : {}
|
|
2034
2091
|
});
|
|
2035
2092
|
if (!result.loaded) return result;
|
|
2036
2093
|
return {
|
|
@@ -2375,7 +2432,11 @@ var writeCommand = define({
|
|
|
2375
2432
|
composeRecord(type, input, actor, now()),
|
|
2376
2433
|
actor
|
|
2377
2434
|
);
|
|
2378
|
-
return {
|
|
2435
|
+
return {
|
|
2436
|
+
conceptId: record.conceptId,
|
|
2437
|
+
action: record.action,
|
|
2438
|
+
supersededIds: record.supersededIds
|
|
2439
|
+
};
|
|
2379
2440
|
}
|
|
2380
2441
|
});
|
|
2381
2442
|
|
|
@@ -2405,7 +2466,11 @@ var writeDecisionCommand = define({
|
|
|
2405
2466
|
composeDecisionRecord(input, actor, now()),
|
|
2406
2467
|
actor
|
|
2407
2468
|
);
|
|
2408
|
-
return {
|
|
2469
|
+
return {
|
|
2470
|
+
conceptId: record.conceptId,
|
|
2471
|
+
action: record.action,
|
|
2472
|
+
supersededIds: record.supersededIds
|
|
2473
|
+
};
|
|
2409
2474
|
}
|
|
2410
2475
|
});
|
|
2411
2476
|
|