@saasontools/strauss-kb 0.1.12 → 0.1.14
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 +54 -50
- package/README.md +172 -420
- package/dist/{chunk-33ZCBEUV.js → chunk-43KALLFU.js} +482 -122
- package/dist/chunk-43KALLFU.js.map +1 -0
- package/dist/{chunk-F2U2YLWV.js → chunk-MBXNCZ4V.js} +2 -2
- package/dist/{chunk-EXKK2KUN.js → chunk-PYA5E7FL.js} +2 -2
- package/dist/cli-main.cjs +480 -132
- package/dist/cli-main.cjs.map +1 -1
- package/dist/cli-main.js +2 -2
- package/dist/index.cjs +559 -188
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +279 -13
- package/dist/index.d.ts +279 -13
- package/dist/index.js +25 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp-main.cjs +480 -132
- package/dist/mcp-main.cjs.map +1 -1
- package/dist/mcp-main.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-33ZCBEUV.js.map +0 -1
- /package/dist/{chunk-F2U2YLWV.js.map → chunk-MBXNCZ4V.js.map} +0 -0
- /package/dist/{chunk-EXKK2KUN.js.map → chunk-PYA5E7FL.js.map} +0 -0
package/dist/mcp-main.cjs
CHANGED
|
@@ -79,6 +79,10 @@ var kbAnchorSchema = import_zod.z.object({
|
|
|
79
79
|
/** Line count of the text the hash was taken over. */
|
|
80
80
|
lines: import_zod.z.number().int().positive().optional()
|
|
81
81
|
}).strict();
|
|
82
|
+
var kbLinkSchema = import_zod.z.object({
|
|
83
|
+
target: import_zod.z.string().min(1),
|
|
84
|
+
rel: import_zod.z.string().min(1)
|
|
85
|
+
}).passthrough();
|
|
82
86
|
var KB_RECORD_TYPES = [
|
|
83
87
|
"fact",
|
|
84
88
|
"requirement",
|
|
@@ -130,6 +134,10 @@ var kbRecordFrontmatterSchema = import_zod.z.object({
|
|
|
130
134
|
// strauss extensions — see the module comment.
|
|
131
135
|
strauss_anchors: import_zod.z.array(kbAnchorSchema).optional(),
|
|
132
136
|
strauss_verify: import_zod.z.array(import_zod.z.string().min(1)).optional(),
|
|
137
|
+
// Typed causal edges, source → target, living on the source. `A depends_on
|
|
138
|
+
// B` means A needs B, so `kb_impact` walks these inbound: what breaks if B
|
|
139
|
+
// changes is whatever declared a dependence on it.
|
|
140
|
+
strauss_links: import_zod.z.array(kbLinkSchema).optional(),
|
|
133
141
|
// Total after parsing, tolerant before it. Our producers must supply a
|
|
134
142
|
// status — an absent one would leave every reader inventing its own default
|
|
135
143
|
// — but OKF calls a concept carrying only `type` fully conformant, so
|
|
@@ -214,8 +222,70 @@ var RECORD_TYPES = {
|
|
|
214
222
|
function isKbRecordType(value) {
|
|
215
223
|
return Object.prototype.hasOwnProperty.call(RECORD_TYPES, value);
|
|
216
224
|
}
|
|
225
|
+
var KB_LINK_RELS = [
|
|
226
|
+
"depends_on",
|
|
227
|
+
"constrains",
|
|
228
|
+
"informs",
|
|
229
|
+
"blocks",
|
|
230
|
+
"invalidates",
|
|
231
|
+
"verified_by",
|
|
232
|
+
"satisfies",
|
|
233
|
+
"related_to"
|
|
234
|
+
];
|
|
235
|
+
var LINK_RELS = {
|
|
236
|
+
depends_on: {
|
|
237
|
+
purpose: "The source needs the target to hold; the source breaks if the target changes",
|
|
238
|
+
phrase: "Depends on",
|
|
239
|
+
dependant: "source"
|
|
240
|
+
},
|
|
241
|
+
constrains: {
|
|
242
|
+
purpose: "The source bounds what the target may do; the target breaks if the constraint changes",
|
|
243
|
+
phrase: "Constrains",
|
|
244
|
+
dependant: "target"
|
|
245
|
+
},
|
|
246
|
+
informs: {
|
|
247
|
+
purpose: "The source shaped the target without binding it; the target is what needs revisiting",
|
|
248
|
+
phrase: "Informs",
|
|
249
|
+
dependant: "target"
|
|
250
|
+
},
|
|
251
|
+
blocks: {
|
|
252
|
+
purpose: "The target cannot proceed until the source is settled; the target is what waits",
|
|
253
|
+
phrase: "Blocks",
|
|
254
|
+
dependant: "target"
|
|
255
|
+
},
|
|
256
|
+
invalidates: {
|
|
257
|
+
purpose: "The source makes the target no longer hold; the target is what stops holding",
|
|
258
|
+
phrase: "Invalidates",
|
|
259
|
+
dependant: "target"
|
|
260
|
+
},
|
|
261
|
+
verified_by: {
|
|
262
|
+
purpose: "The target is the check that confirms the source; the source's confirmation moves with it",
|
|
263
|
+
phrase: "Verified by",
|
|
264
|
+
dependant: "source"
|
|
265
|
+
},
|
|
266
|
+
satisfies: {
|
|
267
|
+
purpose: "The source discharges the target's requirement; the source must change if the requirement does",
|
|
268
|
+
phrase: "Satisfies",
|
|
269
|
+
dependant: "source"
|
|
270
|
+
},
|
|
271
|
+
related_to: {
|
|
272
|
+
purpose: "A pointer worth following, with no claim of dependence",
|
|
273
|
+
phrase: "Relates to",
|
|
274
|
+
dependant: null
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
var KB_CAUSAL_LINK_RELS = KB_LINK_RELS.filter(
|
|
278
|
+
(rel) => LINK_RELS[rel].dependant !== null
|
|
279
|
+
);
|
|
280
|
+
function isKbLinkRel(value) {
|
|
281
|
+
return Object.prototype.hasOwnProperty.call(LINK_RELS, value);
|
|
282
|
+
}
|
|
217
283
|
|
|
218
284
|
// src/compose.ts
|
|
285
|
+
var composeLinkSchema = import_zod2.z.object({
|
|
286
|
+
target: kbConceptIdSchema,
|
|
287
|
+
rel: import_zod2.z.enum(KB_LINK_RELS)
|
|
288
|
+
}).strict();
|
|
219
289
|
var composeInputSchema = import_zod2.z.object({
|
|
220
290
|
slug: import_zod2.z.string().min(1),
|
|
221
291
|
/** One line, in the reader's terms. Becomes OKF `title`. */
|
|
@@ -242,6 +312,18 @@ var composeInputSchema = import_zod2.z.object({
|
|
|
242
312
|
tags: import_zod2.z.array(import_zod2.z.string().min(1)).optional(),
|
|
243
313
|
/** Concept ids this record relates to; rendered as body links. */
|
|
244
314
|
relatedConceptIds: import_zod2.z.array(kbConceptIdSchema).optional(),
|
|
315
|
+
/**
|
|
316
|
+
* Typed causal edges, source → target: `{ target: "fact.b", rel:
|
|
317
|
+
* "depends_on" }` on record A says A needs B. Stored in frontmatter and
|
|
318
|
+
* also rendered as one prose sentence each, so the meaning survives a
|
|
319
|
+
* reader that knows only OKF. The vocabulary goes into the description from
|
|
320
|
+
* the same table the walk uses, so `kb_schema` emits it.
|
|
321
|
+
*/
|
|
322
|
+
links: import_zod2.z.array(composeLinkSchema).max(64).optional().describe(
|
|
323
|
+
`Typed causal edges, source \u2192 target \u2014 a link on this record says this record <rel> the target. ${KB_LINK_RELS.map(
|
|
324
|
+
(rel) => `${rel}: ${LINK_RELS[rel].purpose}`
|
|
325
|
+
).join("; ")}.`
|
|
326
|
+
),
|
|
245
327
|
/** Concept ids this record replaces. The store settles the backlinks. */
|
|
246
328
|
supersedes: import_zod2.z.array(kbConceptIdSchema).max(32).optional(),
|
|
247
329
|
materiality: import_zod2.z.enum(KB_MATERIALITIES).optional(),
|
|
@@ -281,6 +363,15 @@ function composeRecord(type, input, writtenBy, writtenAt) {
|
|
|
281
363
|
if (parsed.owner) frontmatter.strauss_owner = parsed.owner;
|
|
282
364
|
if (parsed.supersedes?.length)
|
|
283
365
|
frontmatter.strauss_supersedes = parsed.supersedes;
|
|
366
|
+
const selfLink = parsed.links?.find(
|
|
367
|
+
(link2) => link2.target === `${type}.${parsed.slug}`
|
|
368
|
+
);
|
|
369
|
+
if (selfLink) {
|
|
370
|
+
throw new Error(
|
|
371
|
+
`kb: ${type}.${parsed.slug} cannot ${selfLink.rel} itself \u2014 a link must name another record`
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
if (parsed.links?.length) frontmatter.strauss_links = parsed.links;
|
|
284
375
|
const blocks = [];
|
|
285
376
|
for (const heading of spec.sections) {
|
|
286
377
|
const text = sections[heading];
|
|
@@ -292,6 +383,11 @@ ${text}`);
|
|
|
292
383
|
for (const related of parsed.relatedConceptIds ?? []) {
|
|
293
384
|
blocks.push(`Relates to [${related}](${related}.md).`);
|
|
294
385
|
}
|
|
386
|
+
for (const link2 of parsed.links ?? []) {
|
|
387
|
+
blocks.push(
|
|
388
|
+
`${LINK_RELS[link2.rel].phrase} [${link2.target}](${link2.target}.md).`
|
|
389
|
+
);
|
|
390
|
+
}
|
|
295
391
|
if (parsed.sources?.length) {
|
|
296
392
|
blocks.push(
|
|
297
393
|
parsed.sources.map((source) => `[^${source.id}]: ${source.title ?? source.resource}`).join("\n")
|
|
@@ -314,7 +410,7 @@ var decisionInputSchema = composeInputSchema.omit({ sections: true }).extend({
|
|
|
314
410
|
impact: import_zod3.z.string().min(1).optional()
|
|
315
411
|
}).strict();
|
|
316
412
|
function composeDecisionRecord(input, writtenBy, writtenAt) {
|
|
317
|
-
const { alternative, impact, ...rest } = input;
|
|
413
|
+
const { alternative, impact: impact2, ...rest } = input;
|
|
318
414
|
return composeRecord(
|
|
319
415
|
DECISION_TYPE,
|
|
320
416
|
{
|
|
@@ -323,7 +419,7 @@ function composeDecisionRecord(input, writtenBy, writtenAt) {
|
|
|
323
419
|
Decision: input.title,
|
|
324
420
|
Rationale: input.why,
|
|
325
421
|
...alternative ? { Rejected: alternative } : {},
|
|
326
|
-
...
|
|
422
|
+
...impact2 ? { Impact: impact2 } : {}
|
|
327
423
|
}
|
|
328
424
|
},
|
|
329
425
|
writtenBy,
|
|
@@ -905,6 +1001,23 @@ var KbPackBudgetExceededError = class extends BaseError {
|
|
|
905
1001
|
budgetTokens;
|
|
906
1002
|
excluded;
|
|
907
1003
|
};
|
|
1004
|
+
var KbUnknownLinkRelError = class extends BaseError {
|
|
1005
|
+
constructor(rel, expected) {
|
|
1006
|
+
super({
|
|
1007
|
+
message: `kb: ${rel} is not a rel a walk can follow \u2014 expected one of ${expected.join(", ")}`,
|
|
1008
|
+
errorType: "KbUnknownLinkRel" /* KbUnknownLinkRel */,
|
|
1009
|
+
code: 400,
|
|
1010
|
+
fault: "User" /* User */,
|
|
1011
|
+
retriable: false,
|
|
1012
|
+
reportToUser: true,
|
|
1013
|
+
details: { rel, expected: expected.join(", ") }
|
|
1014
|
+
});
|
|
1015
|
+
this.rel = rel;
|
|
1016
|
+
this.expected = expected;
|
|
1017
|
+
}
|
|
1018
|
+
rel;
|
|
1019
|
+
expected;
|
|
1020
|
+
};
|
|
908
1021
|
var KbMissingFlagValueError = class extends BaseError {
|
|
909
1022
|
constructor(flag) {
|
|
910
1023
|
super({
|
|
@@ -1422,7 +1535,7 @@ var answerCommand = define({
|
|
|
1422
1535
|
name: "answer",
|
|
1423
1536
|
tool: "kb_answer",
|
|
1424
1537
|
usage: "answer <concept-id> <answer...>",
|
|
1425
|
-
description: "Resolve an open question:
|
|
1538
|
+
description: "Resolve an open question: set status, stamp who and when, append an Answer section. If the answer overturns a decision or assumption, supersede that record explicitly.",
|
|
1426
1539
|
input: import_zod7.z.object({ bundlePath, conceptId, answer: import_zod7.z.string().min(1) }),
|
|
1427
1540
|
fromArgv: (argv, path) => ({
|
|
1428
1541
|
bundlePath: path,
|
|
@@ -1436,8 +1549,20 @@ var answerCommand = define({
|
|
|
1436
1549
|
}
|
|
1437
1550
|
});
|
|
1438
1551
|
|
|
1439
|
-
// src/commands/
|
|
1552
|
+
// src/commands/backlinks.ts
|
|
1440
1553
|
var import_zod8 = require("zod");
|
|
1554
|
+
var backlinksCommand = define({
|
|
1555
|
+
name: "backlinks",
|
|
1556
|
+
tool: "kb_backlinks",
|
|
1557
|
+
usage: "backlinks <concept-id>",
|
|
1558
|
+
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.",
|
|
1559
|
+
input: import_zod8.z.object({ bundlePath, conceptId }),
|
|
1560
|
+
fromArgv: (argv, path) => ({ bundlePath: path, conceptId: argv[1] }),
|
|
1561
|
+
run: async ({ store }, { bundlePath: path, conceptId: id }) => store.backlinks(path, id)
|
|
1562
|
+
});
|
|
1563
|
+
|
|
1564
|
+
// src/commands/catalog.ts
|
|
1565
|
+
var import_zod9 = require("zod");
|
|
1441
1566
|
|
|
1442
1567
|
// src/adjudicate.ts
|
|
1443
1568
|
var STANDING = {
|
|
@@ -1597,9 +1722,9 @@ var catalogCommand = define({
|
|
|
1597
1722
|
tool: "kb_catalog",
|
|
1598
1723
|
usage: "catalog [type]",
|
|
1599
1724
|
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.",
|
|
1600
|
-
input:
|
|
1725
|
+
input: import_zod9.z.object({
|
|
1601
1726
|
bundlePath,
|
|
1602
|
-
type:
|
|
1727
|
+
type: import_zod9.z.enum(KB_RECORD_TYPES).optional()
|
|
1603
1728
|
}),
|
|
1604
1729
|
fromArgv: (argv, path) => ({
|
|
1605
1730
|
bundlePath: path,
|
|
@@ -1654,7 +1779,7 @@ function count(value, noun) {
|
|
|
1654
1779
|
}
|
|
1655
1780
|
|
|
1656
1781
|
// src/commands/context.ts
|
|
1657
|
-
var
|
|
1782
|
+
var import_zod10 = require("zod");
|
|
1658
1783
|
|
|
1659
1784
|
// src/kb-context.ts
|
|
1660
1785
|
var import_promises3 = require("fs/promises");
|
|
@@ -1919,21 +2044,21 @@ var contextCommand = define({
|
|
|
1919
2044
|
name: "context",
|
|
1920
2045
|
tool: "kb_context",
|
|
1921
2046
|
usage: "context [--profile NAME] [--budget N] [--full-under N] [--format json] [--event NAME]",
|
|
1922
|
-
description: "
|
|
1923
|
-
input:
|
|
1924
|
-
budgetTokens:
|
|
2047
|
+
description: "Index block of pinned bases (ids, titles, standing) for injection at context birth. Takes no bundlePath \u2014 reads the workspace pin manifests. Empty when nothing is pinned; refuses over budget rather than truncating. Budget precedence: flags, then the manifest `context[profile]` over `context.default`, then the built-in profile, then package defaults.",
|
|
2048
|
+
input: import_zod10.z.object({
|
|
2049
|
+
budgetTokens: import_zod10.z.number().int().positive().optional().describe(
|
|
1925
2050
|
"Ceiling on the whole emitted block; past it the command refuses with a list of bases rather than truncating. Defaults to 4000."
|
|
1926
2051
|
),
|
|
1927
|
-
fullUnderTokens:
|
|
2052
|
+
fullUnderTokens: import_zod10.z.number().int().positive().optional().describe(
|
|
1928
2053
|
"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."
|
|
1929
2054
|
),
|
|
1930
|
-
profile:
|
|
2055
|
+
profile: import_zod10.z.string().optional().describe(
|
|
1931
2056
|
"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."
|
|
1932
2057
|
),
|
|
1933
|
-
format:
|
|
2058
|
+
format: import_zod10.z.enum(["markdown", "json"]).optional().describe(
|
|
1934
2059
|
"CLI envelope for hook protocols that require strict JSON on stdout. MCP callers omit this \u2014 the block itself is identical."
|
|
1935
2060
|
),
|
|
1936
|
-
event:
|
|
2061
|
+
event: import_zod10.z.string().optional().describe(
|
|
1937
2062
|
"hookEventName stamped into the JSON envelope. Only meaningful with format=json."
|
|
1938
2063
|
)
|
|
1939
2064
|
}),
|
|
@@ -1969,11 +2094,12 @@ var contextCommand = define({
|
|
|
1969
2094
|
});
|
|
1970
2095
|
|
|
1971
2096
|
// src/commands/doctor.ts
|
|
1972
|
-
var
|
|
2097
|
+
var import_zod11 = require("zod");
|
|
1973
2098
|
|
|
1974
2099
|
// src/kb-edges.ts
|
|
1975
2100
|
var KB_EDGE_KINDS = [
|
|
1976
2101
|
"body-link",
|
|
2102
|
+
"typed-link",
|
|
1977
2103
|
"supersession",
|
|
1978
2104
|
"anchor",
|
|
1979
2105
|
"source"
|
|
@@ -1982,10 +2108,11 @@ var BODY_LINK_TARGET = new RegExp(
|
|
|
1982
2108
|
`\\]\\((${KB_CONCEPT_ID_PATTERN.source.replace(/^\^|\$$/g, "")})\\.md\\)`,
|
|
1983
2109
|
"g"
|
|
1984
2110
|
);
|
|
1985
|
-
|
|
2111
|
+
var DEFAULT_TYPED_LINK_RELS = KB_LINK_RELS;
|
|
2112
|
+
function neighbours(from, bundle, kinds = KB_EDGE_KINDS, linkRels = DEFAULT_TYPED_LINK_RELS) {
|
|
1986
2113
|
const found = /* @__PURE__ */ new Map();
|
|
1987
2114
|
for (const kind of kinds) {
|
|
1988
|
-
for (const record of edgeNeighbours(from, bundle, kind)) {
|
|
2115
|
+
for (const record of edgeNeighbours(from, bundle, kind, linkRels)) {
|
|
1989
2116
|
const existing = found.get(record.conceptId);
|
|
1990
2117
|
if (existing) {
|
|
1991
2118
|
if (!existing.via.includes(kind)) existing.via.push(kind);
|
|
@@ -1996,7 +2123,7 @@ function neighbours(from, bundle, kinds = KB_EDGE_KINDS) {
|
|
|
1996
2123
|
}
|
|
1997
2124
|
return [...found.values()];
|
|
1998
2125
|
}
|
|
1999
|
-
function edgeNeighbours(from, bundle, kind) {
|
|
2126
|
+
function edgeNeighbours(from, bundle, kind, linkRels = DEFAULT_TYPED_LINK_RELS) {
|
|
2000
2127
|
switch (kind) {
|
|
2001
2128
|
// A link whose target is not in the bundle is legal per compose.ts —
|
|
2002
2129
|
// records are routinely written before the ones they point at exist — so
|
|
@@ -2010,6 +2137,21 @@ function edgeNeighbours(from, bundle, kind) {
|
|
|
2010
2137
|
(candidate) => candidate.conceptId !== from.conceptId && targets.has(candidate.conceptId)
|
|
2011
2138
|
);
|
|
2012
2139
|
}
|
|
2140
|
+
// Outbound only, like `body-link`, and for the same reason: this is what
|
|
2141
|
+
// the record declares about itself. A missing target is legal — the walk
|
|
2142
|
+
// skips it, and `kb_validate` is what reports it as a warning. A rel
|
|
2143
|
+
// outside `linkRels` is skipped too, which is how an unknown rel stays
|
|
2144
|
+
// untraversable everywhere rather than one walk at a time.
|
|
2145
|
+
case "typed-link": {
|
|
2146
|
+
const allowed = new Set(linkRels);
|
|
2147
|
+
const targets = new Set(
|
|
2148
|
+
(from.frontmatter.strauss_links ?? []).filter((link2) => allowed.has(link2.rel)).map((link2) => link2.target)
|
|
2149
|
+
);
|
|
2150
|
+
if (!targets.size) return [];
|
|
2151
|
+
return bundle.filter(
|
|
2152
|
+
(candidate) => candidate.conceptId !== from.conceptId && targets.has(candidate.conceptId)
|
|
2153
|
+
);
|
|
2154
|
+
}
|
|
2013
2155
|
// Both directions and both pointers: `supersede()` writes the pair, but a
|
|
2014
2156
|
// hand-edit can leave one side behind, and a walk trusting one pointer
|
|
2015
2157
|
// would miss a replacement the bundle openly declares.
|
|
@@ -2051,7 +2193,7 @@ function anchorsTouch(left, right) {
|
|
|
2051
2193
|
function validateBundle(records) {
|
|
2052
2194
|
const byId = new Map(records.map((record) => [record.conceptId, record]));
|
|
2053
2195
|
const problems = [];
|
|
2054
|
-
const report = (check, conceptId2, note) => problems.push({ check, conceptId: conceptId2, note });
|
|
2196
|
+
const report = (check, conceptId2, note, severity = "error") => problems.push({ check, conceptId: conceptId2, note, severity });
|
|
2055
2197
|
for (const record of records) {
|
|
2056
2198
|
const { conceptId: conceptId2, frontmatter: fm } = record;
|
|
2057
2199
|
if (!isKbRecordType(fm.type)) {
|
|
@@ -2075,6 +2217,36 @@ function validateBundle(records) {
|
|
|
2075
2217
|
report("supersedes", conceptId2, `${old} is not marked superseded`);
|
|
2076
2218
|
}
|
|
2077
2219
|
}
|
|
2220
|
+
for (const link2 of fm.strauss_links ?? []) {
|
|
2221
|
+
if (!isKbLinkRel(link2.rel)) {
|
|
2222
|
+
report(
|
|
2223
|
+
"link_rel",
|
|
2224
|
+
conceptId2,
|
|
2225
|
+
`unknown rel "${link2.rel}" on link to ${link2.target} \u2014 expected one of ${KB_LINK_RELS.join(", ")}`
|
|
2226
|
+
);
|
|
2227
|
+
}
|
|
2228
|
+
if (!KB_CONCEPT_ID_PATTERN.test(link2.target)) {
|
|
2229
|
+
report(
|
|
2230
|
+
"link_target",
|
|
2231
|
+
conceptId2,
|
|
2232
|
+
`target "${link2.target}" is not a valid concept id \u2014 expected <type>.<slug>, both kebab-case`
|
|
2233
|
+
);
|
|
2234
|
+
} else if (link2.target === conceptId2) {
|
|
2235
|
+
report(
|
|
2236
|
+
"link_target",
|
|
2237
|
+
conceptId2,
|
|
2238
|
+
`links to itself (${link2.rel})`,
|
|
2239
|
+
"warning"
|
|
2240
|
+
);
|
|
2241
|
+
} else if (!byId.has(link2.target)) {
|
|
2242
|
+
report(
|
|
2243
|
+
"link_target",
|
|
2244
|
+
conceptId2,
|
|
2245
|
+
`target ${link2.target} is not in the bundle`,
|
|
2246
|
+
"warning"
|
|
2247
|
+
);
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2078
2250
|
if (fm.strauss_assumption && fm.sources?.length) {
|
|
2079
2251
|
report("assumption", conceptId2, "marked an assumption but cites sources");
|
|
2080
2252
|
}
|
|
@@ -2351,13 +2523,13 @@ function ageInDays(record, now) {
|
|
|
2351
2523
|
}
|
|
2352
2524
|
|
|
2353
2525
|
// src/commands/doctor.ts
|
|
2354
|
-
var days = (what, fallback) =>
|
|
2526
|
+
var days = (what, fallback) => import_zod11.z.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
|
|
2355
2527
|
var doctorCommand = define({
|
|
2356
2528
|
name: "doctor",
|
|
2357
2529
|
tool: "kb_doctor",
|
|
2358
2530
|
usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--strict]",
|
|
2359
2531
|
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.",
|
|
2360
|
-
input:
|
|
2532
|
+
input: import_zod11.z.object({
|
|
2361
2533
|
bundlePath,
|
|
2362
2534
|
repoRoot: REPO_ROOT,
|
|
2363
2535
|
expiringDays: days(
|
|
@@ -2372,7 +2544,7 @@ var doctorCommand = define({
|
|
|
2372
2544
|
"How long a record may stay `open` or `proposed` before `aging` reports it, in days.",
|
|
2373
2545
|
DEFAULT_AGING_DAYS
|
|
2374
2546
|
),
|
|
2375
|
-
strict:
|
|
2547
|
+
strict: import_zod11.z.boolean().optional().describe(
|
|
2376
2548
|
"Turn an expired record into a non-zero exit for the CLI. No effect on the report itself."
|
|
2377
2549
|
)
|
|
2378
2550
|
}),
|
|
@@ -2447,14 +2619,47 @@ function render2(result) {
|
|
|
2447
2619
|
return lines.join("\n");
|
|
2448
2620
|
}
|
|
2449
2621
|
|
|
2622
|
+
// src/commands/impact.ts
|
|
2623
|
+
var import_zod12 = require("zod");
|
|
2624
|
+
var impactCommand = define({
|
|
2625
|
+
name: "impact",
|
|
2626
|
+
tool: "kb_impact",
|
|
2627
|
+
usage: "impact <concept-id> [--depth N] [--rels a,b]",
|
|
2628
|
+
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.",
|
|
2629
|
+
input: import_zod12.z.object({
|
|
2630
|
+
bundlePath,
|
|
2631
|
+
conceptId,
|
|
2632
|
+
depth: import_zod12.z.number().int().positive().optional().describe(
|
|
2633
|
+
"Hops out from the record. Unbounded when omitted; a walk this cuts reports truncated: true."
|
|
2634
|
+
),
|
|
2635
|
+
rels: import_zod12.z.array(import_zod12.z.enum(KB_CAUSAL_LINK_RELS)).optional().describe(
|
|
2636
|
+
"Narrow which rels the walk follows. Defaults to every rel that carries a dependence \u2014 all but related_to."
|
|
2637
|
+
)
|
|
2638
|
+
}),
|
|
2639
|
+
fromArgv: (argv, path) => {
|
|
2640
|
+
const depth = argvFlag(argv, "--depth");
|
|
2641
|
+
const rels = argvFlag(argv, "--rels");
|
|
2642
|
+
return {
|
|
2643
|
+
bundlePath: path,
|
|
2644
|
+
conceptId: argv[1],
|
|
2645
|
+
...depth ? { depth: Number(depth) } : {},
|
|
2646
|
+
...rels ? { rels: rels.split(",").filter(Boolean) } : {}
|
|
2647
|
+
};
|
|
2648
|
+
},
|
|
2649
|
+
run: async ({ store }, { bundlePath: path, conceptId: id, depth, rels }) => store.impact(path, id, {
|
|
2650
|
+
...depth !== void 0 ? { depth } : {},
|
|
2651
|
+
...rels?.length ? { rels } : {}
|
|
2652
|
+
})
|
|
2653
|
+
});
|
|
2654
|
+
|
|
2450
2655
|
// src/commands/list.ts
|
|
2451
|
-
var
|
|
2656
|
+
var import_zod13 = require("zod");
|
|
2452
2657
|
var listCommand = define({
|
|
2453
2658
|
name: "list",
|
|
2454
2659
|
tool: "kb_list",
|
|
2455
2660
|
usage: "list [type]",
|
|
2456
|
-
description: "Every record, optionally
|
|
2457
|
-
input:
|
|
2661
|
+
description: "Every record, optionally one type. For enumerating; use kb_query for a question.",
|
|
2662
|
+
input: import_zod13.z.object({ bundlePath, type: import_zod13.z.enum(KB_RECORD_TYPES).optional() }),
|
|
2458
2663
|
fromArgv: (argv, path) => ({ bundlePath: path, type: argv[1] }),
|
|
2459
2664
|
run: async ({ store }, { bundlePath: path, type }) => (await store.list(path, type)).map((record) => ({
|
|
2460
2665
|
conceptId: record.conceptId,
|
|
@@ -2466,17 +2671,17 @@ var listCommand = define({
|
|
|
2466
2671
|
});
|
|
2467
2672
|
|
|
2468
2673
|
// src/commands/load.ts
|
|
2469
|
-
var
|
|
2674
|
+
var import_zod14 = require("zod");
|
|
2470
2675
|
var loadCommand = define({
|
|
2471
2676
|
name: "load",
|
|
2472
2677
|
tool: "kb_load",
|
|
2473
2678
|
usage: "load [type] [--budget N | --all] [--repo-root PATH]",
|
|
2474
|
-
description: "
|
|
2475
|
-
input:
|
|
2679
|
+
description: "Load the whole base, each record with its standing \u2014 call it first, at the point of use, since compaction drops it. Superseded records arrive as stubs; kb_trace has the history. Over budget it refuses: kb_catalog, then kb_pack, or narrow with `type`; `all` bypasses. Never read record files directly \u2014 only kb_* tools resolve supersession. `digest` stamps the base's content, so hooks know when to reload.",
|
|
2680
|
+
input: import_zod14.z.object({
|
|
2476
2681
|
bundlePath,
|
|
2477
|
-
type:
|
|
2478
|
-
budgetTokens:
|
|
2479
|
-
all:
|
|
2682
|
+
type: import_zod14.z.enum(KB_RECORD_TYPES).optional(),
|
|
2683
|
+
budgetTokens: import_zod14.z.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
|
|
2684
|
+
all: import_zod14.z.boolean().optional().describe(
|
|
2480
2685
|
"Loads the entire base regardless of size, bypassing the token budget; mutually exclusive with budgetTokens."
|
|
2481
2686
|
),
|
|
2482
2687
|
repoRoot: REPO_ROOT
|
|
@@ -2518,25 +2723,25 @@ var loadCommand = define({
|
|
|
2518
2723
|
});
|
|
2519
2724
|
|
|
2520
2725
|
// src/commands/log.ts
|
|
2521
|
-
var
|
|
2726
|
+
var import_zod15 = require("zod");
|
|
2522
2727
|
var logCommand = define({
|
|
2523
2728
|
name: "log",
|
|
2524
2729
|
tool: "kb_log",
|
|
2525
2730
|
usage: "log",
|
|
2526
|
-
description: "
|
|
2527
|
-
input:
|
|
2731
|
+
description: "Who touched what, and when. Append-only; malformed lines are reported, never repaired.",
|
|
2732
|
+
input: import_zod15.z.object({ bundlePath }),
|
|
2528
2733
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
2529
2734
|
run: ({ store }, { bundlePath: path }) => store.readLog(path)
|
|
2530
2735
|
});
|
|
2531
2736
|
|
|
2532
2737
|
// src/commands/no-decision.ts
|
|
2533
|
-
var
|
|
2738
|
+
var import_zod16 = require("zod");
|
|
2534
2739
|
var noDecisionCommand = define({
|
|
2535
2740
|
name: "no-decision",
|
|
2536
2741
|
tool: "kb_no_decision",
|
|
2537
2742
|
usage: "no-decision <reason...>",
|
|
2538
|
-
description:
|
|
2539
|
-
input:
|
|
2743
|
+
description: "Record in one sentence that a piece of work had nothing to decide. Idempotent.",
|
|
2744
|
+
input: import_zod16.z.object({ bundlePath, reason: import_zod16.z.string().min(1) }),
|
|
2540
2745
|
fromArgv: (argv, path) => ({
|
|
2541
2746
|
bundlePath: path,
|
|
2542
2747
|
reason: argv.slice(1).join(" ").trim()
|
|
@@ -2553,20 +2758,20 @@ var noDecisionCommand = define({
|
|
|
2553
2758
|
});
|
|
2554
2759
|
|
|
2555
2760
|
// src/commands/pack.ts
|
|
2556
|
-
var
|
|
2761
|
+
var import_zod17 = require("zod");
|
|
2557
2762
|
var packCommand = define({
|
|
2558
2763
|
name: "pack",
|
|
2559
2764
|
tool: "kb_pack",
|
|
2560
2765
|
usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
|
|
2561
|
-
description: "
|
|
2562
|
-
input:
|
|
2766
|
+
description: "Bounded neighbourhood around one record: within `hops`, ranked, cut to `maxNodes`, with every cut record named under Excluded. Use when the base is over kb_load's budget and the work centres on a record you can name. Refuses over budget rather than truncating. Everything below the header is byte-stable across runs. Resolves supersession like kb_load.",
|
|
2767
|
+
input: import_zod17.z.object({
|
|
2563
2768
|
bundlePath,
|
|
2564
2769
|
conceptId,
|
|
2565
|
-
hops:
|
|
2566
|
-
maxNodes:
|
|
2770
|
+
hops: import_zod17.z.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
|
|
2771
|
+
maxNodes: import_zod17.z.number().int().positive().optional().describe(
|
|
2567
2772
|
"How many records the pack may hold, root included. Defaults to 20."
|
|
2568
2773
|
),
|
|
2569
|
-
budgetTokens:
|
|
2774
|
+
budgetTokens: import_zod17.z.number().int().positive().optional().describe(
|
|
2570
2775
|
"Approximate token ceiling over what is actually emitted. Defaults to 25000."
|
|
2571
2776
|
)
|
|
2572
2777
|
}),
|
|
@@ -2653,22 +2858,22 @@ function warningLabel(warning) {
|
|
|
2653
2858
|
}
|
|
2654
2859
|
|
|
2655
2860
|
// src/commands/pin.ts
|
|
2656
|
-
var
|
|
2861
|
+
var import_zod18 = require("zod");
|
|
2657
2862
|
var pinCommand = define({
|
|
2658
2863
|
name: "pin",
|
|
2659
2864
|
tool: "kb_pin",
|
|
2660
2865
|
usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
|
|
2661
|
-
description: "Pin a base into a workspace
|
|
2662
|
-
input:
|
|
2866
|
+
description: "Pin a base into a workspace manifest so kb_context surfaces it. Layers, nearest wins: project `.strauss/kb-pins.json` (default), `--local` (personal, gitignored), `--user` (`~/.strauss`). Idempotent; `--mode full|index`, `--profiles`, `--frozen`/`--unfreeze` update only those fields. A path with no records pins with a warning. Never touches the base itself.",
|
|
2867
|
+
input: import_zod18.z.object({
|
|
2663
2868
|
bundlePath,
|
|
2664
|
-
mode:
|
|
2869
|
+
mode: import_zod18.z.enum(["full", "index"]).optional().describe(
|
|
2665
2870
|
"full: always emit this base's records whole (still under the block budget); index: never upgrade. Absent: the profile's full-under threshold decides."
|
|
2666
2871
|
),
|
|
2667
|
-
profiles:
|
|
2668
|
-
layer:
|
|
2872
|
+
profiles: import_zod18.z.array(import_zod18.z.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
|
|
2873
|
+
layer: import_zod18.z.enum(["project", "local", "user"]).optional().describe(
|
|
2669
2874
|
"Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
|
|
2670
2875
|
),
|
|
2671
|
-
frozen:
|
|
2876
|
+
frozen: import_zod18.z.boolean().optional().describe(
|
|
2672
2877
|
"true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
|
|
2673
2878
|
)
|
|
2674
2879
|
}),
|
|
@@ -2697,29 +2902,29 @@ var pinCommand = define({
|
|
|
2697
2902
|
});
|
|
2698
2903
|
|
|
2699
2904
|
// src/commands/pins.ts
|
|
2700
|
-
var
|
|
2905
|
+
var import_zod19 = require("zod");
|
|
2701
2906
|
var pinsCommand = define({
|
|
2702
2907
|
name: "pins",
|
|
2703
2908
|
tool: "kb_pins",
|
|
2704
2909
|
usage: "pins",
|
|
2705
|
-
description: "Every pinned base across the manifest layers,
|
|
2706
|
-
input:
|
|
2910
|
+
description: "Every pinned base across the manifest layers, with its layer and whether it resolves to records. Takes no bundlePath.",
|
|
2911
|
+
input: import_zod19.z.object({}),
|
|
2707
2912
|
fromArgv: () => ({}),
|
|
2708
2913
|
run: ({ store }) => listPins(store, process.cwd())
|
|
2709
2914
|
});
|
|
2710
2915
|
|
|
2711
2916
|
// src/commands/query.ts
|
|
2712
|
-
var
|
|
2917
|
+
var import_zod20 = require("zod");
|
|
2713
2918
|
var queryCommand = define({
|
|
2714
2919
|
name: "query",
|
|
2715
2920
|
tool: "kb_query",
|
|
2716
2921
|
usage: "query <text...> [--repo-root PATH]",
|
|
2717
2922
|
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.",
|
|
2718
|
-
input:
|
|
2923
|
+
input: import_zod20.z.object({
|
|
2719
2924
|
bundlePath,
|
|
2720
|
-
text:
|
|
2721
|
-
type:
|
|
2722
|
-
includeNonCurrent:
|
|
2925
|
+
text: import_zod20.z.string().optional(),
|
|
2926
|
+
type: import_zod20.z.enum(KB_RECORD_TYPES).optional(),
|
|
2927
|
+
includeNonCurrent: import_zod20.z.boolean().optional(),
|
|
2723
2928
|
repoRoot: REPO_ROOT
|
|
2724
2929
|
}),
|
|
2725
2930
|
// `--repo-root` is a flag, so its value must not fall into the search text.
|
|
@@ -2751,27 +2956,27 @@ var queryCommand = define({
|
|
|
2751
2956
|
});
|
|
2752
2957
|
|
|
2753
2958
|
// src/commands/read-index.ts
|
|
2754
|
-
var
|
|
2959
|
+
var import_zod21 = require("zod");
|
|
2755
2960
|
var readIndexCommand = define({
|
|
2756
2961
|
name: "index",
|
|
2757
2962
|
tool: "kb_index",
|
|
2758
2963
|
usage: "index",
|
|
2759
|
-
description: "The index
|
|
2760
|
-
input:
|
|
2964
|
+
description: "The index \u2014 title, type, status, description per record \u2014 rebuilt if stale. Cheapest re-orientation after compaction: call it (or kb_context) first, then kb_load or fetch by id.",
|
|
2965
|
+
input: import_zod21.z.object({ bundlePath }),
|
|
2761
2966
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
2762
2967
|
run: ({ store }, { bundlePath: path }) => store.readIndex(path)
|
|
2763
2968
|
});
|
|
2764
2969
|
|
|
2765
2970
|
// src/commands/schema.ts
|
|
2766
|
-
var
|
|
2971
|
+
var import_zod24 = require("zod");
|
|
2767
2972
|
|
|
2768
2973
|
// src/json-schema.ts
|
|
2769
|
-
var
|
|
2974
|
+
var import_zod23 = require("zod");
|
|
2770
2975
|
|
|
2771
2976
|
// src/kb-log.ts
|
|
2772
|
-
var
|
|
2977
|
+
var import_zod22 = require("zod");
|
|
2773
2978
|
var LOG_FILE = "log.jsonl";
|
|
2774
|
-
var kbLogEntrySchema =
|
|
2979
|
+
var kbLogEntrySchema = import_zod22.z.object({
|
|
2775
2980
|
// Validated, not just `min(1)`: `at` is a sort key (see `parseLog`
|
|
2776
2981
|
// below), and a value that isn't actually chronological — a Unix
|
|
2777
2982
|
// timestamp, a human-typed date, garbage — would sort wrong without
|
|
@@ -2780,12 +2985,12 @@ var kbLogEntrySchema = import_zod20.z.object({
|
|
|
2780
2985
|
// and rejects everything else, including a non-`Z` offset — so a
|
|
2781
2986
|
// malformed `at` is reported the same way a malformed line already is,
|
|
2782
2987
|
// rather than silently sorting into the wrong place.
|
|
2783
|
-
at:
|
|
2784
|
-
by:
|
|
2785
|
-
operation:
|
|
2786
|
-
conceptId:
|
|
2988
|
+
at: import_zod22.z.iso.datetime(),
|
|
2989
|
+
by: import_zod22.z.string().min(1),
|
|
2990
|
+
operation: import_zod22.z.string().min(1),
|
|
2991
|
+
conceptId: import_zod22.z.string().min(1),
|
|
2787
2992
|
/** Second concept id, where the operation relates two — supersession. */
|
|
2788
|
-
target:
|
|
2993
|
+
target: import_zod22.z.string().min(1).optional()
|
|
2789
2994
|
}).strict();
|
|
2790
2995
|
function renderLogEntry(entry) {
|
|
2791
2996
|
return `${JSON.stringify(kbLogEntrySchema.parse(entry))}
|
|
@@ -2823,11 +3028,11 @@ function parseLog(raw) {
|
|
|
2823
3028
|
// src/json-schema.ts
|
|
2824
3029
|
function kbJsonSchemas() {
|
|
2825
3030
|
return {
|
|
2826
|
-
recordFrontmatter:
|
|
3031
|
+
recordFrontmatter: import_zod23.z.toJSONSchema(kbRecordFrontmatterSchema, {
|
|
2827
3032
|
io: "input"
|
|
2828
3033
|
}),
|
|
2829
|
-
composeInput:
|
|
2830
|
-
logEntry:
|
|
3034
|
+
composeInput: import_zod23.z.toJSONSchema(composeInputSchema, { io: "input" }),
|
|
3035
|
+
logEntry: import_zod23.z.toJSONSchema(kbLogEntrySchema, { io: "input" })
|
|
2831
3036
|
};
|
|
2832
3037
|
}
|
|
2833
3038
|
|
|
@@ -2836,23 +3041,23 @@ var schemaCommand = define({
|
|
|
2836
3041
|
name: "schema",
|
|
2837
3042
|
tool: "kb_schema",
|
|
2838
3043
|
usage: "schema",
|
|
2839
|
-
description: "JSON Schema for
|
|
2840
|
-
input:
|
|
3044
|
+
description: "JSON Schema for frontmatter, write input, and log entries, generated from the enforcing code.",
|
|
3045
|
+
input: import_zod24.z.object({}),
|
|
2841
3046
|
fromArgv: () => ({}),
|
|
2842
3047
|
run: () => Promise.resolve(kbJsonSchemas())
|
|
2843
3048
|
});
|
|
2844
3049
|
|
|
2845
3050
|
// src/commands/status.ts
|
|
2846
|
-
var
|
|
3051
|
+
var import_zod25 = require("zod");
|
|
2847
3052
|
var statusCommand = define({
|
|
2848
3053
|
name: "status",
|
|
2849
3054
|
tool: "kb_status",
|
|
2850
3055
|
usage: "status <concept-id> <status>",
|
|
2851
|
-
description: "Move a record's status
|
|
2852
|
-
input:
|
|
3056
|
+
description: "Move a record's status. Compare-and-swap: a concurrent change fails instead of being overwritten.",
|
|
3057
|
+
input: import_zod25.z.object({
|
|
2853
3058
|
bundlePath,
|
|
2854
3059
|
conceptId,
|
|
2855
|
-
status:
|
|
3060
|
+
status: import_zod25.z.enum(KB_RECORD_STATUSES)
|
|
2856
3061
|
}),
|
|
2857
3062
|
fromArgv: (argv, path) => ({
|
|
2858
3063
|
bundlePath: path,
|
|
@@ -2867,13 +3072,13 @@ var statusCommand = define({
|
|
|
2867
3072
|
});
|
|
2868
3073
|
|
|
2869
3074
|
// src/commands/supersede.ts
|
|
2870
|
-
var
|
|
3075
|
+
var import_zod26 = require("zod");
|
|
2871
3076
|
var supersedeCommand = define({
|
|
2872
3077
|
name: "supersede",
|
|
2873
3078
|
tool: "kb_supersede",
|
|
2874
3079
|
usage: "supersede <concept-id> <replacement-id>",
|
|
2875
|
-
description: "Mark a record superseded by another,
|
|
2876
|
-
input:
|
|
3080
|
+
description: "Mark a record superseded by another, linked in both directions. Use instead of editing a record whose meaning changed.",
|
|
3081
|
+
input: import_zod26.z.object({ bundlePath, conceptId, replacementId: conceptId }),
|
|
2877
3082
|
fromArgv: (argv, path) => ({
|
|
2878
3083
|
bundlePath: path,
|
|
2879
3084
|
conceptId: argv[1],
|
|
@@ -2887,16 +3092,16 @@ var supersedeCommand = define({
|
|
|
2887
3092
|
});
|
|
2888
3093
|
|
|
2889
3094
|
// src/commands/sync-instructions.ts
|
|
2890
|
-
var
|
|
3095
|
+
var import_zod27 = require("zod");
|
|
2891
3096
|
var syncInstructionsCommand = define({
|
|
2892
3097
|
name: "sync-instructions",
|
|
2893
3098
|
usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
|
|
2894
|
-
description: "
|
|
2895
|
-
input:
|
|
2896
|
-
file:
|
|
2897
|
-
budgetTokens:
|
|
2898
|
-
fullUnderTokens:
|
|
2899
|
-
profile:
|
|
3099
|
+
description: "CLI-only: plant the kb_context block between sentinel comments in AGENTS.md or CLAUDE.md, idempotently.",
|
|
3100
|
+
input: import_zod27.z.object({
|
|
3101
|
+
file: import_zod27.z.string().min(1).describe("The instruction file to edit in place."),
|
|
3102
|
+
budgetTokens: import_zod27.z.number().int().positive().optional(),
|
|
3103
|
+
fullUnderTokens: import_zod27.z.number().int().positive().optional(),
|
|
3104
|
+
profile: import_zod27.z.string().optional()
|
|
2900
3105
|
}),
|
|
2901
3106
|
fromArgv: (argv) => {
|
|
2902
3107
|
const budget = argvFlag(argv, "--budget");
|
|
@@ -2922,10 +3127,15 @@ var syncInstructionsCommand = define({
|
|
|
2922
3127
|
});
|
|
2923
3128
|
|
|
2924
3129
|
// src/commands/trace.ts
|
|
2925
|
-
var
|
|
3130
|
+
var import_zod28 = require("zod");
|
|
2926
3131
|
|
|
2927
3132
|
// src/trace.ts
|
|
2928
|
-
var TRACE_EDGES = [
|
|
3133
|
+
var TRACE_EDGES = [
|
|
3134
|
+
"typed-link",
|
|
3135
|
+
"supersession",
|
|
3136
|
+
"anchor",
|
|
3137
|
+
"source"
|
|
3138
|
+
];
|
|
2929
3139
|
function trace(seedId, bundle, options = {}) {
|
|
2930
3140
|
const edges = options.edges?.length ? options.edges : TRACE_EDGES;
|
|
2931
3141
|
const maxDepth = options.depth ?? 3;
|
|
@@ -2940,7 +3150,12 @@ function trace(seedId, bundle, options = {}) {
|
|
|
2940
3150
|
const next = [];
|
|
2941
3151
|
for (const from of frontier) {
|
|
2942
3152
|
for (const edge of edges) {
|
|
2943
|
-
for (const record of edgeNeighbours(
|
|
3153
|
+
for (const record of edgeNeighbours(
|
|
3154
|
+
from,
|
|
3155
|
+
bundle,
|
|
3156
|
+
edge,
|
|
3157
|
+
KB_CAUSAL_LINK_RELS
|
|
3158
|
+
)) {
|
|
2944
3159
|
const existing = reached.get(record.conceptId);
|
|
2945
3160
|
if (existing) {
|
|
2946
3161
|
if (existing.depth > 0 && !existing.via.includes(edge)) {
|
|
@@ -2967,12 +3182,12 @@ var traceCommand = define({
|
|
|
2967
3182
|
name: "trace",
|
|
2968
3183
|
tool: "kb_trace",
|
|
2969
3184
|
usage: "trace <concept-id> [edges...]",
|
|
2970
|
-
description: '
|
|
2971
|
-
input:
|
|
3185
|
+
description: 'Timeline of how a position was reached, ordered by write time, following supersession, shared anchors and shared sources. Includes rejected, draft and superseded records \u2014 in a history they are the content. For "why is it like this"; kb_load answers "what holds now".',
|
|
3186
|
+
input: import_zod28.z.object({
|
|
2972
3187
|
bundlePath,
|
|
2973
3188
|
conceptId,
|
|
2974
|
-
edges:
|
|
2975
|
-
depth:
|
|
3189
|
+
edges: import_zod28.z.array(import_zod28.z.enum(TRACE_EDGES)).optional(),
|
|
3190
|
+
depth: import_zod28.z.number().int().positive().optional()
|
|
2976
3191
|
}),
|
|
2977
3192
|
fromArgv: (argv, path) => ({
|
|
2978
3193
|
bundlePath: path,
|
|
@@ -2994,53 +3209,56 @@ var traceCommand = define({
|
|
|
2994
3209
|
});
|
|
2995
3210
|
|
|
2996
3211
|
// src/commands/types.ts
|
|
2997
|
-
var
|
|
3212
|
+
var import_zod29 = require("zod");
|
|
2998
3213
|
var typesCommand = define({
|
|
2999
3214
|
name: "types",
|
|
3000
3215
|
tool: "kb_types",
|
|
3001
3216
|
usage: "types",
|
|
3002
3217
|
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.",
|
|
3003
|
-
input:
|
|
3218
|
+
input: import_zod29.z.object({}),
|
|
3004
3219
|
fromArgv: () => ({}),
|
|
3005
3220
|
run: () => Promise.resolve(RECORD_TYPES)
|
|
3006
3221
|
});
|
|
3007
3222
|
|
|
3008
3223
|
// src/commands/unpin.ts
|
|
3009
|
-
var
|
|
3224
|
+
var import_zod30 = require("zod");
|
|
3010
3225
|
var unpinCommand = define({
|
|
3011
3226
|
name: "unpin",
|
|
3012
3227
|
tool: "kb_unpin",
|
|
3013
3228
|
usage: "unpin [bundle-path]",
|
|
3014
|
-
description: "Remove a base from every
|
|
3015
|
-
input:
|
|
3229
|
+
description: "Remove a base from every manifest layer that holds it. Reports the layers touched.",
|
|
3230
|
+
input: import_zod30.z.object({ bundlePath }),
|
|
3016
3231
|
fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
|
|
3017
3232
|
run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
|
|
3018
3233
|
});
|
|
3019
3234
|
|
|
3020
3235
|
// src/commands/validate.ts
|
|
3021
|
-
var
|
|
3236
|
+
var import_zod31 = require("zod");
|
|
3022
3237
|
var validateCommand = define({
|
|
3023
3238
|
name: "validate",
|
|
3024
3239
|
tool: "kb_validate",
|
|
3025
3240
|
usage: "validate",
|
|
3026
|
-
description: "Check pointers no single record can see: supersession links that disagree between the two records, and assumptions that cite sources.
|
|
3027
|
-
input:
|
|
3241
|
+
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.",
|
|
3242
|
+
input: import_zod31.z.object({ bundlePath }),
|
|
3028
3243
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
3029
3244
|
run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
|
|
3030
|
-
|
|
3245
|
+
// Warnings never fail the exit code; every other severity does.
|
|
3246
|
+
failsWhen: (result) => Array.isArray(result) && result.some(
|
|
3247
|
+
(problem) => problem.severity !== "warning"
|
|
3248
|
+
)
|
|
3031
3249
|
});
|
|
3032
3250
|
|
|
3033
3251
|
// src/commands/verify.ts
|
|
3034
|
-
var
|
|
3252
|
+
var import_zod32 = require("zod");
|
|
3035
3253
|
var verifyCommand = define({
|
|
3036
3254
|
name: "verify",
|
|
3037
3255
|
tool: "kb_verify",
|
|
3038
3256
|
usage: "verify <concept-id> --note <text>",
|
|
3039
|
-
description: "Append
|
|
3040
|
-
input:
|
|
3257
|
+
description: "Append a verified[] event: who checked, when, and what was found. Append-only. A record's own generator is refused unless the actor is `human:`-prefixed.",
|
|
3258
|
+
input: import_zod32.z.object({
|
|
3041
3259
|
bundlePath,
|
|
3042
3260
|
conceptId,
|
|
3043
|
-
note:
|
|
3261
|
+
note: import_zod32.z.string().refine((s) => s.trim().length > 0, {
|
|
3044
3262
|
message: "note must say what the check found"
|
|
3045
3263
|
})
|
|
3046
3264
|
}),
|
|
@@ -3060,23 +3278,15 @@ var verifyCommand = define({
|
|
|
3060
3278
|
});
|
|
3061
3279
|
|
|
3062
3280
|
// src/commands/write.ts
|
|
3063
|
-
var
|
|
3281
|
+
var import_zod33 = require("zod");
|
|
3064
3282
|
var writeCommand = define({
|
|
3065
3283
|
name: "write",
|
|
3066
3284
|
tool: "kb_write",
|
|
3067
3285
|
usage: "write <type> < record.json",
|
|
3068
|
-
description:
|
|
3069
|
-
|
|
3070
|
-
"",
|
|
3071
|
-
"Judgment the tool cannot enforce for you:",
|
|
3072
|
-
"- An unsourced claim is an `assumption` record with assumption: true, never a `fact` with a vague source. The distinction is what lets a later reader separate what was established from what was guessed.",
|
|
3073
|
-
"- When two records conflict, say so in a `risk`, an `open-question`, or a superseding `decision`. Quietly picking a winner destroys the disagreement, which is usually the useful part.",
|
|
3074
|
-
"- Prefer a new record over overloading an existing one, and keep each short. A record nobody finishes reading is not durable memory.",
|
|
3075
|
-
"- Records are never deleted; supersede instead, so the earlier reasoning stays inspectable."
|
|
3076
|
-
].join("\n"),
|
|
3077
|
-
input: import_zod31.z.object({
|
|
3286
|
+
description: "Write one record. Search first \u2014 a duplicate concept id is rejected, not overwritten; kb_types lists each type's sections. An unsourced claim is an `assumption` with assumption: true, never a vague `fact`. Conflicting records get a `risk`, `open-question`, or superseding `decision`. Prefer a new short record over overloading one. Never delete; supersede.",
|
|
3287
|
+
input: import_zod33.z.object({
|
|
3078
3288
|
bundlePath,
|
|
3079
|
-
type:
|
|
3289
|
+
type: import_zod33.z.enum(KB_RECORD_TYPES),
|
|
3080
3290
|
input: composeInputSchema
|
|
3081
3291
|
}),
|
|
3082
3292
|
fromArgv: async (argv, path, stdin) => ({
|
|
@@ -3100,20 +3310,13 @@ var writeCommand = define({
|
|
|
3100
3310
|
});
|
|
3101
3311
|
|
|
3102
3312
|
// src/commands/write-decision.ts
|
|
3103
|
-
var
|
|
3313
|
+
var import_zod34 = require("zod");
|
|
3104
3314
|
var writeDecisionCommand = define({
|
|
3105
3315
|
name: "write-decision",
|
|
3106
3316
|
tool: "kb_write_decision",
|
|
3107
3317
|
usage: "write-decision < decision.json",
|
|
3108
|
-
description:
|
|
3109
|
-
|
|
3110
|
-
"",
|
|
3111
|
-
"What belongs in one:",
|
|
3112
|
-
'- Record a decision when a later reader would otherwise "simplify" the constraint away. If the diff already answers the question, there is nothing here to write.',
|
|
3113
|
-
"- `alternative` is what you turned down and why, not a list of everything considered.",
|
|
3114
|
-
"- A reference to material you read goes in `sources`; a reference to code goes in `anchors`; a reference to another record goes in `relatedConceptIds`."
|
|
3115
|
-
].join("\n"),
|
|
3116
|
-
input: import_zod32.z.object({ bundlePath, input: decisionInputSchema }),
|
|
3318
|
+
description: "Write a decision, with `alternative` (what was rejected and why) and `impact` as fields. Record one when a later reader would otherwise simplify the constraint away; skip when the diff already answers it. `sources` for material read, `anchors` for code, `relatedConceptIds` for records.",
|
|
3319
|
+
input: import_zod34.z.object({ bundlePath, input: decisionInputSchema }),
|
|
3117
3320
|
fromArgv: async (_argv, path, stdin) => ({
|
|
3118
3321
|
bundlePath: path,
|
|
3119
3322
|
input: JSON.parse(await stdin())
|
|
@@ -3148,6 +3351,8 @@ var KB_COMMANDS = [
|
|
|
3148
3351
|
packCommand,
|
|
3149
3352
|
queryCommand,
|
|
3150
3353
|
traceCommand,
|
|
3354
|
+
impactCommand,
|
|
3355
|
+
backlinksCommand,
|
|
3151
3356
|
listCommand,
|
|
3152
3357
|
readIndexCommand,
|
|
3153
3358
|
logCommand,
|
|
@@ -3368,6 +3573,141 @@ function typeRank(record) {
|
|
|
3368
3573
|
return index === -1 ? TYPE_PRIORITY.length : index;
|
|
3369
3574
|
}
|
|
3370
3575
|
|
|
3576
|
+
// src/kb-links/inbound.ts
|
|
3577
|
+
function inboundIndex(bundle) {
|
|
3578
|
+
const byTarget = /* @__PURE__ */ new Map();
|
|
3579
|
+
for (const record of bundle) {
|
|
3580
|
+
for (const link2 of record.frontmatter.strauss_links ?? []) {
|
|
3581
|
+
if (link2.target === record.conceptId) continue;
|
|
3582
|
+
const edges = byTarget.get(link2.target) ?? [];
|
|
3583
|
+
if (edges.some(
|
|
3584
|
+
(edge) => edge.from === record.conceptId && edge.rel === link2.rel
|
|
3585
|
+
)) {
|
|
3586
|
+
continue;
|
|
3587
|
+
}
|
|
3588
|
+
edges.push({ from: record.conceptId, rel: link2.rel });
|
|
3589
|
+
byTarget.set(link2.target, edges);
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
3592
|
+
return byTarget;
|
|
3593
|
+
}
|
|
3594
|
+
|
|
3595
|
+
// src/kb-links/backlinks.ts
|
|
3596
|
+
function backlinks(targetId, bundle) {
|
|
3597
|
+
const byId = new Map(bundle.map((record) => [record.conceptId, record]));
|
|
3598
|
+
if (!byId.has(targetId)) throw new KbRecordNotFoundError(targetId);
|
|
3599
|
+
const standingOf = new Map(
|
|
3600
|
+
adjudicate(bundle, bundle).map((hit) => [hit.record.conceptId, hit])
|
|
3601
|
+
);
|
|
3602
|
+
const rows = [];
|
|
3603
|
+
for (const edge of inboundIndex(bundle).get(targetId) ?? []) {
|
|
3604
|
+
const record = byId.get(edge.from);
|
|
3605
|
+
if (!record) continue;
|
|
3606
|
+
const hit = standingOf.get(edge.from);
|
|
3607
|
+
rows.push({
|
|
3608
|
+
...edge,
|
|
3609
|
+
title: record.frontmatter.title ?? null,
|
|
3610
|
+
standing: hit?.standing ?? "unsettled",
|
|
3611
|
+
warnings: hit?.warnings ?? []
|
|
3612
|
+
});
|
|
3613
|
+
}
|
|
3614
|
+
return {
|
|
3615
|
+
target: targetId,
|
|
3616
|
+
backlinks: rows.sort(
|
|
3617
|
+
(left, right) => left.from.localeCompare(right.from) || left.rel.localeCompare(right.rel)
|
|
3618
|
+
)
|
|
3619
|
+
};
|
|
3620
|
+
}
|
|
3621
|
+
|
|
3622
|
+
// src/kb-links/impact.ts
|
|
3623
|
+
function impact(targetId, bundle, options = {}) {
|
|
3624
|
+
const byId = new Map(bundle.map((record) => [record.conceptId, record]));
|
|
3625
|
+
if (!byId.has(targetId)) throw new KbRecordNotFoundError(targetId);
|
|
3626
|
+
const rels = resolveRels(options.rels);
|
|
3627
|
+
const maxDepth = options.depth ?? Number.POSITIVE_INFINITY;
|
|
3628
|
+
const inbound = inboundIndex(bundle);
|
|
3629
|
+
const standingOf = new Map(
|
|
3630
|
+
adjudicate(bundle, bundle).map((hit) => [hit.record.conceptId, hit])
|
|
3631
|
+
);
|
|
3632
|
+
const reached = /* @__PURE__ */ new Map();
|
|
3633
|
+
const stopped = [];
|
|
3634
|
+
let frontier = [targetId];
|
|
3635
|
+
let depth = 0;
|
|
3636
|
+
while (frontier.length && depth < maxDepth) {
|
|
3637
|
+
depth += 1;
|
|
3638
|
+
const next = [];
|
|
3639
|
+
const consider = (dependantId, edge) => {
|
|
3640
|
+
if (dependantId === targetId) return;
|
|
3641
|
+
const existing = reached.get(dependantId);
|
|
3642
|
+
if (existing) {
|
|
3643
|
+
if (!hasEdge(existing.via, edge)) existing.via.push(edge);
|
|
3644
|
+
return;
|
|
3645
|
+
}
|
|
3646
|
+
const record = byId.get(dependantId);
|
|
3647
|
+
if (!record) return;
|
|
3648
|
+
const hit = standingOf.get(dependantId);
|
|
3649
|
+
const entry = {
|
|
3650
|
+
conceptId: dependantId,
|
|
3651
|
+
title: record.frontmatter.title ?? null,
|
|
3652
|
+
standing: hit?.standing ?? "unsettled",
|
|
3653
|
+
warnings: hit?.warnings ?? [],
|
|
3654
|
+
depth,
|
|
3655
|
+
via: [edge]
|
|
3656
|
+
};
|
|
3657
|
+
reached.set(dependantId, entry);
|
|
3658
|
+
if (entry.standing === "superseded" || entry.standing === "rejected") {
|
|
3659
|
+
stopped.push(dependantId);
|
|
3660
|
+
return;
|
|
3661
|
+
}
|
|
3662
|
+
next.push(dependantId);
|
|
3663
|
+
};
|
|
3664
|
+
for (const id of frontier) {
|
|
3665
|
+
for (const edge of inbound.get(id) ?? []) {
|
|
3666
|
+
if (!rels.has(edge.rel)) continue;
|
|
3667
|
+
if (dependantEnd(edge.rel) !== "source") continue;
|
|
3668
|
+
consider(edge.from, { source: edge.from, target: id, rel: edge.rel });
|
|
3669
|
+
}
|
|
3670
|
+
for (const link2 of byId.get(id)?.frontmatter.strauss_links ?? []) {
|
|
3671
|
+
if (!rels.has(link2.rel)) continue;
|
|
3672
|
+
if (dependantEnd(link2.rel) !== "target") continue;
|
|
3673
|
+
if (link2.target === id) continue;
|
|
3674
|
+
consider(link2.target, {
|
|
3675
|
+
source: id,
|
|
3676
|
+
target: link2.target,
|
|
3677
|
+
rel: link2.rel
|
|
3678
|
+
});
|
|
3679
|
+
}
|
|
3680
|
+
}
|
|
3681
|
+
frontier = next;
|
|
3682
|
+
}
|
|
3683
|
+
return {
|
|
3684
|
+
root: targetId,
|
|
3685
|
+
impacted: [...reached.values()].sort(
|
|
3686
|
+
(left, right) => left.depth - right.depth || left.conceptId.localeCompare(right.conceptId)
|
|
3687
|
+
),
|
|
3688
|
+
stopped: stopped.sort(),
|
|
3689
|
+
truncated: frontier.length > 0,
|
|
3690
|
+
unexpanded: [...frontier].sort()
|
|
3691
|
+
};
|
|
3692
|
+
}
|
|
3693
|
+
function resolveRels(rels) {
|
|
3694
|
+
if (!rels?.length) return new Set(KB_CAUSAL_LINK_RELS);
|
|
3695
|
+
for (const rel of rels) {
|
|
3696
|
+
if (!isKbLinkRel(rel) || LINK_RELS[rel].dependant === null) {
|
|
3697
|
+
throw new KbUnknownLinkRelError(rel, KB_CAUSAL_LINK_RELS);
|
|
3698
|
+
}
|
|
3699
|
+
}
|
|
3700
|
+
return new Set(rels);
|
|
3701
|
+
}
|
|
3702
|
+
function dependantEnd(rel) {
|
|
3703
|
+
return isKbLinkRel(rel) ? LINK_RELS[rel].dependant : null;
|
|
3704
|
+
}
|
|
3705
|
+
function hasEdge(edges, edge) {
|
|
3706
|
+
return edges.some(
|
|
3707
|
+
(existing) => existing.source === edge.source && existing.target === edge.target && existing.rel === edge.rel
|
|
3708
|
+
);
|
|
3709
|
+
}
|
|
3710
|
+
|
|
3371
3711
|
// src/kb-gitattributes.ts
|
|
3372
3712
|
var GITATTRIBUTES_FILE = ".gitattributes";
|
|
3373
3713
|
var UNION_MERGE_LINE = `${LOG_FILE} text eol=lf merge=union`;
|
|
@@ -3771,6 +4111,14 @@ ${answer}
|
|
|
3771
4111
|
async pack(bundlePath2, rootId, options = {}) {
|
|
3772
4112
|
return pack(await this.list(bundlePath2), rootId, options);
|
|
3773
4113
|
}
|
|
4114
|
+
/** What breaks if this record changes. See `kb-links/impact.ts`. */
|
|
4115
|
+
async impact(bundlePath2, targetId, options = {}) {
|
|
4116
|
+
return impact(targetId, await this.list(bundlePath2), options);
|
|
4117
|
+
}
|
|
4118
|
+
/** Who points at this record, one hop. See `kb-links/backlinks.ts`. */
|
|
4119
|
+
async backlinks(bundlePath2, targetId) {
|
|
4120
|
+
return backlinks(targetId, await this.list(bundlePath2));
|
|
4121
|
+
}
|
|
3774
4122
|
/**
|
|
3775
4123
|
* The stored index, rebuilt if it disagrees with the records.
|
|
3776
4124
|
*
|
|
@@ -4091,7 +4439,7 @@ function bundleDigest(records, superseded) {
|
|
|
4091
4439
|
}
|
|
4092
4440
|
|
|
4093
4441
|
// src/version.ts
|
|
4094
|
-
var VERSION = true ? "0.1.
|
|
4442
|
+
var VERSION = true ? "0.1.14" : "0.0.0-dev";
|
|
4095
4443
|
|
|
4096
4444
|
// src/mcp.ts
|
|
4097
4445
|
function createKbMcpServer() {
|