@saasontools/strauss-kb 0.1.11 → 0.1.13
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/ARCHITECTURE.md +15 -0
- package/README.md +49 -4
- package/dist/{chunk-CWWXMD35.js → chunk-WZODZNR6.js} +2 -2
- package/dist/{chunk-OVRQCQ6P.js → chunk-XALWG3EZ.js} +486 -92
- package/dist/chunk-XALWG3EZ.js.map +1 -0
- package/dist/{chunk-I3WW4F6X.js → chunk-ZICKDZGY.js} +2 -2
- package/dist/cli-main.cjs +484 -102
- package/dist/cli-main.cjs.map +1 -1
- package/dist/cli-main.js +2 -2
- package/dist/index.cjs +562 -157
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +292 -13
- package/dist/index.d.ts +292 -13
- package/dist/index.js +25 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp-main.cjs +484 -102
- package/dist/mcp-main.cjs.map +1 -1
- package/dist/mcp-main.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-OVRQCQ6P.js.map +0 -1
- /package/dist/{chunk-CWWXMD35.js.map → chunk-WZODZNR6.js.map} +0 -0
- /package/dist/{chunk-I3WW4F6X.js.map → chunk-ZICKDZGY.js.map} +0 -0
|
@@ -44,6 +44,10 @@ var kbAnchorSchema = z.object({
|
|
|
44
44
|
/** Line count of the text the hash was taken over. */
|
|
45
45
|
lines: z.number().int().positive().optional()
|
|
46
46
|
}).strict();
|
|
47
|
+
var kbLinkSchema = z.object({
|
|
48
|
+
target: z.string().min(1),
|
|
49
|
+
rel: z.string().min(1)
|
|
50
|
+
}).passthrough();
|
|
47
51
|
var KB_RECORD_TYPES = [
|
|
48
52
|
"fact",
|
|
49
53
|
"requirement",
|
|
@@ -95,6 +99,10 @@ var kbRecordFrontmatterSchema = z.object({
|
|
|
95
99
|
// strauss extensions — see the module comment.
|
|
96
100
|
strauss_anchors: z.array(kbAnchorSchema).optional(),
|
|
97
101
|
strauss_verify: z.array(z.string().min(1)).optional(),
|
|
102
|
+
// Typed causal edges, source → target, living on the source. `A depends_on
|
|
103
|
+
// B` means A needs B, so `kb_impact` walks these inbound: what breaks if B
|
|
104
|
+
// changes is whatever declared a dependence on it.
|
|
105
|
+
strauss_links: z.array(kbLinkSchema).optional(),
|
|
98
106
|
// Total after parsing, tolerant before it. Our producers must supply a
|
|
99
107
|
// status — an absent one would leave every reader inventing its own default
|
|
100
108
|
// — but OKF calls a concept carrying only `type` fully conformant, so
|
|
@@ -179,9 +187,71 @@ var RECORD_TYPES = {
|
|
|
179
187
|
function isKbRecordType(value) {
|
|
180
188
|
return Object.prototype.hasOwnProperty.call(RECORD_TYPES, value);
|
|
181
189
|
}
|
|
190
|
+
var KB_LINK_RELS = [
|
|
191
|
+
"depends_on",
|
|
192
|
+
"constrains",
|
|
193
|
+
"informs",
|
|
194
|
+
"blocks",
|
|
195
|
+
"invalidates",
|
|
196
|
+
"verified_by",
|
|
197
|
+
"satisfies",
|
|
198
|
+
"related_to"
|
|
199
|
+
];
|
|
200
|
+
var LINK_RELS = {
|
|
201
|
+
depends_on: {
|
|
202
|
+
purpose: "The source needs the target to hold; the source breaks if the target changes",
|
|
203
|
+
phrase: "Depends on",
|
|
204
|
+
dependant: "source"
|
|
205
|
+
},
|
|
206
|
+
constrains: {
|
|
207
|
+
purpose: "The source bounds what the target may do; the target breaks if the constraint changes",
|
|
208
|
+
phrase: "Constrains",
|
|
209
|
+
dependant: "target"
|
|
210
|
+
},
|
|
211
|
+
informs: {
|
|
212
|
+
purpose: "The source shaped the target without binding it; the target is what needs revisiting",
|
|
213
|
+
phrase: "Informs",
|
|
214
|
+
dependant: "target"
|
|
215
|
+
},
|
|
216
|
+
blocks: {
|
|
217
|
+
purpose: "The target cannot proceed until the source is settled; the target is what waits",
|
|
218
|
+
phrase: "Blocks",
|
|
219
|
+
dependant: "target"
|
|
220
|
+
},
|
|
221
|
+
invalidates: {
|
|
222
|
+
purpose: "The source makes the target no longer hold; the target is what stops holding",
|
|
223
|
+
phrase: "Invalidates",
|
|
224
|
+
dependant: "target"
|
|
225
|
+
},
|
|
226
|
+
verified_by: {
|
|
227
|
+
purpose: "The target is the check that confirms the source; the source's confirmation moves with it",
|
|
228
|
+
phrase: "Verified by",
|
|
229
|
+
dependant: "source"
|
|
230
|
+
},
|
|
231
|
+
satisfies: {
|
|
232
|
+
purpose: "The source discharges the target's requirement; the source must change if the requirement does",
|
|
233
|
+
phrase: "Satisfies",
|
|
234
|
+
dependant: "source"
|
|
235
|
+
},
|
|
236
|
+
related_to: {
|
|
237
|
+
purpose: "A pointer worth following, with no claim of dependence",
|
|
238
|
+
phrase: "Relates to",
|
|
239
|
+
dependant: null
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
var KB_CAUSAL_LINK_RELS = KB_LINK_RELS.filter(
|
|
243
|
+
(rel) => LINK_RELS[rel].dependant !== null
|
|
244
|
+
);
|
|
245
|
+
function isKbLinkRel(value) {
|
|
246
|
+
return Object.prototype.hasOwnProperty.call(LINK_RELS, value);
|
|
247
|
+
}
|
|
182
248
|
|
|
183
249
|
// src/compose.ts
|
|
184
250
|
import { z as z2 } from "zod";
|
|
251
|
+
var composeLinkSchema = z2.object({
|
|
252
|
+
target: kbConceptIdSchema,
|
|
253
|
+
rel: z2.enum(KB_LINK_RELS)
|
|
254
|
+
}).strict();
|
|
185
255
|
var composeInputSchema = z2.object({
|
|
186
256
|
slug: z2.string().min(1),
|
|
187
257
|
/** One line, in the reader's terms. Becomes OKF `title`. */
|
|
@@ -208,6 +278,18 @@ var composeInputSchema = z2.object({
|
|
|
208
278
|
tags: z2.array(z2.string().min(1)).optional(),
|
|
209
279
|
/** Concept ids this record relates to; rendered as body links. */
|
|
210
280
|
relatedConceptIds: z2.array(kbConceptIdSchema).optional(),
|
|
281
|
+
/**
|
|
282
|
+
* Typed causal edges, source → target: `{ target: "fact.b", rel:
|
|
283
|
+
* "depends_on" }` on record A says A needs B. Stored in frontmatter and
|
|
284
|
+
* also rendered as one prose sentence each, so the meaning survives a
|
|
285
|
+
* reader that knows only OKF. The vocabulary goes into the description from
|
|
286
|
+
* the same table the walk uses, so `kb_schema` emits it.
|
|
287
|
+
*/
|
|
288
|
+
links: z2.array(composeLinkSchema).max(64).optional().describe(
|
|
289
|
+
`Typed causal edges, source \u2192 target \u2014 a link on this record says this record <rel> the target. ${KB_LINK_RELS.map(
|
|
290
|
+
(rel) => `${rel}: ${LINK_RELS[rel].purpose}`
|
|
291
|
+
).join("; ")}.`
|
|
292
|
+
),
|
|
211
293
|
/** Concept ids this record replaces. The store settles the backlinks. */
|
|
212
294
|
supersedes: z2.array(kbConceptIdSchema).max(32).optional(),
|
|
213
295
|
materiality: z2.enum(KB_MATERIALITIES).optional(),
|
|
@@ -247,6 +329,15 @@ function composeRecord(type, input, writtenBy, writtenAt) {
|
|
|
247
329
|
if (parsed.owner) frontmatter.strauss_owner = parsed.owner;
|
|
248
330
|
if (parsed.supersedes?.length)
|
|
249
331
|
frontmatter.strauss_supersedes = parsed.supersedes;
|
|
332
|
+
const selfLink = parsed.links?.find(
|
|
333
|
+
(link2) => link2.target === `${type}.${parsed.slug}`
|
|
334
|
+
);
|
|
335
|
+
if (selfLink) {
|
|
336
|
+
throw new Error(
|
|
337
|
+
`kb: ${type}.${parsed.slug} cannot ${selfLink.rel} itself \u2014 a link must name another record`
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
if (parsed.links?.length) frontmatter.strauss_links = parsed.links;
|
|
250
341
|
const blocks = [];
|
|
251
342
|
for (const heading of spec.sections) {
|
|
252
343
|
const text = sections[heading];
|
|
@@ -258,6 +349,11 @@ ${text}`);
|
|
|
258
349
|
for (const related of parsed.relatedConceptIds ?? []) {
|
|
259
350
|
blocks.push(`Relates to [${related}](${related}.md).`);
|
|
260
351
|
}
|
|
352
|
+
for (const link2 of parsed.links ?? []) {
|
|
353
|
+
blocks.push(
|
|
354
|
+
`${LINK_RELS[link2.rel].phrase} [${link2.target}](${link2.target}.md).`
|
|
355
|
+
);
|
|
356
|
+
}
|
|
261
357
|
if (parsed.sources?.length) {
|
|
262
358
|
blocks.push(
|
|
263
359
|
parsed.sources.map((source) => `[^${source.id}]: ${source.title ?? source.resource}`).join("\n")
|
|
@@ -281,7 +377,7 @@ var decisionInputSchema = composeInputSchema.omit({ sections: true }).extend({
|
|
|
281
377
|
impact: z3.string().min(1).optional()
|
|
282
378
|
}).strict();
|
|
283
379
|
function composeDecisionRecord(input, writtenBy, writtenAt) {
|
|
284
|
-
const { alternative, impact, ...rest } = input;
|
|
380
|
+
const { alternative, impact: impact2, ...rest } = input;
|
|
285
381
|
return composeRecord(
|
|
286
382
|
DECISION_TYPE,
|
|
287
383
|
{
|
|
@@ -290,7 +386,7 @@ function composeDecisionRecord(input, writtenBy, writtenAt) {
|
|
|
290
386
|
Decision: input.title,
|
|
291
387
|
Rationale: input.why,
|
|
292
388
|
...alternative ? { Rejected: alternative } : {},
|
|
293
|
-
...
|
|
389
|
+
...impact2 ? { Impact: impact2 } : {}
|
|
294
390
|
}
|
|
295
391
|
},
|
|
296
392
|
writtenBy,
|
|
@@ -785,6 +881,7 @@ var ErrorTypes = /* @__PURE__ */ ((ErrorTypes2) => {
|
|
|
785
881
|
ErrorTypes2["KbPackBudgetExceeded"] = "KbPackBudgetExceeded";
|
|
786
882
|
ErrorTypes2["KbRecordNotFound"] = "KbRecordNotFound";
|
|
787
883
|
ErrorTypes2["KbSelfVerification"] = "KbSelfVerification";
|
|
884
|
+
ErrorTypes2["KbUnknownLinkRel"] = "KbUnknownLinkRel";
|
|
788
885
|
ErrorTypes2["KbWriteConflict"] = "KbWriteConflict";
|
|
789
886
|
return ErrorTypes2;
|
|
790
887
|
})(ErrorTypes || {});
|
|
@@ -893,6 +990,23 @@ var KbPackBudgetExceededError = class extends BaseError {
|
|
|
893
990
|
budgetTokens;
|
|
894
991
|
excluded;
|
|
895
992
|
};
|
|
993
|
+
var KbUnknownLinkRelError = class extends BaseError {
|
|
994
|
+
constructor(rel, expected) {
|
|
995
|
+
super({
|
|
996
|
+
message: `kb: ${rel} is not a rel a walk can follow \u2014 expected one of ${expected.join(", ")}`,
|
|
997
|
+
errorType: "KbUnknownLinkRel" /* KbUnknownLinkRel */,
|
|
998
|
+
code: 400,
|
|
999
|
+
fault: "User" /* User */,
|
|
1000
|
+
retriable: false,
|
|
1001
|
+
reportToUser: true,
|
|
1002
|
+
details: { rel, expected: expected.join(", ") }
|
|
1003
|
+
});
|
|
1004
|
+
this.rel = rel;
|
|
1005
|
+
this.expected = expected;
|
|
1006
|
+
}
|
|
1007
|
+
rel;
|
|
1008
|
+
expected;
|
|
1009
|
+
};
|
|
896
1010
|
var KbMissingFlagValueError = class extends BaseError {
|
|
897
1011
|
constructor(flag) {
|
|
898
1012
|
super({
|
|
@@ -1624,6 +1738,7 @@ ${CONTEXT_END}` : null;
|
|
|
1624
1738
|
// src/kb-edges.ts
|
|
1625
1739
|
var KB_EDGE_KINDS = [
|
|
1626
1740
|
"body-link",
|
|
1741
|
+
"typed-link",
|
|
1627
1742
|
"supersession",
|
|
1628
1743
|
"anchor",
|
|
1629
1744
|
"source"
|
|
@@ -1632,10 +1747,11 @@ var BODY_LINK_TARGET = new RegExp(
|
|
|
1632
1747
|
`\\]\\((${KB_CONCEPT_ID_PATTERN.source.replace(/^\^|\$$/g, "")})\\.md\\)`,
|
|
1633
1748
|
"g"
|
|
1634
1749
|
);
|
|
1635
|
-
|
|
1750
|
+
var DEFAULT_TYPED_LINK_RELS = KB_LINK_RELS;
|
|
1751
|
+
function neighbours(from, bundle, kinds = KB_EDGE_KINDS, linkRels = DEFAULT_TYPED_LINK_RELS) {
|
|
1636
1752
|
const found = /* @__PURE__ */ new Map();
|
|
1637
1753
|
for (const kind of kinds) {
|
|
1638
|
-
for (const record of edgeNeighbours(from, bundle, kind)) {
|
|
1754
|
+
for (const record of edgeNeighbours(from, bundle, kind, linkRels)) {
|
|
1639
1755
|
const existing = found.get(record.conceptId);
|
|
1640
1756
|
if (existing) {
|
|
1641
1757
|
if (!existing.via.includes(kind)) existing.via.push(kind);
|
|
@@ -1646,7 +1762,7 @@ function neighbours(from, bundle, kinds = KB_EDGE_KINDS) {
|
|
|
1646
1762
|
}
|
|
1647
1763
|
return [...found.values()];
|
|
1648
1764
|
}
|
|
1649
|
-
function edgeNeighbours(from, bundle, kind) {
|
|
1765
|
+
function edgeNeighbours(from, bundle, kind, linkRels = DEFAULT_TYPED_LINK_RELS) {
|
|
1650
1766
|
switch (kind) {
|
|
1651
1767
|
// A link whose target is not in the bundle is legal per compose.ts —
|
|
1652
1768
|
// records are routinely written before the ones they point at exist — so
|
|
@@ -1660,6 +1776,21 @@ function edgeNeighbours(from, bundle, kind) {
|
|
|
1660
1776
|
(candidate) => candidate.conceptId !== from.conceptId && targets.has(candidate.conceptId)
|
|
1661
1777
|
);
|
|
1662
1778
|
}
|
|
1779
|
+
// Outbound only, like `body-link`, and for the same reason: this is what
|
|
1780
|
+
// the record declares about itself. A missing target is legal — the walk
|
|
1781
|
+
// skips it, and `kb_validate` is what reports it as a warning. A rel
|
|
1782
|
+
// outside `linkRels` is skipped too, which is how an unknown rel stays
|
|
1783
|
+
// untraversable everywhere rather than one walk at a time.
|
|
1784
|
+
case "typed-link": {
|
|
1785
|
+
const allowed = new Set(linkRels);
|
|
1786
|
+
const targets = new Set(
|
|
1787
|
+
(from.frontmatter.strauss_links ?? []).filter((link2) => allowed.has(link2.rel)).map((link2) => link2.target)
|
|
1788
|
+
);
|
|
1789
|
+
if (!targets.size) return [];
|
|
1790
|
+
return bundle.filter(
|
|
1791
|
+
(candidate) => candidate.conceptId !== from.conceptId && targets.has(candidate.conceptId)
|
|
1792
|
+
);
|
|
1793
|
+
}
|
|
1663
1794
|
// Both directions and both pointers: `supersede()` writes the pair, but a
|
|
1664
1795
|
// hand-edit can leave one side behind, and a walk trusting one pointer
|
|
1665
1796
|
// would miss a replacement the bundle openly declares.
|
|
@@ -1701,7 +1832,7 @@ function anchorsTouch(left, right) {
|
|
|
1701
1832
|
function validateBundle(records) {
|
|
1702
1833
|
const byId = new Map(records.map((record) => [record.conceptId, record]));
|
|
1703
1834
|
const problems = [];
|
|
1704
|
-
const report = (check, conceptId2, note) => problems.push({ check, conceptId: conceptId2, note });
|
|
1835
|
+
const report = (check, conceptId2, note, severity = "error") => problems.push({ check, conceptId: conceptId2, note, severity });
|
|
1705
1836
|
for (const record of records) {
|
|
1706
1837
|
const { conceptId: conceptId2, frontmatter: fm } = record;
|
|
1707
1838
|
if (!isKbRecordType(fm.type)) {
|
|
@@ -1725,6 +1856,36 @@ function validateBundle(records) {
|
|
|
1725
1856
|
report("supersedes", conceptId2, `${old} is not marked superseded`);
|
|
1726
1857
|
}
|
|
1727
1858
|
}
|
|
1859
|
+
for (const link2 of fm.strauss_links ?? []) {
|
|
1860
|
+
if (!isKbLinkRel(link2.rel)) {
|
|
1861
|
+
report(
|
|
1862
|
+
"link_rel",
|
|
1863
|
+
conceptId2,
|
|
1864
|
+
`unknown rel "${link2.rel}" on link to ${link2.target} \u2014 expected one of ${KB_LINK_RELS.join(", ")}`
|
|
1865
|
+
);
|
|
1866
|
+
}
|
|
1867
|
+
if (!KB_CONCEPT_ID_PATTERN.test(link2.target)) {
|
|
1868
|
+
report(
|
|
1869
|
+
"link_target",
|
|
1870
|
+
conceptId2,
|
|
1871
|
+
`target "${link2.target}" is not a valid concept id \u2014 expected <type>.<slug>, both kebab-case`
|
|
1872
|
+
);
|
|
1873
|
+
} else if (link2.target === conceptId2) {
|
|
1874
|
+
report(
|
|
1875
|
+
"link_target",
|
|
1876
|
+
conceptId2,
|
|
1877
|
+
`links to itself (${link2.rel})`,
|
|
1878
|
+
"warning"
|
|
1879
|
+
);
|
|
1880
|
+
} else if (!byId.has(link2.target)) {
|
|
1881
|
+
report(
|
|
1882
|
+
"link_target",
|
|
1883
|
+
conceptId2,
|
|
1884
|
+
`target ${link2.target} is not in the bundle`,
|
|
1885
|
+
"warning"
|
|
1886
|
+
);
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1728
1889
|
if (fm.strauss_assumption && fm.sources?.length) {
|
|
1729
1890
|
report("assumption", conceptId2, "marked an assumption but cites sources");
|
|
1730
1891
|
}
|
|
@@ -2075,7 +2236,12 @@ function kbJsonSchemas() {
|
|
|
2075
2236
|
}
|
|
2076
2237
|
|
|
2077
2238
|
// src/trace.ts
|
|
2078
|
-
var TRACE_EDGES = [
|
|
2239
|
+
var TRACE_EDGES = [
|
|
2240
|
+
"typed-link",
|
|
2241
|
+
"supersession",
|
|
2242
|
+
"anchor",
|
|
2243
|
+
"source"
|
|
2244
|
+
];
|
|
2079
2245
|
function trace(seedId, bundle, options = {}) {
|
|
2080
2246
|
const edges = options.edges?.length ? options.edges : TRACE_EDGES;
|
|
2081
2247
|
const maxDepth = options.depth ?? 3;
|
|
@@ -2090,7 +2256,12 @@ function trace(seedId, bundle, options = {}) {
|
|
|
2090
2256
|
const next = [];
|
|
2091
2257
|
for (const from of frontier) {
|
|
2092
2258
|
for (const edge of edges) {
|
|
2093
|
-
for (const record of edgeNeighbours(
|
|
2259
|
+
for (const record of edgeNeighbours(
|
|
2260
|
+
from,
|
|
2261
|
+
bundle,
|
|
2262
|
+
edge,
|
|
2263
|
+
KB_CAUSAL_LINK_RELS
|
|
2264
|
+
)) {
|
|
2094
2265
|
const existing = reached.get(record.conceptId);
|
|
2095
2266
|
if (existing) {
|
|
2096
2267
|
if (existing.depth > 0 && !existing.via.includes(edge)) {
|
|
@@ -2322,16 +2493,28 @@ var answerCommand = define({
|
|
|
2322
2493
|
}
|
|
2323
2494
|
});
|
|
2324
2495
|
|
|
2325
|
-
// src/commands/
|
|
2496
|
+
// src/commands/backlinks.ts
|
|
2326
2497
|
import { z as z10 } from "zod";
|
|
2498
|
+
var backlinksCommand = define({
|
|
2499
|
+
name: "backlinks",
|
|
2500
|
+
tool: "kb_backlinks",
|
|
2501
|
+
usage: "backlinks <concept-id>",
|
|
2502
|
+
description: "Who points at this record: every inbound typed causal link (`strauss_links`), one hop, every rel including `related_to`, each with its rel and the standing of the record that made it. Use it when you need the exact edges \u2014 reviewing or renaming a record.",
|
|
2503
|
+
input: z10.object({ bundlePath, conceptId }),
|
|
2504
|
+
fromArgv: (argv, path) => ({ bundlePath: path, conceptId: argv[1] }),
|
|
2505
|
+
run: async ({ store }, { bundlePath: path, conceptId: id }) => store.backlinks(path, id)
|
|
2506
|
+
});
|
|
2507
|
+
|
|
2508
|
+
// src/commands/catalog.ts
|
|
2509
|
+
import { z as z11 } from "zod";
|
|
2327
2510
|
var catalogCommand = define({
|
|
2328
2511
|
name: "catalog",
|
|
2329
2512
|
tool: "kb_catalog",
|
|
2330
2513
|
usage: "catalog [type]",
|
|
2331
2514
|
description: "Lists every record as one line \u2014 concept id, type, title, standing, and a stale flag \u2014 at roughly thirty tokens each. Pick this over kb_load once kb_load refuses: kb_catalog never refuses. Superseded records show only their replacement; fetch bodies with kb_load, kb_pack, kb_query, or kb_trace.",
|
|
2332
|
-
input:
|
|
2515
|
+
input: z11.object({
|
|
2333
2516
|
bundlePath,
|
|
2334
|
-
type:
|
|
2517
|
+
type: z11.enum(KB_RECORD_TYPES).optional()
|
|
2335
2518
|
}),
|
|
2336
2519
|
fromArgv: (argv, path) => ({
|
|
2337
2520
|
bundlePath: path,
|
|
@@ -2386,26 +2569,26 @@ function count(value, noun) {
|
|
|
2386
2569
|
}
|
|
2387
2570
|
|
|
2388
2571
|
// src/commands/context.ts
|
|
2389
|
-
import { z as
|
|
2572
|
+
import { z as z12 } from "zod";
|
|
2390
2573
|
var contextCommand = define({
|
|
2391
2574
|
name: "context",
|
|
2392
2575
|
tool: "kb_context",
|
|
2393
2576
|
usage: "context [--profile NAME] [--budget N] [--full-under N] [--format json] [--event NAME]",
|
|
2394
2577
|
description: "The pinned-base index block, for injection at every context birth \u2014 startup, clear, resume, and after compaction. An index, not the content: concept ids, titles and standing, with the bodies left behind kb_load at the point of use. Emits nothing when nothing is pinned. Refuses with the list of bases and their sizes rather than truncating past its budget. Budgets resolve most-specific-first: explicit flags, then the workspace manifests' `context` tables (per profile, over their `default`), then the built-in profile (session-start, compact, turn), then package defaults \u2014 so a repo tunes its own numbers in .strauss/kb-pins.json without touching hook commands. Like kb_schema and kb_types this takes no bundlePath \u2014 it reads the workspace pin manifests, because which bases a session should see is workspace state, not a property of one base.",
|
|
2395
|
-
input:
|
|
2396
|
-
budgetTokens:
|
|
2578
|
+
input: z12.object({
|
|
2579
|
+
budgetTokens: z12.number().int().positive().optional().describe(
|
|
2397
2580
|
"Ceiling on the whole emitted block; past it the command refuses with a list of bases rather than truncating. Defaults to 4000."
|
|
2398
2581
|
),
|
|
2399
|
-
fullUnderTokens:
|
|
2582
|
+
fullUnderTokens: z12.number().int().positive().optional().describe(
|
|
2400
2583
|
"Per-base rendering threshold, applied before the budget: a base whose complete load fits under this arrives as full records instead of index lines, and the whole block still answers to budgetTokens. Off by default \u2014 index-only is the safe default at a context birth, because injected bodies outlive the qualifiers on them; the session-start profile opts tiny bases in at 1500."
|
|
2401
2584
|
),
|
|
2402
|
-
profile:
|
|
2585
|
+
profile: z12.string().optional().describe(
|
|
2403
2586
|
"Named budget set: built-ins are session-start (full-under 1500), compact and turn (budget 2500); the manifests' `context` tables override per repo. Unknown names fall through to defaults rather than failing."
|
|
2404
2587
|
),
|
|
2405
|
-
format:
|
|
2588
|
+
format: z12.enum(["markdown", "json"]).optional().describe(
|
|
2406
2589
|
"CLI envelope for hook protocols that require strict JSON on stdout. MCP callers omit this \u2014 the block itself is identical."
|
|
2407
2590
|
),
|
|
2408
|
-
event:
|
|
2591
|
+
event: z12.string().optional().describe(
|
|
2409
2592
|
"hookEventName stamped into the JSON envelope. Only meaningful with format=json."
|
|
2410
2593
|
)
|
|
2411
2594
|
}),
|
|
@@ -2441,14 +2624,14 @@ var contextCommand = define({
|
|
|
2441
2624
|
});
|
|
2442
2625
|
|
|
2443
2626
|
// src/commands/doctor.ts
|
|
2444
|
-
import { z as
|
|
2445
|
-
var days = (what, fallback) =>
|
|
2627
|
+
import { z as z13 } from "zod";
|
|
2628
|
+
var days = (what, fallback) => z13.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
|
|
2446
2629
|
var doctorCommand = define({
|
|
2447
2630
|
name: "doctor",
|
|
2448
2631
|
tool: "kb_doctor",
|
|
2449
2632
|
usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--strict]",
|
|
2450
2633
|
description: "Read-only health sweep: expired, expiring, unverified, aging, orphaned, broken-supersession, superseded-but-cited, drifted anchors. Every group is reported even when empty; nothing is written or re-stamped. Use it when picking up a base you have not touched in a while; kb_validate only checks that pointers between records agree.",
|
|
2451
|
-
input:
|
|
2634
|
+
input: z13.object({
|
|
2452
2635
|
bundlePath,
|
|
2453
2636
|
repoRoot: REPO_ROOT,
|
|
2454
2637
|
expiringDays: days(
|
|
@@ -2463,7 +2646,7 @@ var doctorCommand = define({
|
|
|
2463
2646
|
"How long a record may stay `open` or `proposed` before `aging` reports it, in days.",
|
|
2464
2647
|
DEFAULT_AGING_DAYS
|
|
2465
2648
|
),
|
|
2466
|
-
strict:
|
|
2649
|
+
strict: z13.boolean().optional().describe(
|
|
2467
2650
|
"Turn an expired record into a non-zero exit for the CLI. No effect on the report itself."
|
|
2468
2651
|
)
|
|
2469
2652
|
}),
|
|
@@ -2538,14 +2721,47 @@ function render2(result) {
|
|
|
2538
2721
|
return lines.join("\n");
|
|
2539
2722
|
}
|
|
2540
2723
|
|
|
2724
|
+
// src/commands/impact.ts
|
|
2725
|
+
import { z as z14 } from "zod";
|
|
2726
|
+
var impactCommand = define({
|
|
2727
|
+
name: "impact",
|
|
2728
|
+
tool: "kb_impact",
|
|
2729
|
+
usage: "impact <concept-id> [--depth N] [--rels a,b]",
|
|
2730
|
+
description: "What breaks if this record changes: its transitive set of dependants, each with its standing. Each rel declares which of its ends depends on the other, and the walk follows each rel in its own direction. Naming `related_to` or an unknown rel in `rels` is an error. kb_backlinks gives one flat hop.",
|
|
2731
|
+
input: z14.object({
|
|
2732
|
+
bundlePath,
|
|
2733
|
+
conceptId,
|
|
2734
|
+
depth: z14.number().int().positive().optional().describe(
|
|
2735
|
+
"Hops out from the record. Unbounded when omitted; a walk this cuts reports truncated: true."
|
|
2736
|
+
),
|
|
2737
|
+
rels: z14.array(z14.enum(KB_CAUSAL_LINK_RELS)).optional().describe(
|
|
2738
|
+
"Narrow which rels the walk follows. Defaults to every rel that carries a dependence \u2014 all but related_to."
|
|
2739
|
+
)
|
|
2740
|
+
}),
|
|
2741
|
+
fromArgv: (argv, path) => {
|
|
2742
|
+
const depth = argvFlag(argv, "--depth");
|
|
2743
|
+
const rels = argvFlag(argv, "--rels");
|
|
2744
|
+
return {
|
|
2745
|
+
bundlePath: path,
|
|
2746
|
+
conceptId: argv[1],
|
|
2747
|
+
...depth ? { depth: Number(depth) } : {},
|
|
2748
|
+
...rels ? { rels: rels.split(",").filter(Boolean) } : {}
|
|
2749
|
+
};
|
|
2750
|
+
},
|
|
2751
|
+
run: async ({ store }, { bundlePath: path, conceptId: id, depth, rels }) => store.impact(path, id, {
|
|
2752
|
+
...depth !== void 0 ? { depth } : {},
|
|
2753
|
+
...rels?.length ? { rels } : {}
|
|
2754
|
+
})
|
|
2755
|
+
});
|
|
2756
|
+
|
|
2541
2757
|
// src/commands/list.ts
|
|
2542
|
-
import { z as
|
|
2758
|
+
import { z as z15 } from "zod";
|
|
2543
2759
|
var listCommand = define({
|
|
2544
2760
|
name: "list",
|
|
2545
2761
|
tool: "kb_list",
|
|
2546
2762
|
usage: "list [type]",
|
|
2547
2763
|
description: "Every record, optionally narrowed to one type. Use kb_query when you have a question; this is for enumerating.",
|
|
2548
|
-
input:
|
|
2764
|
+
input: z15.object({ bundlePath, type: z15.enum(KB_RECORD_TYPES).optional() }),
|
|
2549
2765
|
fromArgv: (argv, path) => ({ bundlePath: path, type: argv[1] }),
|
|
2550
2766
|
run: async ({ store }, { bundlePath: path, type }) => (await store.list(path, type)).map((record) => ({
|
|
2551
2767
|
conceptId: record.conceptId,
|
|
@@ -2557,17 +2773,17 @@ var listCommand = define({
|
|
|
2557
2773
|
});
|
|
2558
2774
|
|
|
2559
2775
|
// src/commands/load.ts
|
|
2560
|
-
import { z as
|
|
2776
|
+
import { z as z16 } from "zod";
|
|
2561
2777
|
var loadCommand = define({
|
|
2562
2778
|
name: "load",
|
|
2563
2779
|
tool: "kb_load",
|
|
2564
2780
|
usage: "load [type] [--budget N | --all] [--repo-root PATH]",
|
|
2565
|
-
description: "Loads the whole knowledge base at once, each record with its standing. Superseded records arrive as stubs
|
|
2566
|
-
input:
|
|
2781
|
+
description: "Loads the whole knowledge base at once, each record with its standing. Superseded records arrive as stubs; rejected and open records arrive whole. Refuses past the token budget \u2014 call kb_catalog, kb_pack on it; `all` bypasses the budget. Never read record files directly. Cache-stable; `digest` is the base's content stamp \u2014 hooks use it to tell you when to reload.",
|
|
2782
|
+
input: z16.object({
|
|
2567
2783
|
bundlePath,
|
|
2568
|
-
type:
|
|
2569
|
-
budgetTokens:
|
|
2570
|
-
all:
|
|
2784
|
+
type: z16.enum(KB_RECORD_TYPES).optional(),
|
|
2785
|
+
budgetTokens: z16.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
|
|
2786
|
+
all: z16.boolean().optional().describe(
|
|
2571
2787
|
"Loads the entire base regardless of size, bypassing the token budget; mutually exclusive with budgetTokens."
|
|
2572
2788
|
),
|
|
2573
2789
|
repoRoot: REPO_ROOT
|
|
@@ -2609,25 +2825,25 @@ var loadCommand = define({
|
|
|
2609
2825
|
});
|
|
2610
2826
|
|
|
2611
2827
|
// src/commands/log.ts
|
|
2612
|
-
import { z as
|
|
2828
|
+
import { z as z17 } from "zod";
|
|
2613
2829
|
var logCommand = define({
|
|
2614
2830
|
name: "log",
|
|
2615
2831
|
tool: "kb_log",
|
|
2616
2832
|
usage: "log",
|
|
2617
2833
|
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.",
|
|
2618
|
-
input:
|
|
2834
|
+
input: z17.object({ bundlePath }),
|
|
2619
2835
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
2620
2836
|
run: ({ store }, { bundlePath: path }) => store.readLog(path)
|
|
2621
2837
|
});
|
|
2622
2838
|
|
|
2623
2839
|
// src/commands/no-decision.ts
|
|
2624
|
-
import { z as
|
|
2840
|
+
import { z as z18 } from "zod";
|
|
2625
2841
|
var noDecisionCommand = define({
|
|
2626
2842
|
name: "no-decision",
|
|
2627
2843
|
tool: "kb_no_decision",
|
|
2628
2844
|
usage: "no-decision <reason...>",
|
|
2629
2845
|
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.',
|
|
2630
|
-
input:
|
|
2846
|
+
input: z18.object({ bundlePath, reason: z18.string().min(1) }),
|
|
2631
2847
|
fromArgv: (argv, path) => ({
|
|
2632
2848
|
bundlePath: path,
|
|
2633
2849
|
reason: argv.slice(1).join(" ").trim()
|
|
@@ -2644,20 +2860,20 @@ var noDecisionCommand = define({
|
|
|
2644
2860
|
});
|
|
2645
2861
|
|
|
2646
2862
|
// src/commands/pack.ts
|
|
2647
|
-
import { z as
|
|
2863
|
+
import { z as z19 } from "zod";
|
|
2648
2864
|
var packCommand = define({
|
|
2649
2865
|
name: "pack",
|
|
2650
2866
|
tool: "kb_pack",
|
|
2651
2867
|
usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
|
|
2652
2868
|
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.",
|
|
2653
|
-
input:
|
|
2869
|
+
input: z19.object({
|
|
2654
2870
|
bundlePath,
|
|
2655
2871
|
conceptId,
|
|
2656
|
-
hops:
|
|
2657
|
-
maxNodes:
|
|
2872
|
+
hops: z19.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
|
|
2873
|
+
maxNodes: z19.number().int().positive().optional().describe(
|
|
2658
2874
|
"How many records the pack may hold, root included. Defaults to 20."
|
|
2659
2875
|
),
|
|
2660
|
-
budgetTokens:
|
|
2876
|
+
budgetTokens: z19.number().int().positive().optional().describe(
|
|
2661
2877
|
"Approximate token ceiling over what is actually emitted. Defaults to 25000."
|
|
2662
2878
|
)
|
|
2663
2879
|
}),
|
|
@@ -2744,22 +2960,22 @@ function warningLabel(warning) {
|
|
|
2744
2960
|
}
|
|
2745
2961
|
|
|
2746
2962
|
// src/commands/pin.ts
|
|
2747
|
-
import { z as
|
|
2963
|
+
import { z as z20 } from "zod";
|
|
2748
2964
|
var pinCommand = define({
|
|
2749
2965
|
name: "pin",
|
|
2750
2966
|
tool: "kb_pin",
|
|
2751
2967
|
usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
|
|
2752
2968
|
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.",
|
|
2753
|
-
input:
|
|
2969
|
+
input: z20.object({
|
|
2754
2970
|
bundlePath,
|
|
2755
|
-
mode:
|
|
2971
|
+
mode: z20.enum(["full", "index"]).optional().describe(
|
|
2756
2972
|
"full: always emit this base's records whole (still under the block budget); index: never upgrade. Absent: the profile's full-under threshold decides."
|
|
2757
2973
|
),
|
|
2758
|
-
profiles:
|
|
2759
|
-
layer:
|
|
2974
|
+
profiles: z20.array(z20.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
|
|
2975
|
+
layer: z20.enum(["project", "local", "user"]).optional().describe(
|
|
2760
2976
|
"Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
|
|
2761
2977
|
),
|
|
2762
|
-
frozen:
|
|
2978
|
+
frozen: z20.boolean().optional().describe(
|
|
2763
2979
|
"true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
|
|
2764
2980
|
)
|
|
2765
2981
|
}),
|
|
@@ -2788,29 +3004,29 @@ var pinCommand = define({
|
|
|
2788
3004
|
});
|
|
2789
3005
|
|
|
2790
3006
|
// src/commands/pins.ts
|
|
2791
|
-
import { z as
|
|
3007
|
+
import { z as z21 } from "zod";
|
|
2792
3008
|
var pinsCommand = define({
|
|
2793
3009
|
name: "pins",
|
|
2794
3010
|
tool: "kb_pins",
|
|
2795
3011
|
usage: "pins",
|
|
2796
3012
|
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.",
|
|
2797
|
-
input:
|
|
3013
|
+
input: z21.object({}),
|
|
2798
3014
|
fromArgv: () => ({}),
|
|
2799
3015
|
run: ({ store }) => listPins(store, process.cwd())
|
|
2800
3016
|
});
|
|
2801
3017
|
|
|
2802
3018
|
// src/commands/query.ts
|
|
2803
|
-
import { z as
|
|
3019
|
+
import { z as z22 } from "zod";
|
|
2804
3020
|
var queryCommand = define({
|
|
2805
3021
|
name: "query",
|
|
2806
3022
|
tool: "kb_query",
|
|
2807
3023
|
usage: "query <text...> [--repo-root PATH]",
|
|
2808
|
-
description: "Search
|
|
2809
|
-
input:
|
|
3024
|
+
description: "Search; every hit carries its standing. Flagged, never filtered: a superseded hit returns with its replacement, a rejected one is marked. Prefer kb_load when the base fits its budget \u2014 a full read beats search. Results are volatile: place them at the tail, not the cached prefix. Never read record files directly.",
|
|
3025
|
+
input: z22.object({
|
|
2810
3026
|
bundlePath,
|
|
2811
|
-
text:
|
|
2812
|
-
type:
|
|
2813
|
-
includeNonCurrent:
|
|
3027
|
+
text: z22.string().optional(),
|
|
3028
|
+
type: z22.enum(KB_RECORD_TYPES).optional(),
|
|
3029
|
+
includeNonCurrent: z22.boolean().optional(),
|
|
2814
3030
|
repoRoot: REPO_ROOT
|
|
2815
3031
|
}),
|
|
2816
3032
|
// `--repo-root` is a flag, so its value must not fall into the search text.
|
|
@@ -2842,40 +3058,40 @@ var queryCommand = define({
|
|
|
2842
3058
|
});
|
|
2843
3059
|
|
|
2844
3060
|
// src/commands/read-index.ts
|
|
2845
|
-
import { z as
|
|
3061
|
+
import { z as z23 } from "zod";
|
|
2846
3062
|
var readIndexCommand = define({
|
|
2847
3063
|
name: "index",
|
|
2848
3064
|
tool: "kb_index",
|
|
2849
3065
|
usage: "index",
|
|
2850
3066
|
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.",
|
|
2851
|
-
input:
|
|
3067
|
+
input: z23.object({ bundlePath }),
|
|
2852
3068
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
2853
3069
|
run: ({ store }, { bundlePath: path }) => store.readIndex(path)
|
|
2854
3070
|
});
|
|
2855
3071
|
|
|
2856
3072
|
// src/commands/schema.ts
|
|
2857
|
-
import { z as
|
|
3073
|
+
import { z as z24 } from "zod";
|
|
2858
3074
|
var schemaCommand = define({
|
|
2859
3075
|
name: "schema",
|
|
2860
3076
|
tool: "kb_schema",
|
|
2861
3077
|
usage: "schema",
|
|
2862
3078
|
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.",
|
|
2863
|
-
input:
|
|
3079
|
+
input: z24.object({}),
|
|
2864
3080
|
fromArgv: () => ({}),
|
|
2865
3081
|
run: () => Promise.resolve(kbJsonSchemas())
|
|
2866
3082
|
});
|
|
2867
3083
|
|
|
2868
3084
|
// src/commands/status.ts
|
|
2869
|
-
import { z as
|
|
3085
|
+
import { z as z25 } from "zod";
|
|
2870
3086
|
var statusCommand = define({
|
|
2871
3087
|
name: "status",
|
|
2872
3088
|
tool: "kb_status",
|
|
2873
3089
|
usage: "status <concept-id> <status>",
|
|
2874
3090
|
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.",
|
|
2875
|
-
input:
|
|
3091
|
+
input: z25.object({
|
|
2876
3092
|
bundlePath,
|
|
2877
3093
|
conceptId,
|
|
2878
|
-
status:
|
|
3094
|
+
status: z25.enum(KB_RECORD_STATUSES)
|
|
2879
3095
|
}),
|
|
2880
3096
|
fromArgv: (argv, path) => ({
|
|
2881
3097
|
bundlePath: path,
|
|
@@ -2890,13 +3106,13 @@ var statusCommand = define({
|
|
|
2890
3106
|
});
|
|
2891
3107
|
|
|
2892
3108
|
// src/commands/supersede.ts
|
|
2893
|
-
import { z as
|
|
3109
|
+
import { z as z26 } from "zod";
|
|
2894
3110
|
var supersedeCommand = define({
|
|
2895
3111
|
name: "supersede",
|
|
2896
3112
|
tool: "kb_supersede",
|
|
2897
3113
|
usage: "supersede <concept-id> <replacement-id>",
|
|
2898
3114
|
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.",
|
|
2899
|
-
input:
|
|
3115
|
+
input: z26.object({ bundlePath, conceptId, replacementId: conceptId }),
|
|
2900
3116
|
fromArgv: (argv, path) => ({
|
|
2901
3117
|
bundlePath: path,
|
|
2902
3118
|
conceptId: argv[1],
|
|
@@ -2910,16 +3126,16 @@ var supersedeCommand = define({
|
|
|
2910
3126
|
});
|
|
2911
3127
|
|
|
2912
3128
|
// src/commands/sync-instructions.ts
|
|
2913
|
-
import { z as
|
|
3129
|
+
import { z as z27 } from "zod";
|
|
2914
3130
|
var syncInstructionsCommand = define({
|
|
2915
3131
|
name: "sync-instructions",
|
|
2916
3132
|
usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
|
|
2917
3133
|
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.",
|
|
2918
|
-
input:
|
|
2919
|
-
file:
|
|
2920
|
-
budgetTokens:
|
|
2921
|
-
fullUnderTokens:
|
|
2922
|
-
profile:
|
|
3134
|
+
input: z27.object({
|
|
3135
|
+
file: z27.string().min(1).describe("The instruction file to edit in place."),
|
|
3136
|
+
budgetTokens: z27.number().int().positive().optional(),
|
|
3137
|
+
fullUnderTokens: z27.number().int().positive().optional(),
|
|
3138
|
+
profile: z27.string().optional()
|
|
2923
3139
|
}),
|
|
2924
3140
|
fromArgv: (argv) => {
|
|
2925
3141
|
const budget = argvFlag(argv, "--budget");
|
|
@@ -2945,17 +3161,17 @@ var syncInstructionsCommand = define({
|
|
|
2945
3161
|
});
|
|
2946
3162
|
|
|
2947
3163
|
// src/commands/trace.ts
|
|
2948
|
-
import { z as
|
|
3164
|
+
import { z as z28 } from "zod";
|
|
2949
3165
|
var traceCommand = define({
|
|
2950
3166
|
name: "trace",
|
|
2951
3167
|
tool: "kb_trace",
|
|
2952
3168
|
usage: "trace <concept-id> [edges...]",
|
|
2953
3169
|
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.',
|
|
2954
|
-
input:
|
|
3170
|
+
input: z28.object({
|
|
2955
3171
|
bundlePath,
|
|
2956
3172
|
conceptId,
|
|
2957
|
-
edges:
|
|
2958
|
-
depth:
|
|
3173
|
+
edges: z28.array(z28.enum(TRACE_EDGES)).optional(),
|
|
3174
|
+
depth: z28.number().int().positive().optional()
|
|
2959
3175
|
}),
|
|
2960
3176
|
fromArgv: (argv, path) => ({
|
|
2961
3177
|
bundlePath: path,
|
|
@@ -2977,53 +3193,56 @@ var traceCommand = define({
|
|
|
2977
3193
|
});
|
|
2978
3194
|
|
|
2979
3195
|
// src/commands/types.ts
|
|
2980
|
-
import { z as
|
|
3196
|
+
import { z as z29 } from "zod";
|
|
2981
3197
|
var typesCommand = define({
|
|
2982
3198
|
name: "types",
|
|
2983
3199
|
tool: "kb_types",
|
|
2984
3200
|
usage: "types",
|
|
2985
3201
|
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.",
|
|
2986
|
-
input:
|
|
3202
|
+
input: z29.object({}),
|
|
2987
3203
|
fromArgv: () => ({}),
|
|
2988
3204
|
run: () => Promise.resolve(RECORD_TYPES)
|
|
2989
3205
|
});
|
|
2990
3206
|
|
|
2991
3207
|
// src/commands/unpin.ts
|
|
2992
|
-
import { z as
|
|
3208
|
+
import { z as z30 } from "zod";
|
|
2993
3209
|
var unpinCommand = define({
|
|
2994
3210
|
name: "unpin",
|
|
2995
3211
|
tool: "kb_unpin",
|
|
2996
3212
|
usage: "unpin [bundle-path]",
|
|
2997
3213
|
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.",
|
|
2998
|
-
input:
|
|
3214
|
+
input: z30.object({ bundlePath }),
|
|
2999
3215
|
fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
|
|
3000
3216
|
run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
|
|
3001
3217
|
});
|
|
3002
3218
|
|
|
3003
3219
|
// src/commands/validate.ts
|
|
3004
|
-
import { z as
|
|
3220
|
+
import { z as z31 } from "zod";
|
|
3005
3221
|
var validateCommand = define({
|
|
3006
3222
|
name: "validate",
|
|
3007
3223
|
tool: "kb_validate",
|
|
3008
3224
|
usage: "validate",
|
|
3009
|
-
description: "Check pointers no single record can see: supersession links that disagree between the two records, and assumptions that cite sources.
|
|
3010
|
-
input:
|
|
3225
|
+
description: "Check pointers no single record can see: supersession links that disagree between the two records, typed causal links, and assumptions that cite sources. Each finding carries a severity: errors fail the exit code, warnings do not.",
|
|
3226
|
+
input: z31.object({ bundlePath }),
|
|
3011
3227
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
3012
3228
|
run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
|
|
3013
|
-
|
|
3229
|
+
// Warnings never fail the exit code; every other severity does.
|
|
3230
|
+
failsWhen: (result) => Array.isArray(result) && result.some(
|
|
3231
|
+
(problem) => problem.severity !== "warning"
|
|
3232
|
+
)
|
|
3014
3233
|
});
|
|
3015
3234
|
|
|
3016
3235
|
// src/commands/verify.ts
|
|
3017
|
-
import { z as
|
|
3236
|
+
import { z as z32 } from "zod";
|
|
3018
3237
|
var verifyCommand = define({
|
|
3019
3238
|
name: "verify",
|
|
3020
3239
|
tool: "kb_verify",
|
|
3021
3240
|
usage: "verify <concept-id> --note <text>",
|
|
3022
3241
|
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.",
|
|
3023
|
-
input:
|
|
3242
|
+
input: z32.object({
|
|
3024
3243
|
bundlePath,
|
|
3025
3244
|
conceptId,
|
|
3026
|
-
note:
|
|
3245
|
+
note: z32.string().refine((s) => s.trim().length > 0, {
|
|
3027
3246
|
message: "note must say what the check found"
|
|
3028
3247
|
})
|
|
3029
3248
|
}),
|
|
@@ -3043,7 +3262,7 @@ var verifyCommand = define({
|
|
|
3043
3262
|
});
|
|
3044
3263
|
|
|
3045
3264
|
// src/commands/write.ts
|
|
3046
|
-
import { z as
|
|
3265
|
+
import { z as z33 } from "zod";
|
|
3047
3266
|
var writeCommand = define({
|
|
3048
3267
|
name: "write",
|
|
3049
3268
|
tool: "kb_write",
|
|
@@ -3057,9 +3276,9 @@ var writeCommand = define({
|
|
|
3057
3276
|
"- Prefer a new record over overloading an existing one, and keep each short. A record nobody finishes reading is not durable memory.",
|
|
3058
3277
|
"- Records are never deleted; supersede instead, so the earlier reasoning stays inspectable."
|
|
3059
3278
|
].join("\n"),
|
|
3060
|
-
input:
|
|
3279
|
+
input: z33.object({
|
|
3061
3280
|
bundlePath,
|
|
3062
|
-
type:
|
|
3281
|
+
type: z33.enum(KB_RECORD_TYPES),
|
|
3063
3282
|
input: composeInputSchema
|
|
3064
3283
|
}),
|
|
3065
3284
|
fromArgv: async (argv, path, stdin) => ({
|
|
@@ -3083,7 +3302,7 @@ var writeCommand = define({
|
|
|
3083
3302
|
});
|
|
3084
3303
|
|
|
3085
3304
|
// src/commands/write-decision.ts
|
|
3086
|
-
import { z as
|
|
3305
|
+
import { z as z34 } from "zod";
|
|
3087
3306
|
var writeDecisionCommand = define({
|
|
3088
3307
|
name: "write-decision",
|
|
3089
3308
|
tool: "kb_write_decision",
|
|
@@ -3096,7 +3315,7 @@ var writeDecisionCommand = define({
|
|
|
3096
3315
|
"- `alternative` is what you turned down and why, not a list of everything considered.",
|
|
3097
3316
|
"- A reference to material you read goes in `sources`; a reference to code goes in `anchors`; a reference to another record goes in `relatedConceptIds`."
|
|
3098
3317
|
].join("\n"),
|
|
3099
|
-
input:
|
|
3318
|
+
input: z34.object({ bundlePath, input: decisionInputSchema }),
|
|
3100
3319
|
fromArgv: async (_argv, path, stdin) => ({
|
|
3101
3320
|
bundlePath: path,
|
|
3102
3321
|
input: JSON.parse(await stdin())
|
|
@@ -3131,6 +3350,8 @@ var KB_COMMANDS = [
|
|
|
3131
3350
|
packCommand,
|
|
3132
3351
|
queryCommand,
|
|
3133
3352
|
traceCommand,
|
|
3353
|
+
impactCommand,
|
|
3354
|
+
backlinksCommand,
|
|
3134
3355
|
listCommand,
|
|
3135
3356
|
readIndexCommand,
|
|
3136
3357
|
logCommand,
|
|
@@ -3276,6 +3497,141 @@ import {
|
|
|
3276
3497
|
} from "fs/promises";
|
|
3277
3498
|
import { join as join4, resolve as resolve5, sep as sep3 } from "path";
|
|
3278
3499
|
|
|
3500
|
+
// src/kb-links/inbound.ts
|
|
3501
|
+
function inboundIndex(bundle) {
|
|
3502
|
+
const byTarget = /* @__PURE__ */ new Map();
|
|
3503
|
+
for (const record of bundle) {
|
|
3504
|
+
for (const link2 of record.frontmatter.strauss_links ?? []) {
|
|
3505
|
+
if (link2.target === record.conceptId) continue;
|
|
3506
|
+
const edges = byTarget.get(link2.target) ?? [];
|
|
3507
|
+
if (edges.some(
|
|
3508
|
+
(edge) => edge.from === record.conceptId && edge.rel === link2.rel
|
|
3509
|
+
)) {
|
|
3510
|
+
continue;
|
|
3511
|
+
}
|
|
3512
|
+
edges.push({ from: record.conceptId, rel: link2.rel });
|
|
3513
|
+
byTarget.set(link2.target, edges);
|
|
3514
|
+
}
|
|
3515
|
+
}
|
|
3516
|
+
return byTarget;
|
|
3517
|
+
}
|
|
3518
|
+
|
|
3519
|
+
// src/kb-links/backlinks.ts
|
|
3520
|
+
function backlinks(targetId, bundle) {
|
|
3521
|
+
const byId = new Map(bundle.map((record) => [record.conceptId, record]));
|
|
3522
|
+
if (!byId.has(targetId)) throw new KbRecordNotFoundError(targetId);
|
|
3523
|
+
const standingOf = new Map(
|
|
3524
|
+
adjudicate(bundle, bundle).map((hit) => [hit.record.conceptId, hit])
|
|
3525
|
+
);
|
|
3526
|
+
const rows = [];
|
|
3527
|
+
for (const edge of inboundIndex(bundle).get(targetId) ?? []) {
|
|
3528
|
+
const record = byId.get(edge.from);
|
|
3529
|
+
if (!record) continue;
|
|
3530
|
+
const hit = standingOf.get(edge.from);
|
|
3531
|
+
rows.push({
|
|
3532
|
+
...edge,
|
|
3533
|
+
title: record.frontmatter.title ?? null,
|
|
3534
|
+
standing: hit?.standing ?? "unsettled",
|
|
3535
|
+
warnings: hit?.warnings ?? []
|
|
3536
|
+
});
|
|
3537
|
+
}
|
|
3538
|
+
return {
|
|
3539
|
+
target: targetId,
|
|
3540
|
+
backlinks: rows.sort(
|
|
3541
|
+
(left, right) => left.from.localeCompare(right.from) || left.rel.localeCompare(right.rel)
|
|
3542
|
+
)
|
|
3543
|
+
};
|
|
3544
|
+
}
|
|
3545
|
+
|
|
3546
|
+
// src/kb-links/impact.ts
|
|
3547
|
+
function impact(targetId, bundle, options = {}) {
|
|
3548
|
+
const byId = new Map(bundle.map((record) => [record.conceptId, record]));
|
|
3549
|
+
if (!byId.has(targetId)) throw new KbRecordNotFoundError(targetId);
|
|
3550
|
+
const rels = resolveRels(options.rels);
|
|
3551
|
+
const maxDepth = options.depth ?? Number.POSITIVE_INFINITY;
|
|
3552
|
+
const inbound = inboundIndex(bundle);
|
|
3553
|
+
const standingOf = new Map(
|
|
3554
|
+
adjudicate(bundle, bundle).map((hit) => [hit.record.conceptId, hit])
|
|
3555
|
+
);
|
|
3556
|
+
const reached = /* @__PURE__ */ new Map();
|
|
3557
|
+
const stopped = [];
|
|
3558
|
+
let frontier = [targetId];
|
|
3559
|
+
let depth = 0;
|
|
3560
|
+
while (frontier.length && depth < maxDepth) {
|
|
3561
|
+
depth += 1;
|
|
3562
|
+
const next = [];
|
|
3563
|
+
const consider = (dependantId, edge) => {
|
|
3564
|
+
if (dependantId === targetId) return;
|
|
3565
|
+
const existing = reached.get(dependantId);
|
|
3566
|
+
if (existing) {
|
|
3567
|
+
if (!hasEdge(existing.via, edge)) existing.via.push(edge);
|
|
3568
|
+
return;
|
|
3569
|
+
}
|
|
3570
|
+
const record = byId.get(dependantId);
|
|
3571
|
+
if (!record) return;
|
|
3572
|
+
const hit = standingOf.get(dependantId);
|
|
3573
|
+
const entry = {
|
|
3574
|
+
conceptId: dependantId,
|
|
3575
|
+
title: record.frontmatter.title ?? null,
|
|
3576
|
+
standing: hit?.standing ?? "unsettled",
|
|
3577
|
+
warnings: hit?.warnings ?? [],
|
|
3578
|
+
depth,
|
|
3579
|
+
via: [edge]
|
|
3580
|
+
};
|
|
3581
|
+
reached.set(dependantId, entry);
|
|
3582
|
+
if (entry.standing === "superseded" || entry.standing === "rejected") {
|
|
3583
|
+
stopped.push(dependantId);
|
|
3584
|
+
return;
|
|
3585
|
+
}
|
|
3586
|
+
next.push(dependantId);
|
|
3587
|
+
};
|
|
3588
|
+
for (const id of frontier) {
|
|
3589
|
+
for (const edge of inbound.get(id) ?? []) {
|
|
3590
|
+
if (!rels.has(edge.rel)) continue;
|
|
3591
|
+
if (dependantEnd(edge.rel) !== "source") continue;
|
|
3592
|
+
consider(edge.from, { source: edge.from, target: id, rel: edge.rel });
|
|
3593
|
+
}
|
|
3594
|
+
for (const link2 of byId.get(id)?.frontmatter.strauss_links ?? []) {
|
|
3595
|
+
if (!rels.has(link2.rel)) continue;
|
|
3596
|
+
if (dependantEnd(link2.rel) !== "target") continue;
|
|
3597
|
+
if (link2.target === id) continue;
|
|
3598
|
+
consider(link2.target, {
|
|
3599
|
+
source: id,
|
|
3600
|
+
target: link2.target,
|
|
3601
|
+
rel: link2.rel
|
|
3602
|
+
});
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3605
|
+
frontier = next;
|
|
3606
|
+
}
|
|
3607
|
+
return {
|
|
3608
|
+
root: targetId,
|
|
3609
|
+
impacted: [...reached.values()].sort(
|
|
3610
|
+
(left, right) => left.depth - right.depth || left.conceptId.localeCompare(right.conceptId)
|
|
3611
|
+
),
|
|
3612
|
+
stopped: stopped.sort(),
|
|
3613
|
+
truncated: frontier.length > 0,
|
|
3614
|
+
unexpanded: [...frontier].sort()
|
|
3615
|
+
};
|
|
3616
|
+
}
|
|
3617
|
+
function resolveRels(rels) {
|
|
3618
|
+
if (!rels?.length) return new Set(KB_CAUSAL_LINK_RELS);
|
|
3619
|
+
for (const rel of rels) {
|
|
3620
|
+
if (!isKbLinkRel(rel) || LINK_RELS[rel].dependant === null) {
|
|
3621
|
+
throw new KbUnknownLinkRelError(rel, KB_CAUSAL_LINK_RELS);
|
|
3622
|
+
}
|
|
3623
|
+
}
|
|
3624
|
+
return new Set(rels);
|
|
3625
|
+
}
|
|
3626
|
+
function dependantEnd(rel) {
|
|
3627
|
+
return isKbLinkRel(rel) ? LINK_RELS[rel].dependant : null;
|
|
3628
|
+
}
|
|
3629
|
+
function hasEdge(edges, edge) {
|
|
3630
|
+
return edges.some(
|
|
3631
|
+
(existing) => existing.source === edge.source && existing.target === edge.target && existing.rel === edge.rel
|
|
3632
|
+
);
|
|
3633
|
+
}
|
|
3634
|
+
|
|
3279
3635
|
// src/kb-gitattributes.ts
|
|
3280
3636
|
var GITATTRIBUTES_FILE = ".gitattributes";
|
|
3281
3637
|
var UNION_MERGE_LINE = `${LOG_FILE} text eol=lf merge=union`;
|
|
@@ -3642,6 +3998,7 @@ ${answer}
|
|
|
3642
3998
|
const records = adjudicated.filter((hit) => hit.standing !== "superseded");
|
|
3643
3999
|
const superseded = adjudicated.filter((hit) => hit.standing === "superseded").map(stub);
|
|
3644
4000
|
const approxTokens2 = records.reduce((total, hit) => total + estimateTokens(hit.record), 0) + superseded.reduce((total, entry) => total + estimateStubTokens(entry), 0);
|
|
4001
|
+
const bundleDigestValue = bundleDigest(records, superseded);
|
|
3645
4002
|
if (!options.all && approxTokens2 > budgetTokens) {
|
|
3646
4003
|
return {
|
|
3647
4004
|
loaded: false,
|
|
@@ -3652,7 +4009,8 @@ ${answer}
|
|
|
3652
4009
|
approxTokens: approxTokens2,
|
|
3653
4010
|
budgetTokens,
|
|
3654
4011
|
type: options.type
|
|
3655
|
-
})
|
|
4012
|
+
}),
|
|
4013
|
+
digest: bundleDigestValue
|
|
3656
4014
|
};
|
|
3657
4015
|
}
|
|
3658
4016
|
return {
|
|
@@ -3661,7 +4019,8 @@ ${answer}
|
|
|
3661
4019
|
tokensLoaded: approxTokens2,
|
|
3662
4020
|
budgetTokens: options.all ? null : budgetTokens,
|
|
3663
4021
|
records,
|
|
3664
|
-
superseded
|
|
4022
|
+
superseded,
|
|
4023
|
+
digest: bundleDigestValue
|
|
3665
4024
|
};
|
|
3666
4025
|
}
|
|
3667
4026
|
/** How a position was arrived at, as a timeline. See `trace.ts`. */
|
|
@@ -3676,6 +4035,14 @@ ${answer}
|
|
|
3676
4035
|
async pack(bundlePath2, rootId, options = {}) {
|
|
3677
4036
|
return pack(await this.list(bundlePath2), rootId, options);
|
|
3678
4037
|
}
|
|
4038
|
+
/** What breaks if this record changes. See `kb-links/impact.ts`. */
|
|
4039
|
+
async impact(bundlePath2, targetId, options = {}) {
|
|
4040
|
+
return impact(targetId, await this.list(bundlePath2), options);
|
|
4041
|
+
}
|
|
4042
|
+
/** Who points at this record, one hop. See `kb-links/backlinks.ts`. */
|
|
4043
|
+
async backlinks(bundlePath2, targetId) {
|
|
4044
|
+
return backlinks(targetId, await this.list(bundlePath2));
|
|
4045
|
+
}
|
|
3679
4046
|
/**
|
|
3680
4047
|
* The stored index, rebuilt if it disagrees with the records.
|
|
3681
4048
|
*
|
|
@@ -3978,6 +4345,22 @@ function normalizeActor(id) {
|
|
|
3978
4345
|
function digest(contents) {
|
|
3979
4346
|
return createHash2("sha256").update(contents).digest("hex");
|
|
3980
4347
|
}
|
|
4348
|
+
function bundleDigest(records, superseded) {
|
|
4349
|
+
const entries = [
|
|
4350
|
+
...records.map(
|
|
4351
|
+
(hit) => `${hit.record.conceptId}:current:${digest(
|
|
4352
|
+
stringifyMarkdownWithFrontmatter(
|
|
4353
|
+
hit.record.body,
|
|
4354
|
+
hit.record.frontmatter
|
|
4355
|
+
)
|
|
4356
|
+
)}`
|
|
4357
|
+
),
|
|
4358
|
+
...superseded.map(
|
|
4359
|
+
(entry) => `${entry.conceptId}:superseded:${digest(JSON.stringify(entry))}`
|
|
4360
|
+
)
|
|
4361
|
+
].sort();
|
|
4362
|
+
return digest(entries.join("\n"));
|
|
4363
|
+
}
|
|
3981
4364
|
|
|
3982
4365
|
// src/pack.ts
|
|
3983
4366
|
var DEFAULT_PACK_HOPS = 2;
|
|
@@ -4064,13 +4447,14 @@ function typeRank(record) {
|
|
|
4064
4447
|
}
|
|
4065
4448
|
|
|
4066
4449
|
// src/version.ts
|
|
4067
|
-
var VERSION = true ? "0.1.
|
|
4450
|
+
var VERSION = true ? "0.1.13" : "0.0.0-dev";
|
|
4068
4451
|
|
|
4069
4452
|
export {
|
|
4070
4453
|
kbSourceSchema,
|
|
4071
4454
|
kbActorStampSchema,
|
|
4072
4455
|
kbVerifiedEventSchema,
|
|
4073
4456
|
kbAnchorSchema,
|
|
4457
|
+
kbLinkSchema,
|
|
4074
4458
|
KB_RECORD_TYPES,
|
|
4075
4459
|
KB_SLUG_PATTERN,
|
|
4076
4460
|
KB_CONCEPT_ID_PATTERN,
|
|
@@ -4081,6 +4465,11 @@ export {
|
|
|
4081
4465
|
kbRecordFrontmatterSchema,
|
|
4082
4466
|
RECORD_TYPES,
|
|
4083
4467
|
isKbRecordType,
|
|
4468
|
+
KB_LINK_RELS,
|
|
4469
|
+
LINK_RELS,
|
|
4470
|
+
KB_CAUSAL_LINK_RELS,
|
|
4471
|
+
isKbLinkRel,
|
|
4472
|
+
composeLinkSchema,
|
|
4084
4473
|
composeInputSchema,
|
|
4085
4474
|
composeRecord,
|
|
4086
4475
|
DECISION_TYPE,
|
|
@@ -4103,6 +4492,7 @@ export {
|
|
|
4103
4492
|
KbWriteConflictError,
|
|
4104
4493
|
KbSelfVerificationError,
|
|
4105
4494
|
KbPackBudgetExceededError,
|
|
4495
|
+
KbUnknownLinkRelError,
|
|
4106
4496
|
KbMissingFlagValueError,
|
|
4107
4497
|
KbInvalidConceptIdError,
|
|
4108
4498
|
contextProfileBudgets,
|
|
@@ -4134,6 +4524,7 @@ export {
|
|
|
4134
4524
|
CONTEXT_END,
|
|
4135
4525
|
syncInstructions,
|
|
4136
4526
|
KB_EDGE_KINDS,
|
|
4527
|
+
DEFAULT_TYPED_LINK_RELS,
|
|
4137
4528
|
neighbours,
|
|
4138
4529
|
edgeNeighbours,
|
|
4139
4530
|
validateBundle,
|
|
@@ -4161,9 +4552,12 @@ export {
|
|
|
4161
4552
|
DEFAULT_PACK_HOPS,
|
|
4162
4553
|
DEFAULT_PACK_MAX_NODES,
|
|
4163
4554
|
pack,
|
|
4555
|
+
inboundIndex,
|
|
4556
|
+
backlinks,
|
|
4557
|
+
impact,
|
|
4164
4558
|
KB_DIR,
|
|
4165
4559
|
DEFAULT_LOAD_BUDGET,
|
|
4166
4560
|
KbStore,
|
|
4167
4561
|
VERSION
|
|
4168
4562
|
};
|
|
4169
|
-
//# sourceMappingURL=chunk-
|
|
4563
|
+
//# sourceMappingURL=chunk-XALWG3EZ.js.map
|