@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.cjs
CHANGED
|
@@ -213,7 +213,7 @@ var composeInputSchema = import_zod2.z.object({
|
|
|
213
213
|
/** Concept ids this record relates to; rendered as body links. */
|
|
214
214
|
relatedConceptIds: import_zod2.z.array(kbConceptIdSchema).optional(),
|
|
215
215
|
/** Concept ids this record replaces. The store settles the backlinks. */
|
|
216
|
-
supersedes: import_zod2.z.array(kbConceptIdSchema).optional(),
|
|
216
|
+
supersedes: import_zod2.z.array(kbConceptIdSchema).max(32).optional(),
|
|
217
217
|
materiality: import_zod2.z.enum(KB_MATERIALITIES).optional(),
|
|
218
218
|
confidence: import_zod2.z.enum(KB_CONFIDENCES).optional(),
|
|
219
219
|
owner: import_zod2.z.string().min(1).optional()
|
|
@@ -1073,25 +1073,32 @@ var import_zod9 = require("zod");
|
|
|
1073
1073
|
var loadCommand = define({
|
|
1074
1074
|
name: "load",
|
|
1075
1075
|
tool: "kb_load",
|
|
1076
|
-
usage: "load [type] [--budget N]",
|
|
1077
|
-
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.",
|
|
1076
|
+
usage: "load [type] [--budget N | --all]",
|
|
1077
|
+
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.",
|
|
1078
1078
|
input: import_zod9.z.object({
|
|
1079
1079
|
bundlePath,
|
|
1080
1080
|
type: import_zod9.z.enum(KB_RECORD_TYPES).optional(),
|
|
1081
|
-
budgetTokens: import_zod9.z.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000.")
|
|
1081
|
+
budgetTokens: import_zod9.z.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
|
|
1082
|
+
all: import_zod9.z.boolean().optional().describe(
|
|
1083
|
+
"Load the entire base regardless of size. The deliberate-operator escape hatch; mutually exclusive with budgetTokens."
|
|
1084
|
+
)
|
|
1085
|
+
}).refine((value) => !(value.all && value.budgetTokens !== void 0), {
|
|
1086
|
+
message: "all and budgetTokens are mutually exclusive: pass a ceiling or none, not both."
|
|
1082
1087
|
}),
|
|
1083
1088
|
fromArgv: (argv, path) => {
|
|
1084
1089
|
const budget = argvFlag(argv, "--budget");
|
|
1085
1090
|
return {
|
|
1086
1091
|
bundlePath: path,
|
|
1087
|
-
...argv[1] && argv[1]
|
|
1088
|
-
...budget ? { budgetTokens: Number(budget) } : {}
|
|
1092
|
+
...argv[1] && !argv[1].startsWith("--") ? { type: argv[1] } : {},
|
|
1093
|
+
...budget ? { budgetTokens: Number(budget) } : {},
|
|
1094
|
+
...argv.includes("--all") ? { all: true } : {}
|
|
1089
1095
|
};
|
|
1090
1096
|
},
|
|
1091
|
-
run: async ({ store }, { bundlePath: path, type, budgetTokens }) => {
|
|
1097
|
+
run: async ({ store }, { bundlePath: path, type, budgetTokens, all }) => {
|
|
1092
1098
|
const result = await store.load(path, {
|
|
1093
1099
|
...type ? { type } : {},
|
|
1094
|
-
...budgetTokens ? { budgetTokens } : {}
|
|
1100
|
+
...budgetTokens ? { budgetTokens } : {},
|
|
1101
|
+
...all ? { all } : {}
|
|
1095
1102
|
});
|
|
1096
1103
|
if (!result.loaded) return result;
|
|
1097
1104
|
return {
|
|
@@ -1601,7 +1608,11 @@ var writeCommand = define({
|
|
|
1601
1608
|
composeRecord(type, input, actor, now()),
|
|
1602
1609
|
actor
|
|
1603
1610
|
);
|
|
1604
|
-
return {
|
|
1611
|
+
return {
|
|
1612
|
+
conceptId: record.conceptId,
|
|
1613
|
+
action: record.action,
|
|
1614
|
+
supersededIds: record.supersededIds
|
|
1615
|
+
};
|
|
1605
1616
|
}
|
|
1606
1617
|
});
|
|
1607
1618
|
|
|
@@ -1631,7 +1642,11 @@ var writeDecisionCommand = define({
|
|
|
1631
1642
|
composeDecisionRecord(input, actor, now()),
|
|
1632
1643
|
actor
|
|
1633
1644
|
);
|
|
1634
|
-
return {
|
|
1645
|
+
return {
|
|
1646
|
+
conceptId: record.conceptId,
|
|
1647
|
+
action: record.action,
|
|
1648
|
+
supersededIds: record.supersededIds
|
|
1649
|
+
};
|
|
1635
1650
|
}
|
|
1636
1651
|
});
|
|
1637
1652
|
|
|
@@ -1722,7 +1737,7 @@ var KbRecordAlreadyExistsError = class extends BaseError {
|
|
|
1722
1737
|
fault: "User" /* User */,
|
|
1723
1738
|
retriable: false,
|
|
1724
1739
|
reportToUser: true,
|
|
1725
|
-
details: { conceptId: conceptId2 }
|
|
1740
|
+
details: { conceptId: conceptId2, action: "refused" }
|
|
1726
1741
|
});
|
|
1727
1742
|
this.conceptId = conceptId2;
|
|
1728
1743
|
}
|
|
@@ -1902,13 +1917,27 @@ var KbStore = class {
|
|
|
1902
1917
|
conceptId: conceptId2,
|
|
1903
1918
|
by: actor
|
|
1904
1919
|
});
|
|
1920
|
+
const targets = new Set(frontmatter.strauss_supersedes ?? []);
|
|
1921
|
+
targets.delete(conceptId2);
|
|
1922
|
+
const supersededIds = [];
|
|
1923
|
+
for (const old of targets) {
|
|
1924
|
+
if (await this.markSupersededRetrying(bundlePath2, old, conceptId2, actor)) {
|
|
1925
|
+
supersededIds.push(old);
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1905
1928
|
this.logger.info?.({
|
|
1906
1929
|
operation: "kb.write",
|
|
1907
1930
|
bundlePath: root,
|
|
1908
1931
|
conceptId: conceptId2,
|
|
1909
1932
|
anchors: frontmatter.strauss_anchors?.length ?? 0
|
|
1910
1933
|
});
|
|
1911
|
-
return {
|
|
1934
|
+
return {
|
|
1935
|
+
conceptId: conceptId2,
|
|
1936
|
+
frontmatter,
|
|
1937
|
+
body: input.body,
|
|
1938
|
+
action: supersededIds.length ? "superseded-prior" : "created",
|
|
1939
|
+
supersededIds
|
|
1940
|
+
};
|
|
1912
1941
|
}
|
|
1913
1942
|
/** One record by concept id, or null when it does not exist. */
|
|
1914
1943
|
async read(bundlePath2, conceptId2) {
|
|
@@ -1972,15 +2001,11 @@ var KbStore = class {
|
|
|
1972
2001
|
async supersede(bundlePath2, conceptId2, replacementId, actor = "unknown") {
|
|
1973
2002
|
const replacement = await this.read(bundlePath2, replacementId);
|
|
1974
2003
|
if (!replacement) throw new KbRecordNotFoundError(replacementId);
|
|
1975
|
-
const superseded = await this.
|
|
2004
|
+
const superseded = await this.markSuperseded(
|
|
1976
2005
|
bundlePath2,
|
|
1977
2006
|
conceptId2,
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
strauss_status: "superseded",
|
|
1981
|
-
strauss_superseded_by: replacementId
|
|
1982
|
-
}),
|
|
1983
|
-
{ operation: "supersede", by: actor, target: replacementId }
|
|
2007
|
+
replacementId,
|
|
2008
|
+
actor
|
|
1984
2009
|
);
|
|
1985
2010
|
await this.mutate(
|
|
1986
2011
|
bundlePath2,
|
|
@@ -2069,6 +2094,10 @@ ${answer}
|
|
|
2069
2094
|
* Refuses rather than truncates when the base is too large. A truncated base
|
|
2070
2095
|
* is indistinguishable from a complete one, so a caller would answer "that
|
|
2071
2096
|
* was never decided" from a slice it did not know was a slice.
|
|
2097
|
+
*
|
|
2098
|
+
* That refusal is the default guardrail. `all` bypasses it outright and
|
|
2099
|
+
* always hands back the whole bundle: an explicit, never-accidental escape
|
|
2100
|
+
* hatch for an operator who has the budget to spend, not a wider default.
|
|
2072
2101
|
*/
|
|
2073
2102
|
async load(bundlePath2, options = {}) {
|
|
2074
2103
|
const budgetTokens = options.budgetTokens ?? DEFAULT_LOAD_BUDGET;
|
|
@@ -2078,7 +2107,7 @@ ${answer}
|
|
|
2078
2107
|
const records = adjudicated.filter((hit) => hit.standing !== "superseded");
|
|
2079
2108
|
const superseded = adjudicated.filter((hit) => hit.standing === "superseded").map(stub);
|
|
2080
2109
|
const approxTokens2 = records.reduce((total, hit) => total + estimateTokens(hit.record), 0) + superseded.reduce((total, entry) => total + estimateStubTokens(entry), 0);
|
|
2081
|
-
if (approxTokens2 > budgetTokens) {
|
|
2110
|
+
if (!options.all && approxTokens2 > budgetTokens) {
|
|
2082
2111
|
return {
|
|
2083
2112
|
loaded: false,
|
|
2084
2113
|
recordCount: wanted.length,
|
|
@@ -2089,8 +2118,8 @@ ${answer}
|
|
|
2089
2118
|
return {
|
|
2090
2119
|
loaded: true,
|
|
2091
2120
|
recordCount: wanted.length,
|
|
2092
|
-
|
|
2093
|
-
budgetTokens,
|
|
2121
|
+
tokensLoaded: approxTokens2,
|
|
2122
|
+
budgetTokens: options.all ? null : budgetTokens,
|
|
2094
2123
|
records,
|
|
2095
2124
|
superseded
|
|
2096
2125
|
};
|
|
@@ -2144,6 +2173,42 @@ ${answer}
|
|
|
2144
2173
|
}
|
|
2145
2174
|
return result;
|
|
2146
2175
|
}
|
|
2176
|
+
/**
|
|
2177
|
+
* `markSuperseded`, tolerant of the two ways it legitimately doesn't land:
|
|
2178
|
+
* a missing target (a broken link, legal per compose.ts) or a CAS conflict
|
|
2179
|
+
* from a concurrent writer touching the same target. A conflict is retried
|
|
2180
|
+
* a bounded number of times — each attempt re-reads the target fresh — and
|
|
2181
|
+
* on the last, `false` reports "not marked" rather than throwing: the
|
|
2182
|
+
* caller's own record is already published, so failing here would leave
|
|
2183
|
+
* that publish unreported instead of undone. kb_validate's existing
|
|
2184
|
+
* "not marked superseded" check is what surfaces the residue.
|
|
2185
|
+
*/
|
|
2186
|
+
async markSupersededRetrying(bundlePath2, conceptId2, replacementId, actor, retries = 3) {
|
|
2187
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
2188
|
+
try {
|
|
2189
|
+
await this.markSuperseded(bundlePath2, conceptId2, replacementId, actor);
|
|
2190
|
+
return true;
|
|
2191
|
+
} catch (error) {
|
|
2192
|
+
if (error instanceof KbRecordNotFoundError) return false;
|
|
2193
|
+
if (!(error instanceof KbWriteConflictError)) throw error;
|
|
2194
|
+
if (attempt === retries) return false;
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
return false;
|
|
2198
|
+
}
|
|
2199
|
+
/** The one-directional half of `supersede`: marks `conceptId` superseded. */
|
|
2200
|
+
async markSuperseded(bundlePath2, conceptId2, replacementId, actor) {
|
|
2201
|
+
return this.mutate(
|
|
2202
|
+
bundlePath2,
|
|
2203
|
+
conceptId2,
|
|
2204
|
+
(frontmatter) => ({
|
|
2205
|
+
...frontmatter,
|
|
2206
|
+
strauss_status: "superseded",
|
|
2207
|
+
strauss_superseded_by: replacementId
|
|
2208
|
+
}),
|
|
2209
|
+
{ operation: "supersede", by: actor, target: replacementId }
|
|
2210
|
+
);
|
|
2211
|
+
}
|
|
2147
2212
|
async mutate(bundlePath2, conceptId2, change, entry, changeBody = (body) => body) {
|
|
2148
2213
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
2149
2214
|
const before = await (0, import_promises4.readFile)(target, "utf8").catch(() => null);
|