@saasontools/strauss-kb 0.1.20 → 0.1.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -8
- package/dist/{chunk-QQLPJO4R.js → chunk-AEO5U42S.js} +2407 -2131
- package/dist/chunk-AEO5U42S.js.map +1 -0
- package/dist/{chunk-XDH6J6CH.js → chunk-DVSPHL4B.js} +2 -2
- package/dist/{chunk-EXZQZJU2.js → chunk-TSE4YQEG.js} +2 -2
- package/dist/cli-main.cjs +1386 -1120
- package/dist/cli-main.cjs.map +1 -1
- package/dist/cli-main.js +2 -2
- package/dist/index.cjs +497 -212
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +295 -17
- package/dist/index.d.ts +295 -17
- package/dist/index.js +17 -3
- package/dist/mcp-main.cjs +1386 -1120
- package/dist/mcp-main.cjs.map +1 -1
- package/dist/mcp-main.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-QQLPJO4R.js.map +0 -1
- /package/dist/{chunk-XDH6J6CH.js.map → chunk-DVSPHL4B.js.map} +0 -0
- /package/dist/{chunk-EXZQZJU2.js.map → chunk-TSE4YQEG.js.map} +0 -0
package/dist/mcp-main.cjs
CHANGED
|
@@ -32,617 +32,139 @@ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
|
32
32
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
33
33
|
|
|
34
34
|
// src/decision-record.ts
|
|
35
|
-
var
|
|
35
|
+
var import_zod4 = require("zod");
|
|
36
36
|
|
|
37
37
|
// src/compose.ts
|
|
38
|
-
var
|
|
38
|
+
var import_zod3 = require("zod");
|
|
39
39
|
|
|
40
|
-
// src/
|
|
41
|
-
var
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
last_modified: import_zod.z.string().min(1).optional()
|
|
48
|
-
}).passthrough();
|
|
49
|
-
var kbActorStampSchema = import_zod.z.object({
|
|
50
|
-
by: import_zod.z.string().min(1),
|
|
51
|
-
at: import_zod.z.string().min(1)
|
|
52
|
-
}).passthrough();
|
|
53
|
-
var kbVerifiedEventSchema = kbActorStampSchema.extend({
|
|
54
|
-
note: import_zod.z.string().refine((s) => s.trim().length > 0, {
|
|
55
|
-
message: "note must say what the check found"
|
|
56
|
-
})
|
|
57
|
-
});
|
|
58
|
-
var kbAnchorSpanSchema = import_zod.z.object({
|
|
59
|
-
start: import_zod.z.number().int().positive(),
|
|
60
|
-
end: import_zod.z.number().int().positive()
|
|
61
|
-
}).strict();
|
|
62
|
-
var kbAnchorSchema = import_zod.z.object({
|
|
63
|
-
file: import_zod.z.string().min(1),
|
|
64
|
-
symbol: import_zod.z.string().min(1).optional(),
|
|
65
|
-
/**
|
|
66
|
-
* The lines the concept names, when no symbol covers them — deleted code,
|
|
67
|
-
* YAML, SQL, Markdown. Alternative to `symbol`, never a refinement of it.
|
|
68
|
-
*/
|
|
69
|
-
span: kbAnchorSpanSchema.optional(),
|
|
70
|
-
/**
|
|
71
|
-
* Which side of the change the anchor describes. `old` is code as it was
|
|
72
|
-
* committed at `ref`, which is the only way to anchor something deleted;
|
|
73
|
-
* absent means the working tree.
|
|
74
|
-
*/
|
|
75
|
-
side: import_zod.z.enum(["old", "new"]).optional(),
|
|
76
|
-
/**
|
|
77
|
-
* Which repository the file lives in — a remote URL
|
|
78
|
-
* (`https://github.com/org/name`) or a short name. Absent means the base's
|
|
79
|
-
* own repository, which is what nearly every anchor means.
|
|
80
|
-
*
|
|
81
|
-
* Unvalidated beyond not-blank: one repository has many spellings, matched
|
|
82
|
-
* after normalisation. Only a full URL can be fetched from, so `validate`
|
|
83
|
-
* warns on a short one; see ARCHITECTURE.
|
|
84
|
-
*/
|
|
85
|
-
repo: import_zod.z.string().trim().min(1).optional(),
|
|
86
|
-
/**
|
|
87
|
-
* The git rev the evidence was taken at. Prefer a commit SHA: a branch
|
|
88
|
-
* name is a moving pointer, so an anchor pinned to one says the evidence
|
|
89
|
-
* came from wherever that branch happens to be now, which is not a
|
|
90
|
-
* baseline. A foreign anchor is checked at this rev, and compared against
|
|
91
|
-
* the remote's default branch on top of it.
|
|
92
|
-
*/
|
|
93
|
-
ref: import_zod.z.string().trim().min(1).optional(),
|
|
94
|
-
hash: import_zod.z.string().regex(/^sha256:[0-9a-f]{64}$/, {
|
|
95
|
-
message: "hash must be sha256:<64 hex chars>"
|
|
96
|
-
}).optional(),
|
|
97
|
-
/**
|
|
98
|
-
* What `hash` was taken over: the span's raw text, or the normalised token
|
|
99
|
-
* stream a parser sees (`ast`). Absent means `raw`, which is what every
|
|
100
|
-
* anchor stamped before this field carries, so old hashes keep comparing
|
|
101
|
-
* the way they were written. An `ast` hash is blind to whitespace and
|
|
102
|
-
* comments, so reformatting the anchored code is not drift.
|
|
103
|
-
*/
|
|
104
|
-
hash_kind: import_zod.z.enum(["raw", "ast"]).optional(),
|
|
105
|
-
/** ISO 8601 timestamp of the last successful resolution. */
|
|
106
|
-
resolved_at: import_zod.z.string().min(1).optional(),
|
|
107
|
-
/** Line count of the text the hash was taken over. */
|
|
108
|
-
lines: import_zod.z.number().int().positive().optional(),
|
|
109
|
-
/**
|
|
110
|
-
* Which resolver produced the hashed span. Absent means an anchor stamped
|
|
111
|
-
* before resolvers were named, which is read as `regex` — the only one
|
|
112
|
-
* there was. A hash from a different resolver is drift, not a match.
|
|
113
|
-
*/
|
|
114
|
-
resolver: import_zod.z.enum(["tree-sitter", "regex", "span"]).optional()
|
|
115
|
-
}).strict();
|
|
116
|
-
var kbAnchorWriteSchema = kbAnchorSchema.superRefine((anchor, ctx) => {
|
|
117
|
-
if (anchor.span && anchor.symbol) {
|
|
118
|
-
ctx.addIssue({
|
|
119
|
-
code: import_zod.z.ZodIssueCode.custom,
|
|
120
|
-
path: ["span"],
|
|
121
|
-
message: "an anchor names a symbol or a span, not both"
|
|
122
|
-
});
|
|
40
|
+
// src/concurrency.ts
|
|
41
|
+
var DEFAULT_IO_CONCURRENCY = 16;
|
|
42
|
+
async function mapLimit(items, limit, fn) {
|
|
43
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
44
|
+
throw new RangeError(
|
|
45
|
+
`mapLimit: "limit" must be a positive integer, got ${limit}`
|
|
46
|
+
);
|
|
123
47
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
48
|
+
const out = new Array(items.length);
|
|
49
|
+
let next = 0;
|
|
50
|
+
let failed = false;
|
|
51
|
+
const runners = Array.from(
|
|
52
|
+
{ length: Math.min(limit, items.length) },
|
|
53
|
+
async () => {
|
|
54
|
+
while (!failed && next < items.length) {
|
|
55
|
+
const at2 = next++;
|
|
56
|
+
try {
|
|
57
|
+
out[at2] = await fn(items[at2], at2);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
failed = true;
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
await Promise.all(runners);
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/drift/git.ts
|
|
70
|
+
var import_node_child_process2 = require("child_process");
|
|
71
|
+
var import_node_util2 = require("util");
|
|
72
|
+
|
|
73
|
+
// src/remote-repo/git.ts
|
|
74
|
+
var import_node_child_process = require("child_process");
|
|
75
|
+
var import_node_util = require("util");
|
|
76
|
+
|
|
77
|
+
// src/anchor-resolver/model.ts
|
|
78
|
+
var MAX_ANCHOR_FILE_BYTES = 1048576;
|
|
79
|
+
|
|
80
|
+
// src/remote-repo/git.ts
|
|
81
|
+
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
82
|
+
function childEnv() {
|
|
83
|
+
const env = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
|
|
84
|
+
for (const name of ["GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE"]) {
|
|
85
|
+
delete env[name];
|
|
130
86
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
87
|
+
return env;
|
|
88
|
+
}
|
|
89
|
+
async function git(args, options = {}) {
|
|
90
|
+
try {
|
|
91
|
+
const { stdout, stderr } = await execFileAsync("git", args, {
|
|
92
|
+
...options.cwd ? { cwd: options.cwd } : {},
|
|
93
|
+
timeout: options.timeoutMs ?? 3e4,
|
|
94
|
+
maxBuffer: options.maxBytes ?? MAX_ANCHOR_FILE_BYTES,
|
|
95
|
+
encoding: "utf8",
|
|
96
|
+
windowsHide: true,
|
|
97
|
+
env: childEnv()
|
|
136
98
|
});
|
|
99
|
+
return { ok: true, stdout, stderr, overflowed: false };
|
|
100
|
+
} catch (error) {
|
|
101
|
+
const failure = error;
|
|
102
|
+
return {
|
|
103
|
+
ok: false,
|
|
104
|
+
stdout: failure.stdout ?? "",
|
|
105
|
+
stderr: failure.stderr ?? "",
|
|
106
|
+
overflowed: failure.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER"
|
|
107
|
+
};
|
|
137
108
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
});
|
|
109
|
+
}
|
|
110
|
+
function transportReason(stderr) {
|
|
111
|
+
const text = stderr.toLowerCase();
|
|
112
|
+
if (text.includes("authentication failed") || text.includes("permission denied") || text.includes("could not read username") || text.includes("403 forbidden") || text.includes("access denied")) {
|
|
113
|
+
return "repo-unauthorized";
|
|
144
114
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
var KB_RECORD_TYPES = [
|
|
151
|
-
"fact",
|
|
152
|
-
"requirement",
|
|
153
|
-
"constraint",
|
|
154
|
-
"decision",
|
|
155
|
-
"assumption",
|
|
156
|
-
"open-question",
|
|
157
|
-
"risk",
|
|
158
|
-
"contract",
|
|
159
|
-
"flow",
|
|
160
|
-
"affected-system",
|
|
161
|
-
"test-obligation",
|
|
162
|
-
"source-note"
|
|
163
|
-
];
|
|
164
|
-
var KB_SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
165
|
-
var KB_CONCEPT_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*\.[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
166
|
-
var kbConceptIdSchema = import_zod.z.string().regex(KB_CONCEPT_ID_PATTERN, {
|
|
167
|
-
message: "concept id must be <type>.<slug>, both kebab-case"
|
|
168
|
-
});
|
|
169
|
-
var KB_RECORD_STATUSES = [
|
|
170
|
-
"draft",
|
|
171
|
-
"proposed",
|
|
172
|
-
"accepted",
|
|
173
|
-
"open",
|
|
174
|
-
"resolved",
|
|
175
|
-
"rejected",
|
|
176
|
-
"superseded"
|
|
177
|
-
];
|
|
178
|
-
var KB_MATERIALITIES = [
|
|
179
|
-
"blocking",
|
|
180
|
-
"important",
|
|
181
|
-
"non-blocking"
|
|
182
|
-
];
|
|
183
|
-
var KB_CONFIDENCES = ["low", "medium", "high"];
|
|
184
|
-
var kbRecordFrontmatterSchema = import_zod.z.object({
|
|
185
|
-
// OKF: the only always-required key. A concept carrying just `type` is
|
|
186
|
-
// fully conformant, so everything below stays optional.
|
|
187
|
-
type: import_zod.z.string().min(1),
|
|
188
|
-
// OKF recommended.
|
|
189
|
-
title: import_zod.z.string().min(1).optional(),
|
|
190
|
-
description: import_zod.z.string().min(1).optional(),
|
|
191
|
-
resource: import_zod.z.string().min(1).optional(),
|
|
192
|
-
tags: import_zod.z.array(import_zod.z.string()).optional(),
|
|
193
|
-
// OKF optional: provenance and freshness.
|
|
194
|
-
sources: import_zod.z.array(kbSourceSchema).optional(),
|
|
195
|
-
generated: kbActorStampSchema.optional(),
|
|
196
|
-
verified: import_zod.z.array(kbActorStampSchema).optional(),
|
|
197
|
-
stale_after: import_zod.z.string().min(1).optional(),
|
|
198
|
-
// strauss extensions — see the module comment.
|
|
199
|
-
strauss_anchors: import_zod.z.array(kbAnchorSchema).optional(),
|
|
200
|
-
strauss_verify: import_zod.z.array(import_zod.z.string().min(1)).optional(),
|
|
201
|
-
// Typed causal edges, source → target, living on the source. `A depends_on
|
|
202
|
-
// B` means A needs B, so `kb_impact` walks these inbound: what breaks if B
|
|
203
|
-
// changes is whatever declared a dependence on it.
|
|
204
|
-
strauss_links: import_zod.z.array(kbLinkSchema).optional(),
|
|
205
|
-
// Total after parsing, tolerant before it. Our producers must supply a
|
|
206
|
-
// status — an absent one would leave every reader inventing its own default
|
|
207
|
-
// — but OKF calls a concept carrying only `type` fully conformant, so
|
|
208
|
-
// rejecting a foreign record for the lack of one would put us outside the
|
|
209
|
-
// spec. The default resolves it in the single place that can: here.
|
|
210
|
-
strauss_status: import_zod.z.enum(KB_RECORD_STATUSES).default("draft"),
|
|
211
|
-
strauss_supersedes: import_zod.z.array(import_zod.z.string().min(1)).optional(),
|
|
212
|
-
strauss_superseded_by: import_zod.z.string().min(1).optional(),
|
|
213
|
-
strauss_answered: kbActorStampSchema.optional(),
|
|
214
|
-
strauss_materiality: import_zod.z.enum(KB_MATERIALITIES).optional(),
|
|
215
|
-
strauss_confidence: import_zod.z.enum(KB_CONFIDENCES).optional(),
|
|
216
|
-
strauss_owner: import_zod.z.string().min(1).optional(),
|
|
217
|
-
// "No source exists" as a field rather than a sentinel entry inside
|
|
218
|
-
// `sources`. A sentinel in a reference list is a value doing work a field
|
|
219
|
-
// should do; as a field, `sources` may be legitimately empty.
|
|
220
|
-
strauss_assumption: import_zod.z.boolean().optional()
|
|
221
|
-
}).passthrough();
|
|
115
|
+
if (text.includes("couldn't find remote ref") || text.includes("unadvertised object") || text.includes("not our ref")) {
|
|
116
|
+
return "ref-not-found";
|
|
117
|
+
}
|
|
118
|
+
return "remote-unreachable";
|
|
119
|
+
}
|
|
222
120
|
|
|
223
|
-
// src/
|
|
224
|
-
var
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
requirement: {
|
|
231
|
-
purpose: "Required behavior or outcome",
|
|
232
|
-
sections: ["Claim", "Evidence", "Implication"],
|
|
233
|
-
initialStatus: "proposed"
|
|
234
|
-
},
|
|
235
|
-
constraint: {
|
|
236
|
-
purpose: "Limitation, compatibility boundary, policy, or restriction",
|
|
237
|
-
sections: ["Claim", "Evidence", "Implication"],
|
|
238
|
-
initialStatus: "accepted"
|
|
239
|
-
},
|
|
240
|
-
decision: {
|
|
241
|
-
purpose: "Chosen or proposed direction",
|
|
242
|
-
sections: ["Decision", "Rationale", "Rejected", "Impact"],
|
|
243
|
-
initialStatus: "accepted"
|
|
244
|
-
},
|
|
245
|
-
assumption: {
|
|
246
|
-
purpose: "Unsourced or not-yet-confirmed working assumption",
|
|
247
|
-
sections: ["Claim", "Why we think so", "What would falsify it"],
|
|
248
|
-
initialStatus: "draft"
|
|
249
|
-
},
|
|
250
|
-
"open-question": {
|
|
251
|
-
purpose: "Question needing resolution",
|
|
252
|
-
sections: ["Question", "Why it matters", "Default assumption"],
|
|
253
|
-
initialStatus: "open"
|
|
254
|
-
},
|
|
255
|
-
risk: {
|
|
256
|
-
purpose: "Something that can go wrong",
|
|
257
|
-
sections: ["Risk", "Why it matters", "Mitigation", "Verification"],
|
|
258
|
-
initialStatus: "open"
|
|
259
|
-
},
|
|
260
|
-
contract: {
|
|
261
|
-
purpose: "API, data, event, schema, or permission contract",
|
|
262
|
-
sections: ["Contract", "Producer", "Consumer", "Compatibility"],
|
|
263
|
-
initialStatus: "proposed"
|
|
264
|
-
},
|
|
265
|
-
flow: {
|
|
266
|
-
purpose: "Sequence, lifecycle, or state behavior",
|
|
267
|
-
sections: ["Flow", "Trigger", "Steps", "Failure modes"],
|
|
268
|
-
initialStatus: "accepted"
|
|
269
|
-
},
|
|
270
|
-
"affected-system": {
|
|
271
|
-
purpose: "Component, service, package, integration, or external system",
|
|
272
|
-
sections: ["System", "How it is affected", "Blast radius"],
|
|
273
|
-
initialStatus: "accepted"
|
|
274
|
-
},
|
|
275
|
-
"test-obligation": {
|
|
276
|
-
purpose: "Behavior or contract that must be verified",
|
|
277
|
-
sections: ["Obligation", "Why it matters", "How to verify"],
|
|
278
|
-
initialStatus: "open"
|
|
279
|
-
},
|
|
280
|
-
"source-note": {
|
|
281
|
-
purpose: "Extracted note from source material",
|
|
282
|
-
sections: ["Note", "Where it came from"],
|
|
283
|
-
initialStatus: "accepted"
|
|
284
|
-
}
|
|
285
|
-
};
|
|
286
|
-
function isKbRecordType(value) {
|
|
287
|
-
return Object.prototype.hasOwnProperty.call(RECORD_TYPES, value);
|
|
121
|
+
// src/remote-repo/validate.ts
|
|
122
|
+
var MAX_REF_LENGTH = 200;
|
|
123
|
+
var REF_SHAPE = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
|
|
124
|
+
function refShapeIsSafe(ref) {
|
|
125
|
+
if (!ref || ref.length > MAX_REF_LENGTH) return false;
|
|
126
|
+
if (ref.includes("..")) return false;
|
|
127
|
+
return REF_SHAPE.test(ref);
|
|
288
128
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
"
|
|
292
|
-
|
|
293
|
-
"blocks",
|
|
294
|
-
"invalidates",
|
|
295
|
-
"verified_by",
|
|
296
|
-
"satisfies",
|
|
297
|
-
"related_to"
|
|
298
|
-
];
|
|
299
|
-
var LINK_RELS = {
|
|
300
|
-
depends_on: {
|
|
301
|
-
purpose: "The source needs the target to hold; the source breaks if the target changes",
|
|
302
|
-
phrase: "Depends on",
|
|
303
|
-
dependant: "source"
|
|
304
|
-
},
|
|
305
|
-
constrains: {
|
|
306
|
-
purpose: "The source bounds what the target may do; the target breaks if the constraint changes",
|
|
307
|
-
phrase: "Constrains",
|
|
308
|
-
dependant: "target"
|
|
309
|
-
},
|
|
310
|
-
informs: {
|
|
311
|
-
purpose: "The source shaped the target without binding it; the target is what needs revisiting",
|
|
312
|
-
phrase: "Informs",
|
|
313
|
-
dependant: "target"
|
|
314
|
-
},
|
|
315
|
-
blocks: {
|
|
316
|
-
purpose: "The target cannot proceed until the source is settled; the target is what waits",
|
|
317
|
-
phrase: "Blocks",
|
|
318
|
-
dependant: "target"
|
|
319
|
-
},
|
|
320
|
-
invalidates: {
|
|
321
|
-
purpose: "The source makes the target no longer hold; the target is what stops holding",
|
|
322
|
-
phrase: "Invalidates",
|
|
323
|
-
dependant: "target"
|
|
324
|
-
},
|
|
325
|
-
verified_by: {
|
|
326
|
-
purpose: "The target is the check that confirms the source; the source's confirmation moves with it",
|
|
327
|
-
phrase: "Verified by",
|
|
328
|
-
dependant: "source"
|
|
329
|
-
},
|
|
330
|
-
satisfies: {
|
|
331
|
-
purpose: "The source discharges the target's requirement; the source must change if the requirement does",
|
|
332
|
-
phrase: "Satisfies",
|
|
333
|
-
dependant: "source"
|
|
334
|
-
},
|
|
335
|
-
related_to: {
|
|
336
|
-
purpose: "A pointer worth following, with no claim of dependence",
|
|
337
|
-
phrase: "Relates to",
|
|
338
|
-
dependant: null
|
|
339
|
-
}
|
|
340
|
-
};
|
|
341
|
-
var KB_CAUSAL_LINK_RELS = KB_LINK_RELS.filter(
|
|
342
|
-
(rel) => LINK_RELS[rel].dependant !== null
|
|
343
|
-
);
|
|
344
|
-
function isKbLinkRel(value) {
|
|
345
|
-
return Object.prototype.hasOwnProperty.call(LINK_RELS, value);
|
|
129
|
+
function localRevShapeIsSafe(rev) {
|
|
130
|
+
if (!rev || rev.length > MAX_REF_LENGTH) return false;
|
|
131
|
+
if (rev.includes("..")) return false;
|
|
132
|
+
return /^[A-Za-z0-9][A-Za-z0-9._/^~-]*$/.test(rev);
|
|
346
133
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
rel: import_zod2.z.enum(KB_LINK_RELS)
|
|
352
|
-
}).strict();
|
|
353
|
-
var composeInputSchema = import_zod2.z.object({
|
|
354
|
-
slug: import_zod2.z.string().min(1),
|
|
355
|
-
/** One line, in the reader's terms. Becomes OKF `title`. */
|
|
356
|
-
title: import_zod2.z.string().min(1),
|
|
357
|
-
/** The consequence — what breaks if this is wrong. Becomes `description`. */
|
|
358
|
-
why: import_zod2.z.string().min(1),
|
|
359
|
-
/** Keyed by section heading from the type's spec. Unknown keys rejected. */
|
|
360
|
-
sections: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.string().min(1)).optional(),
|
|
361
|
-
anchors: import_zod2.z.array(kbAnchorWriteSchema).optional(),
|
|
362
|
-
sources: import_zod2.z.array(kbSourceSchema).optional(),
|
|
363
|
-
/** No source exists, as a claim rather than a sentinel in `sources`. */
|
|
364
|
-
assumption: import_zod2.z.boolean().optional(),
|
|
365
|
-
/**
|
|
366
|
-
* OKF `stale_after`: the absolute date this record stops being trusted.
|
|
367
|
-
* Anything the outside world can change — pricing, quotas, versions,
|
|
368
|
-
* reception counts — should carry one.
|
|
369
|
-
*/
|
|
370
|
-
stale_after: import_zod2.z.string().regex(/^\d{4}-\d{2}-\d{2}$/, {
|
|
371
|
-
message: "stale_after must be YYYY-MM-DD"
|
|
372
|
-
}).refine((date) => !Number.isNaN(Date.parse(date)), {
|
|
373
|
-
message: "stale_after must be a real date"
|
|
374
|
-
}).optional(),
|
|
375
|
-
verify: import_zod2.z.array(import_zod2.z.string().min(1)).optional(),
|
|
376
|
-
tags: import_zod2.z.array(import_zod2.z.string().min(1)).optional(),
|
|
377
|
-
/** Concept ids this record relates to; rendered as body links. */
|
|
378
|
-
relatedConceptIds: import_zod2.z.array(kbConceptIdSchema).optional(),
|
|
379
|
-
/**
|
|
380
|
-
* Typed causal edges, source → target: `{ target: "fact.b", rel:
|
|
381
|
-
* "depends_on" }` on record A says A needs B. Stored in frontmatter and
|
|
382
|
-
* also rendered as one prose sentence each, so the meaning survives a
|
|
383
|
-
* reader that knows only OKF. The vocabulary goes into the description from
|
|
384
|
-
* the same table the walk uses, so `kb_schema` emits it.
|
|
385
|
-
*/
|
|
386
|
-
links: import_zod2.z.array(composeLinkSchema).max(64).optional().describe(
|
|
387
|
-
`Typed causal edges, source \u2192 target \u2014 a link on this record says this record <rel> the target. ${KB_LINK_RELS.map(
|
|
388
|
-
(rel) => `${rel}: ${LINK_RELS[rel].purpose}`
|
|
389
|
-
).join("; ")}.`
|
|
390
|
-
),
|
|
391
|
-
/** Concept ids this record replaces. The store settles the backlinks. */
|
|
392
|
-
supersedes: import_zod2.z.array(kbConceptIdSchema).max(32).optional(),
|
|
393
|
-
materiality: import_zod2.z.enum(KB_MATERIALITIES).optional(),
|
|
394
|
-
confidence: import_zod2.z.enum(KB_CONFIDENCES).optional(),
|
|
395
|
-
owner: import_zod2.z.string().min(1).optional()
|
|
396
|
-
}).strict();
|
|
397
|
-
function composeRecord(type, input, writtenBy, writtenAt) {
|
|
398
|
-
const parsed = composeInputSchema.parse(input);
|
|
399
|
-
const spec = RECORD_TYPES[type];
|
|
400
|
-
const sections = parsed.sections ?? {};
|
|
401
|
-
const unknown = Object.keys(sections).filter(
|
|
402
|
-
(heading) => !spec.sections.includes(heading)
|
|
403
|
-
);
|
|
404
|
-
if (unknown.length) {
|
|
405
|
-
throw new Error(
|
|
406
|
-
`kb: ${type} has no section ${unknown.join(", ")} \u2014 expected one of ${spec.sections.join(", ")}`
|
|
407
|
-
);
|
|
408
|
-
}
|
|
409
|
-
const frontmatter = {
|
|
410
|
-
title: parsed.title,
|
|
411
|
-
description: parsed.why,
|
|
412
|
-
generated: { by: writtenBy, at: writtenAt },
|
|
413
|
-
// Empty rather than absent: a later verification pass appends here, and an
|
|
414
|
-
// empty list says "not yet verified" where a missing key would only say
|
|
415
|
-
// "this producer didn't think about it".
|
|
416
|
-
verified: [],
|
|
417
|
-
strauss_status: spec.initialStatus
|
|
418
|
-
};
|
|
419
|
-
if (parsed.stale_after) frontmatter.stale_after = parsed.stale_after;
|
|
420
|
-
if (parsed.anchors?.length) frontmatter.strauss_anchors = parsed.anchors;
|
|
421
|
-
if (parsed.verify?.length) frontmatter.strauss_verify = parsed.verify;
|
|
422
|
-
if (parsed.tags?.length) frontmatter.tags = parsed.tags;
|
|
423
|
-
if (parsed.sources?.length) frontmatter.sources = parsed.sources;
|
|
424
|
-
if (parsed.assumption) frontmatter.strauss_assumption = true;
|
|
425
|
-
if (parsed.materiality) frontmatter.strauss_materiality = parsed.materiality;
|
|
426
|
-
if (parsed.confidence) frontmatter.strauss_confidence = parsed.confidence;
|
|
427
|
-
if (parsed.owner) frontmatter.strauss_owner = parsed.owner;
|
|
428
|
-
if (parsed.supersedes?.length)
|
|
429
|
-
frontmatter.strauss_supersedes = parsed.supersedes;
|
|
430
|
-
const selfLink = parsed.links?.find(
|
|
431
|
-
(link2) => link2.target === `${type}.${parsed.slug}`
|
|
432
|
-
);
|
|
433
|
-
if (selfLink) {
|
|
434
|
-
throw new Error(
|
|
435
|
-
`kb: ${type}.${parsed.slug} cannot ${selfLink.rel} itself \u2014 a link must name another record`
|
|
436
|
-
);
|
|
437
|
-
}
|
|
438
|
-
if (parsed.links?.length) frontmatter.strauss_links = parsed.links;
|
|
439
|
-
const blocks = [];
|
|
440
|
-
for (const heading of spec.sections) {
|
|
441
|
-
const text = sections[heading];
|
|
442
|
-
if (text) blocks.push(`## ${heading}
|
|
443
|
-
|
|
444
|
-
${text}`);
|
|
445
|
-
}
|
|
446
|
-
if (!blocks.length) blocks.push(parsed.why);
|
|
447
|
-
for (const related of parsed.relatedConceptIds ?? []) {
|
|
448
|
-
blocks.push(`Relates to [${related}](${related}.md).`);
|
|
449
|
-
}
|
|
450
|
-
for (const link2 of parsed.links ?? []) {
|
|
451
|
-
blocks.push(
|
|
452
|
-
`${LINK_RELS[link2.rel].phrase} [${link2.target}](${link2.target}.md).`
|
|
453
|
-
);
|
|
454
|
-
}
|
|
455
|
-
if (parsed.sources?.length) {
|
|
456
|
-
blocks.push(
|
|
457
|
-
parsed.sources.map((source) => `[^${source.id}]: ${source.title ?? source.resource}`).join("\n")
|
|
458
|
-
);
|
|
459
|
-
}
|
|
460
|
-
return {
|
|
461
|
-
type,
|
|
462
|
-
slug: parsed.slug,
|
|
463
|
-
frontmatter,
|
|
464
|
-
body: `${blocks.join("\n\n")}
|
|
465
|
-
`
|
|
466
|
-
};
|
|
134
|
+
async function refIsWellFormed(ref) {
|
|
135
|
+
if (!refShapeIsSafe(ref)) return false;
|
|
136
|
+
const checked = await git(["check-ref-format", "--allow-onelevel", ref]);
|
|
137
|
+
return checked.ok;
|
|
467
138
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
var decisionInputSchema = composeInputSchema.omit({ sections: true }).extend({
|
|
473
|
-
alternative: import_zod3.z.string().min(1).optional(),
|
|
474
|
-
impact: import_zod3.z.string().min(1).optional()
|
|
475
|
-
}).strict();
|
|
476
|
-
function composeDecisionRecord(input, writtenBy, writtenAt) {
|
|
477
|
-
const { alternative, impact: impact2, ...rest } = input;
|
|
478
|
-
return composeRecord(
|
|
479
|
-
DECISION_TYPE,
|
|
480
|
-
{
|
|
481
|
-
...rest,
|
|
482
|
-
sections: {
|
|
483
|
-
Decision: input.title,
|
|
484
|
-
Rationale: input.why,
|
|
485
|
-
...alternative ? { Rejected: alternative } : {},
|
|
486
|
-
...impact2 ? { Impact: impact2 } : {}
|
|
487
|
-
}
|
|
488
|
-
},
|
|
489
|
-
writtenBy,
|
|
490
|
-
writtenAt
|
|
491
|
-
);
|
|
139
|
+
function filePathIsSafe(file) {
|
|
140
|
+
const path = file.replace(/^\.\//, "");
|
|
141
|
+
if (!path || path.startsWith("-") || path.includes("\0")) return false;
|
|
142
|
+
return !path.split("/").includes("..");
|
|
492
143
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
why: reason,
|
|
500
|
-
sections: { Decision: reason }
|
|
501
|
-
},
|
|
502
|
-
writtenBy,
|
|
503
|
-
writtenAt
|
|
504
|
-
);
|
|
144
|
+
var DEFAULT_PROTOCOLS = ["https", "ssh", "git"];
|
|
145
|
+
function allowedProtocols() {
|
|
146
|
+
const raw = process.env["STRAUSS_KB_REPO_PROTOCOLS"];
|
|
147
|
+
if (raw === void 0) return [...DEFAULT_PROTOCOLS];
|
|
148
|
+
const listed = raw.split(",").map((entry) => entry.trim().toLowerCase()).filter(Boolean);
|
|
149
|
+
return listed.length ? listed : [...DEFAULT_PROTOCOLS];
|
|
505
150
|
}
|
|
506
|
-
function
|
|
507
|
-
return
|
|
151
|
+
function isShortRepoName(repo) {
|
|
152
|
+
return /^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/.test(repo.trim());
|
|
508
153
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
`mapLimit: "limit" must be a positive integer, got ${limit}`
|
|
524
|
-
);
|
|
525
|
-
}
|
|
526
|
-
const out = new Array(items.length);
|
|
527
|
-
let next = 0;
|
|
528
|
-
let failed = false;
|
|
529
|
-
const runners = Array.from(
|
|
530
|
-
{ length: Math.min(limit, items.length) },
|
|
531
|
-
async () => {
|
|
532
|
-
while (!failed && next < items.length) {
|
|
533
|
-
const at2 = next++;
|
|
534
|
-
try {
|
|
535
|
-
out[at2] = await fn(items[at2], at2);
|
|
536
|
-
} catch (error) {
|
|
537
|
-
failed = true;
|
|
538
|
-
throw error;
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
);
|
|
543
|
-
await Promise.all(runners);
|
|
544
|
-
return out;
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
// src/drift/git.ts
|
|
548
|
-
var import_node_child_process2 = require("child_process");
|
|
549
|
-
var import_node_util2 = require("util");
|
|
550
|
-
|
|
551
|
-
// src/remote-repo/git.ts
|
|
552
|
-
var import_node_child_process = require("child_process");
|
|
553
|
-
var import_node_util = require("util");
|
|
554
|
-
|
|
555
|
-
// src/anchor-resolver/model.ts
|
|
556
|
-
var MAX_ANCHOR_FILE_BYTES = 1048576;
|
|
557
|
-
|
|
558
|
-
// src/remote-repo/git.ts
|
|
559
|
-
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
560
|
-
function childEnv() {
|
|
561
|
-
const env = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
|
|
562
|
-
for (const name of ["GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE"]) {
|
|
563
|
-
delete env[name];
|
|
564
|
-
}
|
|
565
|
-
return env;
|
|
566
|
-
}
|
|
567
|
-
async function git(args, options = {}) {
|
|
568
|
-
try {
|
|
569
|
-
const { stdout, stderr } = await execFileAsync("git", args, {
|
|
570
|
-
...options.cwd ? { cwd: options.cwd } : {},
|
|
571
|
-
timeout: options.timeoutMs ?? 3e4,
|
|
572
|
-
maxBuffer: options.maxBytes ?? MAX_ANCHOR_FILE_BYTES,
|
|
573
|
-
encoding: "utf8",
|
|
574
|
-
windowsHide: true,
|
|
575
|
-
env: childEnv()
|
|
576
|
-
});
|
|
577
|
-
return { ok: true, stdout, stderr, overflowed: false };
|
|
578
|
-
} catch (error) {
|
|
579
|
-
const failure = error;
|
|
580
|
-
return {
|
|
581
|
-
ok: false,
|
|
582
|
-
stdout: failure.stdout ?? "",
|
|
583
|
-
stderr: failure.stderr ?? "",
|
|
584
|
-
overflowed: failure.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER"
|
|
585
|
-
};
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
function transportReason(stderr) {
|
|
589
|
-
const text = stderr.toLowerCase();
|
|
590
|
-
if (text.includes("authentication failed") || text.includes("permission denied") || text.includes("could not read username") || text.includes("403 forbidden") || text.includes("access denied")) {
|
|
591
|
-
return "repo-unauthorized";
|
|
592
|
-
}
|
|
593
|
-
if (text.includes("couldn't find remote ref") || text.includes("unadvertised object") || text.includes("not our ref")) {
|
|
594
|
-
return "ref-not-found";
|
|
595
|
-
}
|
|
596
|
-
return "remote-unreachable";
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
// src/remote-repo/validate.ts
|
|
600
|
-
var MAX_REF_LENGTH = 200;
|
|
601
|
-
var REF_SHAPE = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
|
|
602
|
-
function refShapeIsSafe(ref) {
|
|
603
|
-
if (!ref || ref.length > MAX_REF_LENGTH) return false;
|
|
604
|
-
if (ref.includes("..")) return false;
|
|
605
|
-
return REF_SHAPE.test(ref);
|
|
606
|
-
}
|
|
607
|
-
function localRevShapeIsSafe(rev) {
|
|
608
|
-
if (!rev || rev.length > MAX_REF_LENGTH) return false;
|
|
609
|
-
if (rev.includes("..")) return false;
|
|
610
|
-
return /^[A-Za-z0-9][A-Za-z0-9._/^~-]*$/.test(rev);
|
|
611
|
-
}
|
|
612
|
-
async function refIsWellFormed(ref) {
|
|
613
|
-
if (!refShapeIsSafe(ref)) return false;
|
|
614
|
-
const checked = await git(["check-ref-format", "--allow-onelevel", ref]);
|
|
615
|
-
return checked.ok;
|
|
616
|
-
}
|
|
617
|
-
function filePathIsSafe(file) {
|
|
618
|
-
const path = file.replace(/^\.\//, "");
|
|
619
|
-
if (!path || path.startsWith("-") || path.includes("\0")) return false;
|
|
620
|
-
return !path.split("/").includes("..");
|
|
621
|
-
}
|
|
622
|
-
var DEFAULT_PROTOCOLS = ["https", "ssh", "git"];
|
|
623
|
-
function allowedProtocols() {
|
|
624
|
-
const raw = process.env["STRAUSS_KB_REPO_PROTOCOLS"];
|
|
625
|
-
if (raw === void 0) return [...DEFAULT_PROTOCOLS];
|
|
626
|
-
const listed = raw.split(",").map((entry) => entry.trim().toLowerCase()).filter(Boolean);
|
|
627
|
-
return listed.length ? listed : [...DEFAULT_PROTOCOLS];
|
|
628
|
-
}
|
|
629
|
-
function isShortRepoName(repo) {
|
|
630
|
-
return /^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/.test(repo.trim());
|
|
631
|
-
}
|
|
632
|
-
var SCP_LIKE = /^[\w.-]+@[\w.-]+:(?!\/)\S+$/;
|
|
633
|
-
var URL_SCHEME = /^([A-Za-z0-9+.-]+):\/\//;
|
|
634
|
-
var CONTROL_CHARS = /[\u0000-\u001f\u007f]/;
|
|
635
|
-
function repoUrlIsSafe(repo) {
|
|
636
|
-
const url = repo.trim();
|
|
637
|
-
if (!url || url.startsWith("-") || CONTROL_CHARS.test(url)) return false;
|
|
638
|
-
const allowed = allowedProtocols();
|
|
639
|
-
if (SCP_LIKE.test(url)) return allowed.includes("ssh");
|
|
640
|
-
const scheme = URL_SCHEME.exec(url);
|
|
641
|
-
if (!scheme?.[1]) return false;
|
|
642
|
-
if (!allowed.includes(scheme[1].toLowerCase())) return false;
|
|
643
|
-
const authority = url.slice(scheme[0].length).split("/")[0] ?? "";
|
|
644
|
-
const at2 = authority.lastIndexOf("@");
|
|
645
|
-
return at2 < 0 || !authority.slice(0, at2).includes(":");
|
|
154
|
+
var SCP_LIKE = /^[\w.-]+@[\w.-]+:(?!\/)\S+$/;
|
|
155
|
+
var URL_SCHEME = /^([A-Za-z0-9+.-]+):\/\//;
|
|
156
|
+
var CONTROL_CHARS = /[\u0000-\u001f\u007f]/;
|
|
157
|
+
function repoUrlIsSafe(repo) {
|
|
158
|
+
const url = repo.trim();
|
|
159
|
+
if (!url || url.startsWith("-") || CONTROL_CHARS.test(url)) return false;
|
|
160
|
+
const allowed = allowedProtocols();
|
|
161
|
+
if (SCP_LIKE.test(url)) return allowed.includes("ssh");
|
|
162
|
+
const scheme = URL_SCHEME.exec(url);
|
|
163
|
+
if (!scheme?.[1]) return false;
|
|
164
|
+
if (!allowed.includes(scheme[1].toLowerCase())) return false;
|
|
165
|
+
const authority = url.slice(scheme[0].length).split("/")[0] ?? "";
|
|
166
|
+
const at2 = authority.lastIndexOf("@");
|
|
167
|
+
return at2 < 0 || !authority.slice(0, at2).includes(":");
|
|
646
168
|
}
|
|
647
169
|
function protocolArgs() {
|
|
648
170
|
const allowed = allowedProtocols();
|
|
@@ -1327,26 +849,26 @@ var import_node_path4 = require("path");
|
|
|
1327
849
|
var import_node_url = require("url");
|
|
1328
850
|
|
|
1329
851
|
// src/grammars/model.ts
|
|
1330
|
-
var
|
|
1331
|
-
var sha2562 =
|
|
1332
|
-
var grammarWasmSchema =
|
|
1333
|
-
url:
|
|
852
|
+
var import_zod = require("zod");
|
|
853
|
+
var sha2562 = import_zod.z.string().regex(/^[0-9a-f]{64}$/);
|
|
854
|
+
var grammarWasmSchema = import_zod.z.object({
|
|
855
|
+
url: import_zod.z.string().min(1),
|
|
1334
856
|
sha256: sha2562,
|
|
1335
|
-
bytes:
|
|
857
|
+
bytes: import_zod.z.number().int().positive()
|
|
1336
858
|
});
|
|
1337
|
-
var grammarTagsSchema =
|
|
1338
|
-
var grammarPackSchema =
|
|
1339
|
-
package:
|
|
859
|
+
var grammarTagsSchema = import_zod.z.object({ url: import_zod.z.string().min(1), sha256: sha2562 });
|
|
860
|
+
var grammarPackSchema = import_zod.z.object({
|
|
861
|
+
package: import_zod.z.string().min(1),
|
|
1340
862
|
wasm: grammarWasmSchema,
|
|
1341
|
-
tags:
|
|
1342
|
-
license:
|
|
1343
|
-
extensions:
|
|
863
|
+
tags: import_zod.z.array(grammarTagsSchema),
|
|
864
|
+
license: import_zod.z.string().min(1),
|
|
865
|
+
extensions: import_zod.z.array(import_zod.z.string().min(1))
|
|
1344
866
|
});
|
|
1345
|
-
var grammarManifestSchema =
|
|
867
|
+
var grammarManifestSchema = import_zod.z.object({
|
|
1346
868
|
/** The runtime the packs were proved against. */
|
|
1347
|
-
webTreeSitter:
|
|
1348
|
-
linguist:
|
|
1349
|
-
packs:
|
|
869
|
+
webTreeSitter: import_zod.z.string().min(1),
|
|
870
|
+
linguist: import_zod.z.object({ tag: import_zod.z.string().min(1), commit: import_zod.z.string().min(1) }),
|
|
871
|
+
packs: import_zod.z.record(import_zod.z.string().min(1), grammarPackSchema)
|
|
1350
872
|
});
|
|
1351
873
|
|
|
1352
874
|
// src/grammars/manifest.ts
|
|
@@ -1968,321 +1490,908 @@ function distanceToParent(lines, index2, parent) {
|
|
|
1968
1490
|
for (let at2 = index2; at2 >= floor; at2--) {
|
|
1969
1491
|
if (parent.test(lines[at2] ?? "")) return index2 - at2;
|
|
1970
1492
|
}
|
|
1971
|
-
return Number.POSITIVE_INFINITY;
|
|
1493
|
+
return Number.POSITIVE_INFINITY;
|
|
1494
|
+
}
|
|
1495
|
+
function hashAnchorText(text) {
|
|
1496
|
+
return `sha256:${(0, import_node_crypto3.createHash)("sha256").update(text.replace(/\r\n/g, "\n")).digest("hex")}`;
|
|
1497
|
+
}
|
|
1498
|
+
function resolveAnchorSpan(source, anchor, resolvers = [regexResolver]) {
|
|
1499
|
+
const normalized = source.replace(/\r\n/g, "\n");
|
|
1500
|
+
if (anchor.span) return sliceSpan(normalized, anchor.span);
|
|
1501
|
+
if (!anchor.symbol) {
|
|
1502
|
+
const lines = normalized.split("\n");
|
|
1503
|
+
if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
|
|
1504
|
+
return {
|
|
1505
|
+
ok: true,
|
|
1506
|
+
span: {
|
|
1507
|
+
text: normalized,
|
|
1508
|
+
startLine: 1,
|
|
1509
|
+
endLine: Math.max(1, lines.length)
|
|
1510
|
+
}
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1513
|
+
let afterParsedMiss = false;
|
|
1514
|
+
for (const resolver of resolvers) {
|
|
1515
|
+
const attempt = resolver.attempt ? resolver.attempt(normalized, anchor.symbol, anchor.file, {
|
|
1516
|
+
afterParsedMiss
|
|
1517
|
+
}) : fromResolve(resolver, normalized, anchor.symbol, anchor.file);
|
|
1518
|
+
if (attempt.kind === "abstain") continue;
|
|
1519
|
+
if (attempt.kind === "unresolved") {
|
|
1520
|
+
if (attempt.reason === "symbol-not-found") {
|
|
1521
|
+
if (resolver.attempt) afterParsedMiss = true;
|
|
1522
|
+
continue;
|
|
1523
|
+
}
|
|
1524
|
+
return { ok: false, reason: attempt.reason };
|
|
1525
|
+
}
|
|
1526
|
+
const tokens2 = resolver.normalize?.(attempt.span.text, anchor.file);
|
|
1527
|
+
return {
|
|
1528
|
+
ok: true,
|
|
1529
|
+
span: attempt.span,
|
|
1530
|
+
...isResolverName(resolver.name) ? { resolver: resolver.name } : {},
|
|
1531
|
+
...tokens2 ? { normalized: tokens2 } : {}
|
|
1532
|
+
};
|
|
1533
|
+
}
|
|
1534
|
+
return { ok: false, reason: "symbol-not-found" };
|
|
1535
|
+
}
|
|
1536
|
+
function sliceSpan(source, range) {
|
|
1537
|
+
const lines = source.split("\n");
|
|
1538
|
+
if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
|
|
1539
|
+
if (range.end > lines.length) {
|
|
1540
|
+
return { ok: false, reason: "span-out-of-range" };
|
|
1541
|
+
}
|
|
1542
|
+
return {
|
|
1543
|
+
ok: true,
|
|
1544
|
+
span: {
|
|
1545
|
+
text: lines.slice(range.start - 1, range.end).join("\n"),
|
|
1546
|
+
startLine: range.start,
|
|
1547
|
+
endLine: range.end
|
|
1548
|
+
},
|
|
1549
|
+
resolver: "span"
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
function fromResolve(resolver, source, symbol, file) {
|
|
1553
|
+
const span2 = resolver.resolve(source, symbol, file);
|
|
1554
|
+
return span2 ? { kind: "resolved", span: span2 } : { kind: "unresolved", reason: "symbol-not-found" };
|
|
1555
|
+
}
|
|
1556
|
+
function isResolverName(name) {
|
|
1557
|
+
return name === "tree-sitter" || name === "regex" || name === "span";
|
|
1558
|
+
}
|
|
1559
|
+
async function prepareResolvers(resolvers, files) {
|
|
1560
|
+
for (const resolver of resolvers) await resolver.prepare?.(files);
|
|
1561
|
+
}
|
|
1562
|
+
function defaultAnchorResolvers(grammars = {}) {
|
|
1563
|
+
return [new TreeSitterResolver(grammars), regexResolver];
|
|
1564
|
+
}
|
|
1565
|
+
function resolverChanged(source, anchor, produced) {
|
|
1566
|
+
const previous = anchor.resolver ?? "regex";
|
|
1567
|
+
if (!produced || !anchor.symbol || previous === produced) return false;
|
|
1568
|
+
if (previous !== "regex") return false;
|
|
1569
|
+
const before = regexResolver.resolve(
|
|
1570
|
+
source.replace(/\r\n/g, "\n"),
|
|
1571
|
+
anchor.symbol
|
|
1572
|
+
);
|
|
1573
|
+
return before !== null && hashAnchorText(before.text) === anchor.hash;
|
|
1574
|
+
}
|
|
1575
|
+
function anchorHashOf(anchor, outcome) {
|
|
1576
|
+
if (outcome.resolver === "span") {
|
|
1577
|
+
return { hash: hashAnchorText(outcome.span.text), kind: "raw" };
|
|
1578
|
+
}
|
|
1579
|
+
const stored = anchor.hash ? anchor.hash_kind ?? "raw" : void 0;
|
|
1580
|
+
const wanted = stored ?? (outcome.normalized ? "ast" : "raw");
|
|
1581
|
+
return wanted === "ast" && outcome.normalized ? { hash: hashAnchorText(outcome.normalized), kind: "ast" } : { hash: hashAnchorText(outcome.span.text), kind: "raw" };
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
// src/anchor-resolver/drift.ts
|
|
1585
|
+
async function detectAnchorDrift(records, options = {}) {
|
|
1586
|
+
const repoRoot = options.repoRoot ?? process.cwd();
|
|
1587
|
+
const resolvers = options.resolvers ?? (options.resolver ? [options.resolver] : defaultAnchorResolvers({
|
|
1588
|
+
offline: options.remote?.offline === true
|
|
1589
|
+
}));
|
|
1590
|
+
const origin = new LazyOrigin(repoRoot);
|
|
1591
|
+
const planned = /* @__PURE__ */ new Map();
|
|
1592
|
+
let declaresRepo = false;
|
|
1593
|
+
for (const record of records) {
|
|
1594
|
+
const anchors = (record.frontmatter.strauss_anchors ?? []).filter(
|
|
1595
|
+
(anchor) => anchor.hash
|
|
1596
|
+
);
|
|
1597
|
+
if (!anchors.length) continue;
|
|
1598
|
+
if (anchors.some((anchor) => anchor.repo)) declaresRepo = true;
|
|
1599
|
+
planned.set(
|
|
1600
|
+
record.conceptId,
|
|
1601
|
+
anchors.map((anchor) => ({ anchor, foreign: false }))
|
|
1602
|
+
);
|
|
1603
|
+
}
|
|
1604
|
+
if (declaresRepo) {
|
|
1605
|
+
await origin.prime();
|
|
1606
|
+
for (const entries of planned.values()) {
|
|
1607
|
+
for (const entry of entries)
|
|
1608
|
+
entry.foreign = origin.isForeign(entry.anchor);
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
const files = [];
|
|
1612
|
+
const committedWants = [];
|
|
1613
|
+
const wants = [];
|
|
1614
|
+
for (const entries of planned.values()) {
|
|
1615
|
+
for (const { anchor, foreign } of entries) {
|
|
1616
|
+
if (foreign) wants.push(...remoteWants(anchor));
|
|
1617
|
+
else if (anchor.side === "old") committedWants.push(anchor);
|
|
1618
|
+
else files.push(anchor.file);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
const [reads, committed, remote] = await Promise.all([
|
|
1622
|
+
readAnchorFiles(
|
|
1623
|
+
files,
|
|
1624
|
+
options.reader ?? anchorFileReader(repoRoot),
|
|
1625
|
+
options.concurrency ?? DEFAULT_IO_CONCURRENCY
|
|
1626
|
+
),
|
|
1627
|
+
readCommitted(repoRoot, committedWants, options),
|
|
1628
|
+
(options.readRemote ?? readRemoteAnchors)(wants, options.remote ?? {})
|
|
1629
|
+
]);
|
|
1630
|
+
await prepareResolvers(resolvers, [
|
|
1631
|
+
...files,
|
|
1632
|
+
...committedWants.map((anchor) => anchor.file),
|
|
1633
|
+
...wants.map((want) => want.file)
|
|
1634
|
+
]);
|
|
1635
|
+
const drift = /* @__PURE__ */ new Map();
|
|
1636
|
+
for (const record of records) {
|
|
1637
|
+
const entries = [];
|
|
1638
|
+
for (const { anchor, foreign } of planned.get(record.conceptId) ?? []) {
|
|
1639
|
+
if (foreign) {
|
|
1640
|
+
entries.push(remoteEntry(anchor, remote, resolvers));
|
|
1641
|
+
continue;
|
|
1642
|
+
}
|
|
1643
|
+
const read = anchor.side === "old" ? committed.get(atRefKey(anchor)) : reads.get(anchor.file);
|
|
1644
|
+
entries.push(localEntry(anchor, read, resolvers));
|
|
1645
|
+
}
|
|
1646
|
+
if (entries.length) drift.set(record.conceptId, entries);
|
|
1647
|
+
}
|
|
1648
|
+
return drift;
|
|
1649
|
+
}
|
|
1650
|
+
function atRefKey(anchor) {
|
|
1651
|
+
return `${anchor.ref ?? ""}\0${anchor.file}`;
|
|
1652
|
+
}
|
|
1653
|
+
async function readCommitted(repoRoot, anchors, options = {}) {
|
|
1654
|
+
if (!anchors.length) return /* @__PURE__ */ new Map();
|
|
1655
|
+
const read = options.readAtRef ?? readFileAtRef;
|
|
1656
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
1657
|
+
for (const anchor of anchors) byKey.set(atRefKey(anchor), anchor);
|
|
1658
|
+
const keys = [...byKey.keys()];
|
|
1659
|
+
const results = await mapLimit(
|
|
1660
|
+
keys,
|
|
1661
|
+
options.concurrency ?? DEFAULT_IO_CONCURRENCY,
|
|
1662
|
+
(key2) => read(repoRoot, byKey.get(key2))
|
|
1663
|
+
);
|
|
1664
|
+
return new Map(keys.map((key2, at2) => [key2, results[at2]]));
|
|
1665
|
+
}
|
|
1666
|
+
function remoteWants(anchor) {
|
|
1667
|
+
const repo = anchor.repo;
|
|
1668
|
+
const wants = [{ repo, file: anchor.file }];
|
|
1669
|
+
if (anchor.ref) wants.unshift({ repo, ref: anchor.ref, file: anchor.file });
|
|
1670
|
+
return wants;
|
|
1671
|
+
}
|
|
1672
|
+
function base(anchor) {
|
|
1673
|
+
return {
|
|
1674
|
+
file: anchor.file,
|
|
1675
|
+
...anchor.symbol ? { symbol: anchor.symbol } : {},
|
|
1676
|
+
...anchor.side === "old" ? { side: "old" } : {},
|
|
1677
|
+
storedHash: anchor.hash
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
function unresolved(anchor, reason, repo) {
|
|
1681
|
+
return {
|
|
1682
|
+
...base(anchor),
|
|
1683
|
+
state: "unresolved",
|
|
1684
|
+
diffSize: null,
|
|
1685
|
+
...reason ? { reason } : {},
|
|
1686
|
+
...repo ? { repo } : {},
|
|
1687
|
+
...classOf(reason)
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
var GONE_REASONS = /* @__PURE__ */ new Set([
|
|
1691
|
+
"file-missing",
|
|
1692
|
+
"symbol-not-found",
|
|
1693
|
+
"span-out-of-range",
|
|
1694
|
+
"ref-unreadable"
|
|
1695
|
+
]);
|
|
1696
|
+
function provisionalDriftClass(entry) {
|
|
1697
|
+
if (entry.state === "unresolved") {
|
|
1698
|
+
return GONE_REASONS.has(entry.reason) ? "gone" : void 0;
|
|
1699
|
+
}
|
|
1700
|
+
return entry.state === "drifted" ? "changed" : void 0;
|
|
1701
|
+
}
|
|
1702
|
+
function classOf(reason) {
|
|
1703
|
+
const settled = provisionalDriftClass({ state: "unresolved", reason });
|
|
1704
|
+
return settled ? { class: settled } : {};
|
|
1705
|
+
}
|
|
1706
|
+
function hashIn(source, anchor, resolvers) {
|
|
1707
|
+
const outcome = resolveAnchorSpan(source, anchor, resolvers);
|
|
1708
|
+
if (!outcome.ok) return { ok: false, reason: outcome.reason };
|
|
1709
|
+
const { hash, kind } = anchorHashOf(anchor, outcome);
|
|
1710
|
+
return {
|
|
1711
|
+
ok: true,
|
|
1712
|
+
current: {
|
|
1713
|
+
hash,
|
|
1714
|
+
kind,
|
|
1715
|
+
lines: outcome.span.endLine - outcome.span.startLine + 1,
|
|
1716
|
+
...outcome.resolver ? { resolver: outcome.resolver } : {}
|
|
1717
|
+
}
|
|
1718
|
+
};
|
|
1719
|
+
}
|
|
1720
|
+
function resolverExtras(source, anchor, current) {
|
|
1721
|
+
return {
|
|
1722
|
+
...current.resolver ? { resolver: current.resolver } : {},
|
|
1723
|
+
...current.hash !== anchor.hash && resolverChanged(source, anchor, current.resolver) ? { reason: "resolver-changed" } : {}
|
|
1724
|
+
};
|
|
1725
|
+
}
|
|
1726
|
+
function compared(anchor, current, extra = {}) {
|
|
1727
|
+
const matched = current.hash === anchor.hash;
|
|
1728
|
+
return {
|
|
1729
|
+
...base(anchor),
|
|
1730
|
+
state: matched ? "match" : "drifted",
|
|
1731
|
+
currentHash: current.hash,
|
|
1732
|
+
hashKind: current.kind,
|
|
1733
|
+
diffSize: anchor.lines === void 0 ? null : Math.abs(current.lines - anchor.lines),
|
|
1734
|
+
...matched ? {} : { class: "changed" },
|
|
1735
|
+
...extra
|
|
1736
|
+
};
|
|
1737
|
+
}
|
|
1738
|
+
function localEntry(anchor, read, resolvers) {
|
|
1739
|
+
if (!read.ok) return unresolved(anchor, read.reason);
|
|
1740
|
+
const found = hashIn(read.source, anchor, resolvers);
|
|
1741
|
+
if (!found.ok) return unresolved(anchor, found.reason);
|
|
1742
|
+
return compared(
|
|
1743
|
+
anchor,
|
|
1744
|
+
found.current,
|
|
1745
|
+
resolverExtras(read.source, anchor, found.current)
|
|
1746
|
+
);
|
|
1747
|
+
}
|
|
1748
|
+
function remoteEntry(anchor, remote, resolvers) {
|
|
1749
|
+
const repo = anchor.repo;
|
|
1750
|
+
const key2 = normalizeRepoUrl(repo);
|
|
1751
|
+
const atDefault = remote.get(wantKey(key2, void 0, anchor.file));
|
|
1752
|
+
const primary = anchor.ref ? remote.get(wantKey(key2, anchor.ref, anchor.file)) : atDefault;
|
|
1753
|
+
if (!primary) return unresolved(anchor, "remote-unreachable", repo);
|
|
1754
|
+
if (!primary.ok) return unresolved(anchor, primary.reason, repo);
|
|
1755
|
+
const found = hashIn(primary.source, anchor, resolvers);
|
|
1756
|
+
if (!found.ok) return unresolved(anchor, found.reason, repo);
|
|
1757
|
+
const current = found.current;
|
|
1758
|
+
const extras = resolverExtras(primary.source, anchor, current);
|
|
1759
|
+
if (!anchor.ref) return compared(anchor, current, { repo, ...extras });
|
|
1760
|
+
if (current.hash !== anchor.hash) {
|
|
1761
|
+
return compared(anchor, current, {
|
|
1762
|
+
repo,
|
|
1763
|
+
...extras,
|
|
1764
|
+
remoteState: "drifted-from-ref"
|
|
1765
|
+
});
|
|
1766
|
+
}
|
|
1767
|
+
if (anchor.side === "old") {
|
|
1768
|
+
return compared(anchor, current, {
|
|
1769
|
+
repo,
|
|
1770
|
+
...extras,
|
|
1771
|
+
remoteState: "matches-ref"
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1774
|
+
const head = atDefault?.ok ? hashIn(atDefault.source, anchor, resolvers) : null;
|
|
1775
|
+
return head?.ok && head.current.hash !== anchor.hash ? {
|
|
1776
|
+
...compared(anchor, head.current, {
|
|
1777
|
+
repo,
|
|
1778
|
+
...head.current.resolver ? { resolver: head.current.resolver } : {}
|
|
1779
|
+
}),
|
|
1780
|
+
state: "drifted",
|
|
1781
|
+
remoteState: "drifted-on-default"
|
|
1782
|
+
} : compared(anchor, current, {
|
|
1783
|
+
repo,
|
|
1784
|
+
...extras,
|
|
1785
|
+
remoteState: "matches-ref"
|
|
1786
|
+
});
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
// src/kb-record.schema.ts
|
|
1790
|
+
var import_zod2 = require("zod");
|
|
1791
|
+
var kbSourceSchema = import_zod2.z.object({
|
|
1792
|
+
id: import_zod2.z.string().min(1),
|
|
1793
|
+
resource: import_zod2.z.string().min(1),
|
|
1794
|
+
title: import_zod2.z.string().min(1).optional(),
|
|
1795
|
+
author: import_zod2.z.string().min(1).optional(),
|
|
1796
|
+
last_modified: import_zod2.z.string().min(1).optional()
|
|
1797
|
+
}).passthrough();
|
|
1798
|
+
var kbActorStampSchema = import_zod2.z.object({
|
|
1799
|
+
by: import_zod2.z.string().min(1),
|
|
1800
|
+
at: import_zod2.z.string().min(1)
|
|
1801
|
+
}).passthrough();
|
|
1802
|
+
var kbVerifiedEventSchema = kbActorStampSchema.extend({
|
|
1803
|
+
note: import_zod2.z.string().refine((s) => s.trim().length > 0, {
|
|
1804
|
+
message: "note must say what the check found"
|
|
1805
|
+
})
|
|
1806
|
+
});
|
|
1807
|
+
var kbAnchorSpanSchema = import_zod2.z.object({
|
|
1808
|
+
start: import_zod2.z.number().int().positive(),
|
|
1809
|
+
end: import_zod2.z.number().int().positive()
|
|
1810
|
+
}).strict();
|
|
1811
|
+
var kbAnchorSchema = import_zod2.z.object({
|
|
1812
|
+
file: import_zod2.z.string().min(1),
|
|
1813
|
+
symbol: import_zod2.z.string().min(1).optional(),
|
|
1814
|
+
/**
|
|
1815
|
+
* The lines the concept names, when no symbol covers them — deleted code,
|
|
1816
|
+
* YAML, SQL, Markdown. Alternative to `symbol`, never a refinement of it.
|
|
1817
|
+
*/
|
|
1818
|
+
span: kbAnchorSpanSchema.optional(),
|
|
1819
|
+
/**
|
|
1820
|
+
* Which side of the change the anchor describes. `old` is code as it was
|
|
1821
|
+
* committed at `ref`, which is the only way to anchor something deleted;
|
|
1822
|
+
* absent means the working tree.
|
|
1823
|
+
*/
|
|
1824
|
+
side: import_zod2.z.enum(["old", "new"]).optional(),
|
|
1825
|
+
/**
|
|
1826
|
+
* Which repository the file lives in — a remote URL
|
|
1827
|
+
* (`https://github.com/org/name`) or a short name. Absent means the base's
|
|
1828
|
+
* own repository, which is what nearly every anchor means.
|
|
1829
|
+
*
|
|
1830
|
+
* Unvalidated beyond not-blank: one repository has many spellings, matched
|
|
1831
|
+
* after normalisation. Only a full URL can be fetched from, so `validate`
|
|
1832
|
+
* warns on a short one; see ARCHITECTURE.
|
|
1833
|
+
*/
|
|
1834
|
+
repo: import_zod2.z.string().trim().min(1).optional(),
|
|
1835
|
+
/**
|
|
1836
|
+
* The git rev the evidence was taken at. Prefer a commit SHA: a branch
|
|
1837
|
+
* name is a moving pointer, so an anchor pinned to one says the evidence
|
|
1838
|
+
* came from wherever that branch happens to be now, which is not a
|
|
1839
|
+
* baseline. A foreign anchor is checked at this rev, and compared against
|
|
1840
|
+
* the remote's default branch on top of it.
|
|
1841
|
+
*/
|
|
1842
|
+
ref: import_zod2.z.string().trim().min(1).optional(),
|
|
1843
|
+
hash: import_zod2.z.string().regex(/^sha256:[0-9a-f]{64}$/, {
|
|
1844
|
+
message: "hash must be sha256:<64 hex chars>"
|
|
1845
|
+
}).optional(),
|
|
1846
|
+
/**
|
|
1847
|
+
* What `hash` was taken over: the span's raw text, or the normalised token
|
|
1848
|
+
* stream a parser sees (`ast`). Absent means `raw`, which is what every
|
|
1849
|
+
* anchor stamped before this field carries, so old hashes keep comparing
|
|
1850
|
+
* the way they were written. An `ast` hash is blind to whitespace and
|
|
1851
|
+
* comments, so reformatting the anchored code is not drift.
|
|
1852
|
+
*/
|
|
1853
|
+
hash_kind: import_zod2.z.enum(["raw", "ast"]).optional(),
|
|
1854
|
+
/** ISO 8601 timestamp of the last successful resolution. */
|
|
1855
|
+
resolved_at: import_zod2.z.string().min(1).optional(),
|
|
1856
|
+
/** Line count of the text the hash was taken over. */
|
|
1857
|
+
lines: import_zod2.z.number().int().positive().optional(),
|
|
1858
|
+
/**
|
|
1859
|
+
* Which resolver produced the hashed span. Absent means an anchor stamped
|
|
1860
|
+
* before resolvers were named, which is read as `regex` — the only one
|
|
1861
|
+
* there was. A hash from a different resolver is drift, not a match.
|
|
1862
|
+
*/
|
|
1863
|
+
resolver: import_zod2.z.enum(["tree-sitter", "regex", "span"]).optional()
|
|
1864
|
+
}).strict();
|
|
1865
|
+
var kbAnchorWriteSchema = kbAnchorSchema.superRefine((anchor, ctx) => {
|
|
1866
|
+
if (anchor.span && anchor.symbol) {
|
|
1867
|
+
ctx.addIssue({
|
|
1868
|
+
code: import_zod2.z.ZodIssueCode.custom,
|
|
1869
|
+
path: ["span"],
|
|
1870
|
+
message: "an anchor names a symbol or a span, not both"
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
1873
|
+
if (anchor.span && anchor.span.end < anchor.span.start) {
|
|
1874
|
+
ctx.addIssue({
|
|
1875
|
+
code: import_zod2.z.ZodIssueCode.custom,
|
|
1876
|
+
path: ["span", "end"],
|
|
1877
|
+
message: "span end must not precede start"
|
|
1878
|
+
});
|
|
1879
|
+
}
|
|
1880
|
+
if (anchor.span && anchor.hash_kind === "ast") {
|
|
1881
|
+
ctx.addIssue({
|
|
1882
|
+
code: import_zod2.z.ZodIssueCode.custom,
|
|
1883
|
+
path: ["hash_kind"],
|
|
1884
|
+
message: "a span is hashed raw, never ast"
|
|
1885
|
+
});
|
|
1886
|
+
}
|
|
1887
|
+
if (anchor.side === "old" && !anchor.ref) {
|
|
1888
|
+
ctx.addIssue({
|
|
1889
|
+
code: import_zod2.z.ZodIssueCode.custom,
|
|
1890
|
+
path: ["ref"],
|
|
1891
|
+
message: 'side: "old" needs a ref \u2014 committed code has no other address'
|
|
1892
|
+
});
|
|
1893
|
+
}
|
|
1894
|
+
});
|
|
1895
|
+
var kbAnchorLocatorSchema = kbAnchorSchema.pick({
|
|
1896
|
+
file: true,
|
|
1897
|
+
symbol: true,
|
|
1898
|
+
span: true,
|
|
1899
|
+
side: true,
|
|
1900
|
+
repo: true,
|
|
1901
|
+
ref: true
|
|
1902
|
+
});
|
|
1903
|
+
var kbLinkSchema = import_zod2.z.object({
|
|
1904
|
+
target: import_zod2.z.string().min(1),
|
|
1905
|
+
rel: import_zod2.z.string().min(1)
|
|
1906
|
+
}).passthrough();
|
|
1907
|
+
var KB_RECORD_TYPES = [
|
|
1908
|
+
"fact",
|
|
1909
|
+
"requirement",
|
|
1910
|
+
"constraint",
|
|
1911
|
+
"decision",
|
|
1912
|
+
"assumption",
|
|
1913
|
+
"open-question",
|
|
1914
|
+
"risk",
|
|
1915
|
+
"contract",
|
|
1916
|
+
"flow",
|
|
1917
|
+
"affected-system",
|
|
1918
|
+
"test-obligation",
|
|
1919
|
+
"source-note"
|
|
1920
|
+
];
|
|
1921
|
+
var KB_SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
1922
|
+
var KB_CONCEPT_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*\.[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
1923
|
+
var kbConceptIdSchema = import_zod2.z.string().regex(KB_CONCEPT_ID_PATTERN, {
|
|
1924
|
+
message: "concept id must be <type>.<slug>, both kebab-case"
|
|
1925
|
+
});
|
|
1926
|
+
var KB_RECORD_STATUSES = [
|
|
1927
|
+
"draft",
|
|
1928
|
+
"proposed",
|
|
1929
|
+
"accepted",
|
|
1930
|
+
"open",
|
|
1931
|
+
"resolved",
|
|
1932
|
+
"rejected",
|
|
1933
|
+
"superseded"
|
|
1934
|
+
];
|
|
1935
|
+
var KB_MATERIALITIES = [
|
|
1936
|
+
"blocking",
|
|
1937
|
+
"important",
|
|
1938
|
+
"non-blocking"
|
|
1939
|
+
];
|
|
1940
|
+
var KB_CONFIDENCES = ["low", "medium", "high"];
|
|
1941
|
+
var kbRecordFrontmatterSchema = import_zod2.z.object({
|
|
1942
|
+
// OKF: the only always-required key. A concept carrying just `type` is
|
|
1943
|
+
// fully conformant, so everything below stays optional.
|
|
1944
|
+
type: import_zod2.z.string().min(1),
|
|
1945
|
+
// OKF recommended.
|
|
1946
|
+
title: import_zod2.z.string().min(1).optional(),
|
|
1947
|
+
description: import_zod2.z.string().min(1).optional(),
|
|
1948
|
+
resource: import_zod2.z.string().min(1).optional(),
|
|
1949
|
+
tags: import_zod2.z.array(import_zod2.z.string()).optional(),
|
|
1950
|
+
// OKF optional: provenance and freshness.
|
|
1951
|
+
sources: import_zod2.z.array(kbSourceSchema).optional(),
|
|
1952
|
+
generated: kbActorStampSchema.optional(),
|
|
1953
|
+
verified: import_zod2.z.array(kbActorStampSchema).optional(),
|
|
1954
|
+
stale_after: import_zod2.z.string().min(1).optional(),
|
|
1955
|
+
// strauss extensions — see the module comment.
|
|
1956
|
+
strauss_anchors: import_zod2.z.array(kbAnchorSchema).optional(),
|
|
1957
|
+
strauss_verify: import_zod2.z.array(import_zod2.z.string().min(1)).optional(),
|
|
1958
|
+
// Typed causal edges, source → target, living on the source. `A depends_on
|
|
1959
|
+
// B` means A needs B, so `kb_impact` walks these inbound: what breaks if B
|
|
1960
|
+
// changes is whatever declared a dependence on it.
|
|
1961
|
+
strauss_links: import_zod2.z.array(kbLinkSchema).optional(),
|
|
1962
|
+
// Total after parsing, tolerant before it. Our producers must supply a
|
|
1963
|
+
// status — an absent one would leave every reader inventing its own default
|
|
1964
|
+
// — but OKF calls a concept carrying only `type` fully conformant, so
|
|
1965
|
+
// rejecting a foreign record for the lack of one would put us outside the
|
|
1966
|
+
// spec. The default resolves it in the single place that can: here.
|
|
1967
|
+
strauss_status: import_zod2.z.enum(KB_RECORD_STATUSES).default("draft"),
|
|
1968
|
+
strauss_supersedes: import_zod2.z.array(import_zod2.z.string().min(1)).optional(),
|
|
1969
|
+
strauss_superseded_by: import_zod2.z.string().min(1).optional(),
|
|
1970
|
+
strauss_answered: kbActorStampSchema.optional(),
|
|
1971
|
+
strauss_materiality: import_zod2.z.enum(KB_MATERIALITIES).optional(),
|
|
1972
|
+
strauss_confidence: import_zod2.z.enum(KB_CONFIDENCES).optional(),
|
|
1973
|
+
strauss_owner: import_zod2.z.string().min(1).optional(),
|
|
1974
|
+
// "No source exists" as a field rather than a sentinel entry inside
|
|
1975
|
+
// `sources`. A sentinel in a reference list is a value doing work a field
|
|
1976
|
+
// should do; as a field, `sources` may be legitimately empty.
|
|
1977
|
+
strauss_assumption: import_zod2.z.boolean().optional()
|
|
1978
|
+
}).passthrough();
|
|
1979
|
+
|
|
1980
|
+
// src/errors.ts
|
|
1981
|
+
var BaseError = class extends Error {
|
|
1982
|
+
code;
|
|
1983
|
+
errorType;
|
|
1984
|
+
fault;
|
|
1985
|
+
retriable;
|
|
1986
|
+
reportToUser;
|
|
1987
|
+
details;
|
|
1988
|
+
constructor(props) {
|
|
1989
|
+
super(props.message);
|
|
1990
|
+
this.name = props.name ?? this.constructor.name;
|
|
1991
|
+
this.code = props.code ?? 500;
|
|
1992
|
+
this.errorType = props.errorType;
|
|
1993
|
+
this.fault = props.fault;
|
|
1994
|
+
this.retriable = props.retriable ?? true;
|
|
1995
|
+
this.reportToUser = props.reportToUser ?? false;
|
|
1996
|
+
this.details = props.details;
|
|
1997
|
+
}
|
|
1998
|
+
};
|
|
1999
|
+
|
|
2000
|
+
// src/anchors/errors.ts
|
|
2001
|
+
function locatorText(locator) {
|
|
2002
|
+
const span2 = locator.span ? `:${locator.span.start}-${locator.span.end}` : "";
|
|
2003
|
+
const symbol = locator.symbol ? `:${cap(locator.symbol)}` : "";
|
|
2004
|
+
const repo = locator.repo ? `${cap(locator.repo)}@` : "";
|
|
2005
|
+
const ref = locator.ref ? `@${cap(locator.ref)}` : "";
|
|
2006
|
+
return `${repo}${cap(locator.file)}${symbol}${span2}${ref}`;
|
|
1972
2007
|
}
|
|
1973
|
-
|
|
1974
|
-
|
|
2008
|
+
var FIELD_CAP = 120;
|
|
2009
|
+
function cap(value) {
|
|
2010
|
+
return value.length > FIELD_CAP ? `${value.slice(0, FIELD_CAP - 1)}\u2026` : value;
|
|
1975
2011
|
}
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
}
|
|
1989
|
-
};
|
|
2012
|
+
var KbAnchorSetDuplicateError = class extends BaseError {
|
|
2013
|
+
constructor(locator) {
|
|
2014
|
+
super({
|
|
2015
|
+
message: `kb: ${locator} appears twice in this set \u2014 a record holds each pointer once`,
|
|
2016
|
+
errorType: "KbAnchorSetDuplicate" /* KbAnchorSetDuplicate */,
|
|
2017
|
+
code: 400,
|
|
2018
|
+
fault: "User" /* User */,
|
|
2019
|
+
retriable: false,
|
|
2020
|
+
reportToUser: true,
|
|
2021
|
+
details: { locator, action: "refused" }
|
|
2022
|
+
});
|
|
2023
|
+
this.locator = locator;
|
|
1990
2024
|
}
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2025
|
+
locator;
|
|
2026
|
+
};
|
|
2027
|
+
|
|
2028
|
+
// src/anchors/apply.ts
|
|
2029
|
+
var LOCATOR_FIELDS = [
|
|
2030
|
+
"file",
|
|
2031
|
+
"symbol",
|
|
2032
|
+
"span",
|
|
2033
|
+
"side",
|
|
2034
|
+
"repo",
|
|
2035
|
+
"ref"
|
|
2036
|
+
];
|
|
2037
|
+
function applyAnchorSet(current, incoming) {
|
|
2038
|
+
const anchors = incoming.map((anchor) => ({ ...anchor }));
|
|
2039
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2040
|
+
for (const anchor of anchors) {
|
|
2041
|
+
const key2 = locatorKey(anchor);
|
|
2042
|
+
if (seen.has(key2)) {
|
|
2043
|
+
throw new KbAnchorSetDuplicateError(locatorText(locatorOf(anchor)));
|
|
2003
2044
|
}
|
|
2004
|
-
|
|
2005
|
-
return {
|
|
2006
|
-
ok: true,
|
|
2007
|
-
span: attempt.span,
|
|
2008
|
-
...isResolverName(resolver.name) ? { resolver: resolver.name } : {},
|
|
2009
|
-
...tokens2 ? { normalized: tokens2 } : {}
|
|
2010
|
-
};
|
|
2045
|
+
seen.add(key2);
|
|
2011
2046
|
}
|
|
2012
|
-
return {
|
|
2047
|
+
return { anchors, changes: diff(current, anchors) };
|
|
2013
2048
|
}
|
|
2014
|
-
function
|
|
2015
|
-
const
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2049
|
+
function diff(current, next) {
|
|
2050
|
+
const before = new Map(
|
|
2051
|
+
current.filter((anchor) => anchor.hash).map((a) => [a.hash, a])
|
|
2052
|
+
);
|
|
2053
|
+
const beforeLocators = new Map(current.map((a) => [locatorKey(a), a]));
|
|
2054
|
+
const afterLocators = new Set(next.map((anchor) => locatorKey(anchor)));
|
|
2055
|
+
const moved = /* @__PURE__ */ new Set();
|
|
2056
|
+
const changes = [];
|
|
2057
|
+
for (const anchor of next) {
|
|
2058
|
+
const source = anchor.hash ? before.get(anchor.hash) : void 0;
|
|
2059
|
+
if (source) {
|
|
2060
|
+
if (locatorKey(source) === locatorKey(anchor)) continue;
|
|
2061
|
+
moved.add(locatorKey(source));
|
|
2062
|
+
changes.push({
|
|
2063
|
+
op: "move",
|
|
2064
|
+
from: locatorOf(source),
|
|
2065
|
+
to: locatorOf(anchor)
|
|
2066
|
+
});
|
|
2067
|
+
continue;
|
|
2068
|
+
}
|
|
2069
|
+
if (beforeLocators.has(locatorKey(anchor))) continue;
|
|
2070
|
+
changes.push({ op: "add", to: locatorOf(anchor) });
|
|
2019
2071
|
}
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
},
|
|
2027
|
-
resolver: "span"
|
|
2028
|
-
};
|
|
2029
|
-
}
|
|
2030
|
-
function fromResolve(resolver, source, symbol, file) {
|
|
2031
|
-
const span2 = resolver.resolve(source, symbol, file);
|
|
2032
|
-
return span2 ? { kind: "resolved", span: span2 } : { kind: "unresolved", reason: "symbol-not-found" };
|
|
2033
|
-
}
|
|
2034
|
-
function isResolverName(name) {
|
|
2035
|
-
return name === "tree-sitter" || name === "regex" || name === "span";
|
|
2072
|
+
for (const anchor of current) {
|
|
2073
|
+
const key2 = locatorKey(anchor);
|
|
2074
|
+
if (afterLocators.has(key2) || moved.has(key2)) continue;
|
|
2075
|
+
changes.push({ op: "drop", from: locatorOf(anchor) });
|
|
2076
|
+
}
|
|
2077
|
+
return changes;
|
|
2036
2078
|
}
|
|
2037
|
-
|
|
2038
|
-
|
|
2079
|
+
function locatorOf(anchor) {
|
|
2080
|
+
return kbAnchorLocatorSchema.parse(
|
|
2081
|
+
Object.fromEntries(
|
|
2082
|
+
LOCATOR_FIELDS.flatMap(
|
|
2083
|
+
(field) => anchor[field] === void 0 ? [] : [[field, anchor[field]]]
|
|
2084
|
+
)
|
|
2085
|
+
)
|
|
2086
|
+
);
|
|
2039
2087
|
}
|
|
2040
|
-
function
|
|
2041
|
-
return [
|
|
2088
|
+
function locatorKey(anchor) {
|
|
2089
|
+
return JSON.stringify([
|
|
2090
|
+
anchor.file,
|
|
2091
|
+
anchor.symbol ?? "",
|
|
2092
|
+
anchor.span ? `${anchor.span.start}-${anchor.span.end}` : "",
|
|
2093
|
+
anchor.side ?? "new",
|
|
2094
|
+
anchor.repo === void 0 ? "" : normalizeRepoUrl(anchor.repo),
|
|
2095
|
+
anchor.ref ?? ""
|
|
2096
|
+
]);
|
|
2042
2097
|
}
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2098
|
+
|
|
2099
|
+
// src/record-types.ts
|
|
2100
|
+
var RECORD_TYPES = {
|
|
2101
|
+
fact: {
|
|
2102
|
+
purpose: "Observed or sourced fact",
|
|
2103
|
+
sections: ["Claim", "Evidence", "Implication"],
|
|
2104
|
+
initialStatus: "accepted"
|
|
2105
|
+
},
|
|
2106
|
+
requirement: {
|
|
2107
|
+
purpose: "Required behavior or outcome",
|
|
2108
|
+
sections: ["Claim", "Evidence", "Implication"],
|
|
2109
|
+
initialStatus: "proposed"
|
|
2110
|
+
},
|
|
2111
|
+
constraint: {
|
|
2112
|
+
purpose: "Limitation, compatibility boundary, policy, or restriction",
|
|
2113
|
+
sections: ["Claim", "Evidence", "Implication"],
|
|
2114
|
+
initialStatus: "accepted"
|
|
2115
|
+
},
|
|
2116
|
+
decision: {
|
|
2117
|
+
purpose: "Chosen or proposed direction",
|
|
2118
|
+
sections: ["Decision", "Rationale", "Rejected", "Impact"],
|
|
2119
|
+
initialStatus: "accepted"
|
|
2120
|
+
},
|
|
2121
|
+
assumption: {
|
|
2122
|
+
purpose: "Unsourced or not-yet-confirmed working assumption",
|
|
2123
|
+
sections: ["Claim", "Why we think so", "What would falsify it"],
|
|
2124
|
+
initialStatus: "draft"
|
|
2125
|
+
},
|
|
2126
|
+
"open-question": {
|
|
2127
|
+
purpose: "Question needing resolution",
|
|
2128
|
+
sections: ["Question", "Why it matters", "Default assumption"],
|
|
2129
|
+
initialStatus: "open"
|
|
2130
|
+
},
|
|
2131
|
+
risk: {
|
|
2132
|
+
purpose: "Something that can go wrong",
|
|
2133
|
+
sections: ["Risk", "Why it matters", "Mitigation", "Verification"],
|
|
2134
|
+
initialStatus: "open"
|
|
2135
|
+
},
|
|
2136
|
+
contract: {
|
|
2137
|
+
purpose: "API, data, event, schema, or permission contract",
|
|
2138
|
+
sections: ["Contract", "Producer", "Consumer", "Compatibility"],
|
|
2139
|
+
initialStatus: "proposed"
|
|
2140
|
+
},
|
|
2141
|
+
flow: {
|
|
2142
|
+
purpose: "Sequence, lifecycle, or state behavior",
|
|
2143
|
+
sections: ["Flow", "Trigger", "Steps", "Failure modes"],
|
|
2144
|
+
initialStatus: "accepted"
|
|
2145
|
+
},
|
|
2146
|
+
"affected-system": {
|
|
2147
|
+
purpose: "Component, service, package, integration, or external system",
|
|
2148
|
+
sections: ["System", "How it is affected", "Blast radius"],
|
|
2149
|
+
initialStatus: "accepted"
|
|
2150
|
+
},
|
|
2151
|
+
"test-obligation": {
|
|
2152
|
+
purpose: "Behavior or contract that must be verified",
|
|
2153
|
+
sections: ["Obligation", "Why it matters", "How to verify"],
|
|
2154
|
+
initialStatus: "open"
|
|
2155
|
+
},
|
|
2156
|
+
"source-note": {
|
|
2157
|
+
purpose: "Extracted note from source material",
|
|
2158
|
+
sections: ["Note", "Where it came from"],
|
|
2159
|
+
initialStatus: "accepted"
|
|
2160
|
+
}
|
|
2161
|
+
};
|
|
2162
|
+
function isKbRecordType(value) {
|
|
2163
|
+
return Object.prototype.hasOwnProperty.call(RECORD_TYPES, value);
|
|
2052
2164
|
}
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2165
|
+
var KB_LINK_RELS = [
|
|
2166
|
+
"depends_on",
|
|
2167
|
+
"constrains",
|
|
2168
|
+
"informs",
|
|
2169
|
+
"blocks",
|
|
2170
|
+
"invalidates",
|
|
2171
|
+
"verified_by",
|
|
2172
|
+
"satisfies",
|
|
2173
|
+
"related_to"
|
|
2174
|
+
];
|
|
2175
|
+
var LINK_RELS = {
|
|
2176
|
+
depends_on: {
|
|
2177
|
+
purpose: "The source needs the target to hold; the source breaks if the target changes",
|
|
2178
|
+
phrase: "Depends on",
|
|
2179
|
+
dependant: "source"
|
|
2180
|
+
},
|
|
2181
|
+
constrains: {
|
|
2182
|
+
purpose: "The source bounds what the target may do; the target breaks if the constraint changes",
|
|
2183
|
+
phrase: "Constrains",
|
|
2184
|
+
dependant: "target"
|
|
2185
|
+
},
|
|
2186
|
+
informs: {
|
|
2187
|
+
purpose: "The source shaped the target without binding it; the target is what needs revisiting",
|
|
2188
|
+
phrase: "Informs",
|
|
2189
|
+
dependant: "target"
|
|
2190
|
+
},
|
|
2191
|
+
blocks: {
|
|
2192
|
+
purpose: "The target cannot proceed until the source is settled; the target is what waits",
|
|
2193
|
+
phrase: "Blocks",
|
|
2194
|
+
dependant: "target"
|
|
2195
|
+
},
|
|
2196
|
+
invalidates: {
|
|
2197
|
+
purpose: "The source makes the target no longer hold; the target is what stops holding",
|
|
2198
|
+
phrase: "Invalidates",
|
|
2199
|
+
dependant: "target"
|
|
2200
|
+
},
|
|
2201
|
+
verified_by: {
|
|
2202
|
+
purpose: "The target is the check that confirms the source; the source's confirmation moves with it",
|
|
2203
|
+
phrase: "Verified by",
|
|
2204
|
+
dependant: "source"
|
|
2205
|
+
},
|
|
2206
|
+
satisfies: {
|
|
2207
|
+
purpose: "The source discharges the target's requirement; the source must change if the requirement does",
|
|
2208
|
+
phrase: "Satisfies",
|
|
2209
|
+
dependant: "source"
|
|
2210
|
+
},
|
|
2211
|
+
related_to: {
|
|
2212
|
+
purpose: "A pointer worth following, with no claim of dependence",
|
|
2213
|
+
phrase: "Relates to",
|
|
2214
|
+
dependant: null
|
|
2056
2215
|
}
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2216
|
+
};
|
|
2217
|
+
var KB_CAUSAL_LINK_RELS = KB_LINK_RELS.filter(
|
|
2218
|
+
(rel) => LINK_RELS[rel].dependant !== null
|
|
2219
|
+
);
|
|
2220
|
+
function isKbLinkRel(value) {
|
|
2221
|
+
return Object.prototype.hasOwnProperty.call(LINK_RELS, value);
|
|
2060
2222
|
}
|
|
2061
2223
|
|
|
2062
|
-
// src/
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2224
|
+
// src/compose.ts
|
|
2225
|
+
var composeLinkSchema = import_zod3.z.object({
|
|
2226
|
+
target: kbConceptIdSchema,
|
|
2227
|
+
rel: import_zod3.z.enum(KB_LINK_RELS)
|
|
2228
|
+
}).strict();
|
|
2229
|
+
var composeInputSchema = import_zod3.z.object({
|
|
2230
|
+
slug: import_zod3.z.string().min(1),
|
|
2231
|
+
/** One line, in the reader's terms. Becomes OKF `title`. */
|
|
2232
|
+
title: import_zod3.z.string().min(1),
|
|
2233
|
+
/** The consequence — what breaks if this is wrong. Becomes `description`. */
|
|
2234
|
+
why: import_zod3.z.string().min(1),
|
|
2235
|
+
/** Keyed by section heading from the type's spec. Unknown keys rejected. */
|
|
2236
|
+
sections: import_zod3.z.record(import_zod3.z.string(), import_zod3.z.string().min(1)).optional(),
|
|
2237
|
+
anchors: import_zod3.z.array(kbAnchorWriteSchema).optional(),
|
|
2238
|
+
sources: import_zod3.z.array(kbSourceSchema).optional(),
|
|
2239
|
+
/** No source exists, as a claim rather than a sentinel in `sources`. */
|
|
2240
|
+
assumption: import_zod3.z.boolean().optional(),
|
|
2241
|
+
/**
|
|
2242
|
+
* OKF `stale_after`: the absolute date this record stops being trusted.
|
|
2243
|
+
* Anything the outside world can change — pricing, quotas, versions,
|
|
2244
|
+
* reception counts — should carry one.
|
|
2245
|
+
*/
|
|
2246
|
+
stale_after: import_zod3.z.string().regex(/^\d{4}-\d{2}-\d{2}$/, {
|
|
2247
|
+
message: "stale_after must be YYYY-MM-DD"
|
|
2248
|
+
}).refine((date) => !Number.isNaN(Date.parse(date)), {
|
|
2249
|
+
message: "stale_after must be a real date"
|
|
2250
|
+
}).optional(),
|
|
2251
|
+
verify: import_zod3.z.array(import_zod3.z.string().min(1)).optional(),
|
|
2252
|
+
tags: import_zod3.z.array(import_zod3.z.string().min(1)).optional(),
|
|
2253
|
+
/** Concept ids this record relates to; rendered as body links. */
|
|
2254
|
+
relatedConceptIds: import_zod3.z.array(kbConceptIdSchema).optional(),
|
|
2255
|
+
/**
|
|
2256
|
+
* Typed causal edges, source → target: `{ target: "fact.b", rel:
|
|
2257
|
+
* "depends_on" }` on record A says A needs B. Stored in frontmatter and
|
|
2258
|
+
* also rendered as one prose sentence each, so the meaning survives a
|
|
2259
|
+
* reader that knows only OKF. The vocabulary goes into the description from
|
|
2260
|
+
* the same table the walk uses, so `kb_schema` emits it.
|
|
2261
|
+
*/
|
|
2262
|
+
links: import_zod3.z.array(composeLinkSchema).max(64).optional().describe(
|
|
2263
|
+
`Typed causal edges, source \u2192 target \u2014 a link on this record says this record <rel> the target. ${KB_LINK_RELS.map(
|
|
2264
|
+
(rel) => `${rel}: ${LINK_RELS[rel].purpose}`
|
|
2265
|
+
).join("; ")}.`
|
|
2266
|
+
),
|
|
2267
|
+
/** Concept ids this record replaces. The store settles the backlinks. */
|
|
2268
|
+
supersedes: import_zod3.z.array(kbConceptIdSchema).max(32).optional(),
|
|
2269
|
+
materiality: import_zod3.z.enum(KB_MATERIALITIES).optional(),
|
|
2270
|
+
confidence: import_zod3.z.enum(KB_CONFIDENCES).optional(),
|
|
2271
|
+
owner: import_zod3.z.string().min(1).optional()
|
|
2272
|
+
}).strict();
|
|
2273
|
+
function composeRecord(type, input, writtenBy, writtenAt) {
|
|
2274
|
+
const parsed = composeInputSchema.parse(input);
|
|
2275
|
+
const spec = RECORD_TYPES[type];
|
|
2276
|
+
const sections = parsed.sections ?? {};
|
|
2277
|
+
const unknown = Object.keys(sections).filter(
|
|
2278
|
+
(heading) => !spec.sections.includes(heading)
|
|
2279
|
+
);
|
|
2280
|
+
if (unknown.length) {
|
|
2281
|
+
throw new Error(
|
|
2282
|
+
`kb: ${type} has no section ${unknown.join(", ")} \u2014 expected one of ${spec.sections.join(", ")}`
|
|
2074
2283
|
);
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2284
|
+
}
|
|
2285
|
+
const frontmatter = {
|
|
2286
|
+
title: parsed.title,
|
|
2287
|
+
description: parsed.why,
|
|
2288
|
+
generated: { by: writtenBy, at: writtenAt },
|
|
2289
|
+
// Empty rather than absent: a later verification pass appends here, and an
|
|
2290
|
+
// empty list says "not yet verified" where a missing key would only say
|
|
2291
|
+
// "this producer didn't think about it".
|
|
2292
|
+
verified: [],
|
|
2293
|
+
strauss_status: spec.initialStatus
|
|
2294
|
+
};
|
|
2295
|
+
if (parsed.stale_after) frontmatter.stale_after = parsed.stale_after;
|
|
2296
|
+
if (parsed.anchors?.length) {
|
|
2297
|
+
frontmatter.strauss_anchors = applyAnchorSet([], parsed.anchors).anchors;
|
|
2298
|
+
}
|
|
2299
|
+
if (parsed.verify?.length) frontmatter.strauss_verify = parsed.verify;
|
|
2300
|
+
if (parsed.tags?.length) frontmatter.tags = parsed.tags;
|
|
2301
|
+
if (parsed.sources?.length) frontmatter.sources = parsed.sources;
|
|
2302
|
+
if (parsed.assumption) frontmatter.strauss_assumption = true;
|
|
2303
|
+
if (parsed.materiality) frontmatter.strauss_materiality = parsed.materiality;
|
|
2304
|
+
if (parsed.confidence) frontmatter.strauss_confidence = parsed.confidence;
|
|
2305
|
+
if (parsed.owner) frontmatter.strauss_owner = parsed.owner;
|
|
2306
|
+
if (parsed.supersedes?.length)
|
|
2307
|
+
frontmatter.strauss_supersedes = parsed.supersedes;
|
|
2308
|
+
const selfLink = parsed.links?.find(
|
|
2309
|
+
(link2) => link2.target === `${type}.${parsed.slug}`
|
|
2310
|
+
);
|
|
2311
|
+
if (selfLink) {
|
|
2312
|
+
throw new Error(
|
|
2313
|
+
`kb: ${type}.${parsed.slug} cannot ${selfLink.rel} itself \u2014 a link must name another record`
|
|
2080
2314
|
);
|
|
2081
2315
|
}
|
|
2082
|
-
if (
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2316
|
+
if (parsed.links?.length) frontmatter.strauss_links = parsed.links;
|
|
2317
|
+
const blocks = [];
|
|
2318
|
+
for (const heading of spec.sections) {
|
|
2319
|
+
const text = sections[heading];
|
|
2320
|
+
if (text) blocks.push(`## ${heading}
|
|
2321
|
+
|
|
2322
|
+
${text}`);
|
|
2088
2323
|
}
|
|
2089
|
-
|
|
2090
|
-
const
|
|
2091
|
-
|
|
2092
|
-
for (const entries of planned.values()) {
|
|
2093
|
-
for (const { anchor, foreign } of entries) {
|
|
2094
|
-
if (foreign) wants.push(...remoteWants(anchor));
|
|
2095
|
-
else if (anchor.side === "old") committedWants.push(anchor);
|
|
2096
|
-
else files.push(anchor.file);
|
|
2097
|
-
}
|
|
2324
|
+
if (!blocks.length) blocks.push(parsed.why);
|
|
2325
|
+
for (const related of parsed.relatedConceptIds ?? []) {
|
|
2326
|
+
blocks.push(`Relates to [${related}](${related}.md).`);
|
|
2098
2327
|
}
|
|
2099
|
-
const
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
options.concurrency ?? DEFAULT_IO_CONCURRENCY
|
|
2104
|
-
),
|
|
2105
|
-
readCommitted(repoRoot, committedWants, options),
|
|
2106
|
-
(options.readRemote ?? readRemoteAnchors)(wants, options.remote ?? {})
|
|
2107
|
-
]);
|
|
2108
|
-
await prepareResolvers(resolvers, [
|
|
2109
|
-
...files,
|
|
2110
|
-
...committedWants.map((anchor) => anchor.file),
|
|
2111
|
-
...wants.map((want) => want.file)
|
|
2112
|
-
]);
|
|
2113
|
-
const drift = /* @__PURE__ */ new Map();
|
|
2114
|
-
for (const record of records) {
|
|
2115
|
-
const entries = [];
|
|
2116
|
-
for (const { anchor, foreign } of planned.get(record.conceptId) ?? []) {
|
|
2117
|
-
if (foreign) {
|
|
2118
|
-
entries.push(remoteEntry(anchor, remote, resolvers));
|
|
2119
|
-
continue;
|
|
2120
|
-
}
|
|
2121
|
-
const read = anchor.side === "old" ? committed.get(atRefKey(anchor)) : reads.get(anchor.file);
|
|
2122
|
-
entries.push(localEntry(anchor, read, resolvers));
|
|
2123
|
-
}
|
|
2124
|
-
if (entries.length) drift.set(record.conceptId, entries);
|
|
2328
|
+
for (const link2 of parsed.links ?? []) {
|
|
2329
|
+
blocks.push(
|
|
2330
|
+
`${LINK_RELS[link2.rel].phrase} [${link2.target}](${link2.target}.md).`
|
|
2331
|
+
);
|
|
2125
2332
|
}
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
}
|
|
2131
|
-
async function readCommitted(repoRoot, anchors, options = {}) {
|
|
2132
|
-
if (!anchors.length) return /* @__PURE__ */ new Map();
|
|
2133
|
-
const read = options.readAtRef ?? readFileAtRef;
|
|
2134
|
-
const byKey = /* @__PURE__ */ new Map();
|
|
2135
|
-
for (const anchor of anchors) byKey.set(atRefKey(anchor), anchor);
|
|
2136
|
-
const keys = [...byKey.keys()];
|
|
2137
|
-
const results = await mapLimit(
|
|
2138
|
-
keys,
|
|
2139
|
-
options.concurrency ?? DEFAULT_IO_CONCURRENCY,
|
|
2140
|
-
(key2) => read(repoRoot, byKey.get(key2))
|
|
2141
|
-
);
|
|
2142
|
-
return new Map(keys.map((key2, at2) => [key2, results[at2]]));
|
|
2143
|
-
}
|
|
2144
|
-
function remoteWants(anchor) {
|
|
2145
|
-
const repo = anchor.repo;
|
|
2146
|
-
const wants = [{ repo, file: anchor.file }];
|
|
2147
|
-
if (anchor.ref) wants.unshift({ repo, ref: anchor.ref, file: anchor.file });
|
|
2148
|
-
return wants;
|
|
2149
|
-
}
|
|
2150
|
-
function base(anchor) {
|
|
2151
|
-
return {
|
|
2152
|
-
file: anchor.file,
|
|
2153
|
-
...anchor.symbol ? { symbol: anchor.symbol } : {},
|
|
2154
|
-
...anchor.side === "old" ? { side: "old" } : {},
|
|
2155
|
-
storedHash: anchor.hash
|
|
2156
|
-
};
|
|
2157
|
-
}
|
|
2158
|
-
function unresolved(anchor, reason, repo) {
|
|
2159
|
-
return {
|
|
2160
|
-
...base(anchor),
|
|
2161
|
-
state: "unresolved",
|
|
2162
|
-
diffSize: null,
|
|
2163
|
-
...reason ? { reason } : {},
|
|
2164
|
-
...repo ? { repo } : {},
|
|
2165
|
-
...classOf(reason)
|
|
2166
|
-
};
|
|
2167
|
-
}
|
|
2168
|
-
var GONE_REASONS = /* @__PURE__ */ new Set([
|
|
2169
|
-
"file-missing",
|
|
2170
|
-
"symbol-not-found",
|
|
2171
|
-
"span-out-of-range",
|
|
2172
|
-
"ref-unreadable"
|
|
2173
|
-
]);
|
|
2174
|
-
function provisionalDriftClass(entry) {
|
|
2175
|
-
if (entry.state === "unresolved") {
|
|
2176
|
-
return GONE_REASONS.has(entry.reason) ? "gone" : void 0;
|
|
2333
|
+
if (parsed.sources?.length) {
|
|
2334
|
+
blocks.push(
|
|
2335
|
+
parsed.sources.map((source) => `[^${source.id}]: ${source.title ?? source.resource}`).join("\n")
|
|
2336
|
+
);
|
|
2177
2337
|
}
|
|
2178
|
-
return entry.state === "drifted" ? "changed" : void 0;
|
|
2179
|
-
}
|
|
2180
|
-
function classOf(reason) {
|
|
2181
|
-
const settled = provisionalDriftClass({ state: "unresolved", reason });
|
|
2182
|
-
return settled ? { class: settled } : {};
|
|
2183
|
-
}
|
|
2184
|
-
function hashIn(source, anchor, resolvers) {
|
|
2185
|
-
const outcome = resolveAnchorSpan(source, anchor, resolvers);
|
|
2186
|
-
if (!outcome.ok) return { ok: false, reason: outcome.reason };
|
|
2187
|
-
const { hash, kind } = anchorHashOf(anchor, outcome);
|
|
2188
|
-
return {
|
|
2189
|
-
ok: true,
|
|
2190
|
-
current: {
|
|
2191
|
-
hash,
|
|
2192
|
-
kind,
|
|
2193
|
-
lines: outcome.span.endLine - outcome.span.startLine + 1,
|
|
2194
|
-
...outcome.resolver ? { resolver: outcome.resolver } : {}
|
|
2195
|
-
}
|
|
2196
|
-
};
|
|
2197
|
-
}
|
|
2198
|
-
function resolverExtras(source, anchor, current) {
|
|
2199
|
-
return {
|
|
2200
|
-
...current.resolver ? { resolver: current.resolver } : {},
|
|
2201
|
-
...current.hash !== anchor.hash && resolverChanged(source, anchor, current.resolver) ? { reason: "resolver-changed" } : {}
|
|
2202
|
-
};
|
|
2203
|
-
}
|
|
2204
|
-
function compared(anchor, current, extra = {}) {
|
|
2205
|
-
const matched = current.hash === anchor.hash;
|
|
2206
2338
|
return {
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
...matched ? {} : { class: "changed" },
|
|
2213
|
-
...extra
|
|
2339
|
+
type,
|
|
2340
|
+
slug: parsed.slug,
|
|
2341
|
+
frontmatter,
|
|
2342
|
+
body: `${blocks.join("\n\n")}
|
|
2343
|
+
`
|
|
2214
2344
|
};
|
|
2215
2345
|
}
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
}
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
repo,
|
|
2262
|
-
...extras,
|
|
2263
|
-
remoteState: "matches-ref"
|
|
2264
|
-
});
|
|
2346
|
+
|
|
2347
|
+
// src/decision-record.ts
|
|
2348
|
+
var DECISION_TYPE = "decision";
|
|
2349
|
+
var NO_DECISION_SLUG = "none";
|
|
2350
|
+
var decisionInputSchema = composeInputSchema.omit({ sections: true }).extend({
|
|
2351
|
+
alternative: import_zod4.z.string().min(1).optional(),
|
|
2352
|
+
impact: import_zod4.z.string().min(1).optional()
|
|
2353
|
+
}).strict();
|
|
2354
|
+
function composeDecisionRecord(input, writtenBy, writtenAt) {
|
|
2355
|
+
const { alternative, impact: impact2, ...rest } = input;
|
|
2356
|
+
return composeRecord(
|
|
2357
|
+
DECISION_TYPE,
|
|
2358
|
+
{
|
|
2359
|
+
...rest,
|
|
2360
|
+
sections: {
|
|
2361
|
+
Decision: input.title,
|
|
2362
|
+
Rationale: input.why,
|
|
2363
|
+
...alternative ? { Rejected: alternative } : {},
|
|
2364
|
+
...impact2 ? { Impact: impact2 } : {}
|
|
2365
|
+
}
|
|
2366
|
+
},
|
|
2367
|
+
writtenBy,
|
|
2368
|
+
writtenAt
|
|
2369
|
+
);
|
|
2370
|
+
}
|
|
2371
|
+
function composeNoDecisionRecord(reason, writtenBy, writtenAt) {
|
|
2372
|
+
return composeRecord(
|
|
2373
|
+
DECISION_TYPE,
|
|
2374
|
+
{
|
|
2375
|
+
slug: NO_DECISION_SLUG,
|
|
2376
|
+
title: "No decision to record",
|
|
2377
|
+
why: reason,
|
|
2378
|
+
sections: { Decision: reason }
|
|
2379
|
+
},
|
|
2380
|
+
writtenBy,
|
|
2381
|
+
writtenAt
|
|
2382
|
+
);
|
|
2383
|
+
}
|
|
2384
|
+
function isNoDecisionRecord(record) {
|
|
2385
|
+
return record.conceptId === `${DECISION_TYPE}.${NO_DECISION_SLUG}`;
|
|
2386
|
+
}
|
|
2387
|
+
function selectDecisions(records) {
|
|
2388
|
+
return records.filter(
|
|
2389
|
+
(record) => record.conceptId.startsWith(`${DECISION_TYPE}.`) && !isNoDecisionRecord(record)
|
|
2390
|
+
);
|
|
2265
2391
|
}
|
|
2266
2392
|
|
|
2267
|
-
// src/
|
|
2268
|
-
var
|
|
2269
|
-
code;
|
|
2270
|
-
errorType;
|
|
2271
|
-
fault;
|
|
2272
|
-
retriable;
|
|
2273
|
-
reportToUser;
|
|
2274
|
-
details;
|
|
2275
|
-
constructor(props) {
|
|
2276
|
-
super(props.message);
|
|
2277
|
-
this.name = props.name ?? this.constructor.name;
|
|
2278
|
-
this.code = props.code ?? 500;
|
|
2279
|
-
this.errorType = props.errorType;
|
|
2280
|
-
this.fault = props.fault;
|
|
2281
|
-
this.retriable = props.retriable ?? true;
|
|
2282
|
-
this.reportToUser = props.reportToUser ?? false;
|
|
2283
|
-
this.details = props.details;
|
|
2284
|
-
}
|
|
2285
|
-
};
|
|
2393
|
+
// src/commands/anchor-resolve.ts
|
|
2394
|
+
var import_zod7 = require("zod");
|
|
2286
2395
|
|
|
2287
2396
|
// src/kb-errors.ts
|
|
2288
2397
|
var KbRecordAlreadyExistsError = class extends BaseError {
|
|
@@ -2349,6 +2458,38 @@ var KbSelfVerificationError = class extends BaseError {
|
|
|
2349
2458
|
actor;
|
|
2350
2459
|
generatedBy;
|
|
2351
2460
|
};
|
|
2461
|
+
var KbInvalidActorError = class extends BaseError {
|
|
2462
|
+
constructor(actor, reason) {
|
|
2463
|
+
super({
|
|
2464
|
+
message: `kb: actor ${JSON.stringify(actor)} ${reason} \u2014 set STRAUSS_KB_ACTOR, e.g. human:alice or agent:reviewer`,
|
|
2465
|
+
errorType: "KbInvalidActor" /* KbInvalidActor */,
|
|
2466
|
+
code: 400,
|
|
2467
|
+
fault: "User" /* User */,
|
|
2468
|
+
retriable: false,
|
|
2469
|
+
reportToUser: true,
|
|
2470
|
+
details: { actor, reason, action: "refused" }
|
|
2471
|
+
});
|
|
2472
|
+
this.actor = actor;
|
|
2473
|
+
this.reason = reason;
|
|
2474
|
+
}
|
|
2475
|
+
actor;
|
|
2476
|
+
reason;
|
|
2477
|
+
};
|
|
2478
|
+
var KbFlagConflictError = class extends BaseError {
|
|
2479
|
+
constructor(flags) {
|
|
2480
|
+
super({
|
|
2481
|
+
message: `kb: ${flags.join(" and ")} cannot be combined`,
|
|
2482
|
+
errorType: "KbFlagConflict" /* KbFlagConflict */,
|
|
2483
|
+
code: 400,
|
|
2484
|
+
fault: "User" /* User */,
|
|
2485
|
+
retriable: false,
|
|
2486
|
+
reportToUser: true,
|
|
2487
|
+
details: { flags }
|
|
2488
|
+
});
|
|
2489
|
+
this.flags = flags;
|
|
2490
|
+
}
|
|
2491
|
+
flags;
|
|
2492
|
+
};
|
|
2352
2493
|
var KbPackBudgetExceededError = class extends BaseError {
|
|
2353
2494
|
constructor(recordCount, approxTokens2, budgetTokens, excluded) {
|
|
2354
2495
|
super({
|
|
@@ -2892,19 +3033,11 @@ function argvPositional(argv, ...names) {
|
|
|
2892
3033
|
}
|
|
2893
3034
|
|
|
2894
3035
|
// src/commands/anchor-resolve.ts
|
|
2895
|
-
function resolverSummary(results) {
|
|
2896
|
-
const names = [
|
|
2897
|
-
...new Set(
|
|
2898
|
-
results.flatMap((entry) => entry.resolver ? [entry.resolver] : [])
|
|
2899
|
-
)
|
|
2900
|
-
].sort();
|
|
2901
|
-
return names.length ? `${names.join(" + ")} resolver` : "whole-file";
|
|
2902
|
-
}
|
|
2903
3036
|
var anchorResolveCommand = define({
|
|
2904
3037
|
name: "anchor-resolve",
|
|
2905
3038
|
tool: "kb_anchor_resolve",
|
|
2906
|
-
usage: "anchor-resolve <concept-id> [--repo-root <path>] [--offline] [--rebaseline] [--restamp]",
|
|
2907
|
-
description: "Resolve a record's anchors: stamp a hash onto anchors that lack one, report drift where the code moved. An anchor naming another repository is read from that remote through a bare cache; --offline uses the cache only.
|
|
3039
|
+
usage: "anchor-resolve <concept-id> [--repo-root <path>] [--offline] [--rebaseline] [--restamp] [--check]",
|
|
3040
|
+
description: "Resolve a record's anchors: stamp a hash onto anchors that lack one, report drift where the code moved. An anchor naming another repository is read from that remote through a bare cache; --offline uses the cache only. Never writes verified[]; a judgment is kb_verify. Exits non-zero on drift.",
|
|
2908
3041
|
input: import_zod7.z.object({
|
|
2909
3042
|
bundlePath,
|
|
2910
3043
|
conceptId,
|
|
@@ -2917,6 +3050,9 @@ var anchorResolveCommand = define({
|
|
|
2917
3050
|
),
|
|
2918
3051
|
restamp: import_zod7.z.boolean().optional().describe(
|
|
2919
3052
|
"Refresh `resolved_at` on anchors that already match. Off by default, so a green run writes nothing."
|
|
3053
|
+
),
|
|
3054
|
+
check: import_zod7.z.boolean().optional().describe(
|
|
3055
|
+
"Resolve and report only: no hash, no `resolved_at`, no log entry."
|
|
2920
3056
|
)
|
|
2921
3057
|
}),
|
|
2922
3058
|
fromArgv: (argv, path) => ({
|
|
@@ -2925,9 +3061,24 @@ var anchorResolveCommand = define({
|
|
|
2925
3061
|
repoRoot: argvFlag(argv, "--repo-root"),
|
|
2926
3062
|
offline: argv.includes("--offline"),
|
|
2927
3063
|
rebaseline: argv.includes("--rebaseline"),
|
|
2928
|
-
restamp: argv.includes("--restamp")
|
|
3064
|
+
restamp: argv.includes("--restamp"),
|
|
3065
|
+
check: argv.includes("--check")
|
|
2929
3066
|
}),
|
|
2930
|
-
run: async ({ store, actor, now }, {
|
|
3067
|
+
run: async ({ store, actor, now }, {
|
|
3068
|
+
bundlePath: path,
|
|
3069
|
+
conceptId: id,
|
|
3070
|
+
repoRoot,
|
|
3071
|
+
offline,
|
|
3072
|
+
rebaseline,
|
|
3073
|
+
restamp,
|
|
3074
|
+
check
|
|
3075
|
+
}) => {
|
|
3076
|
+
if (check && (rebaseline || restamp)) {
|
|
3077
|
+
throw new KbFlagConflictError([
|
|
3078
|
+
"check",
|
|
3079
|
+
rebaseline ? "rebaseline" : "restamp"
|
|
3080
|
+
]);
|
|
3081
|
+
}
|
|
2931
3082
|
const root = repoRoot ?? process.cwd();
|
|
2932
3083
|
const record = await store.read(path, id);
|
|
2933
3084
|
if (!record) throw new KbRecordNotFoundError(id);
|
|
@@ -2936,7 +3087,6 @@ var anchorResolveCommand = define({
|
|
|
2936
3087
|
return {
|
|
2937
3088
|
conceptId: id,
|
|
2938
3089
|
results: [],
|
|
2939
|
-
verified: false,
|
|
2940
3090
|
note: "record has no anchors"
|
|
2941
3091
|
};
|
|
2942
3092
|
}
|
|
@@ -2994,7 +3144,7 @@ var anchorResolveCommand = define({
|
|
|
2994
3144
|
if (!anchor.hash) {
|
|
2995
3145
|
results.push({
|
|
2996
3146
|
...base2,
|
|
2997
|
-
state: "stamped",
|
|
3147
|
+
state: check ? "unstamped" : "stamped",
|
|
2998
3148
|
currentHash: stampedHash,
|
|
2999
3149
|
hashKind: stampedKind,
|
|
3000
3150
|
...producedBy ? { resolver: producedBy } : {}
|
|
@@ -3046,7 +3196,7 @@ var anchorResolveCommand = define({
|
|
|
3046
3196
|
if (refresh) dirty = true;
|
|
3047
3197
|
}
|
|
3048
3198
|
let frozen = false;
|
|
3049
|
-
if (dirty) {
|
|
3199
|
+
if (dirty && !check) {
|
|
3050
3200
|
try {
|
|
3051
3201
|
await assertBaseNotFrozen(process.cwd(), path);
|
|
3052
3202
|
} catch (error) {
|
|
@@ -3061,42 +3211,11 @@ var anchorResolveCommand = define({
|
|
|
3061
3211
|
const unreachable = results.filter(
|
|
3062
3212
|
(entry) => isUncheckedReason(entry.reason)
|
|
3063
3213
|
).length;
|
|
3064
|
-
const checked = results.length - unreachable;
|
|
3065
3214
|
const matches3 = results.filter((entry) => entry.state === "match").length;
|
|
3066
|
-
const note = `${matches3}/${
|
|
3067
|
-
const clean = checked > 0 && matches3 === checked && unreachable === 0;
|
|
3068
|
-
if (clean) {
|
|
3069
|
-
try {
|
|
3070
|
-
await store.verify(
|
|
3071
|
-
path,
|
|
3072
|
-
id,
|
|
3073
|
-
`anchor-resolve: ${note} (${resolverSummary(results)})`,
|
|
3074
|
-
actor,
|
|
3075
|
-
now()
|
|
3076
|
-
);
|
|
3077
|
-
} catch (error) {
|
|
3078
|
-
if (!(error instanceof KbSelfVerificationError)) throw error;
|
|
3079
|
-
return {
|
|
3080
|
-
conceptId: id,
|
|
3081
|
-
results,
|
|
3082
|
-
verified: false,
|
|
3083
|
-
verifyRefused: "self-verification",
|
|
3084
|
-
...frozenNote,
|
|
3085
|
-
...hintNote
|
|
3086
|
-
};
|
|
3087
|
-
}
|
|
3088
|
-
return {
|
|
3089
|
-
conceptId: id,
|
|
3090
|
-
results,
|
|
3091
|
-
verified: true,
|
|
3092
|
-
...frozenNote,
|
|
3093
|
-
...hintNote
|
|
3094
|
-
};
|
|
3095
|
-
}
|
|
3215
|
+
const note = `${matches3}/${results.length - unreachable} anchors match, ${unreachable} unreachable`;
|
|
3096
3216
|
return {
|
|
3097
3217
|
conceptId: id,
|
|
3098
3218
|
results,
|
|
3099
|
-
verified: false,
|
|
3100
3219
|
...unreachable ? { note } : {},
|
|
3101
3220
|
...frozenNote,
|
|
3102
3221
|
...hintNote
|
|
@@ -3183,14 +3302,117 @@ async function readSources(anchors, root, offline) {
|
|
|
3183
3302
|
return sources;
|
|
3184
3303
|
}
|
|
3185
3304
|
|
|
3186
|
-
// src/commands/
|
|
3305
|
+
// src/commands/anchor-set/model.ts
|
|
3187
3306
|
var import_zod8 = require("zod");
|
|
3307
|
+
var anchorSetInputSchema = import_zod8.z.object({
|
|
3308
|
+
reason: import_zod8.z.string().refine((text) => text.trim().length > 0, {
|
|
3309
|
+
message: "reason must say what was reviewed"
|
|
3310
|
+
}).describe(
|
|
3311
|
+
"What the reviewer read that makes these the right pointers. Recorded in the log."
|
|
3312
|
+
),
|
|
3313
|
+
anchors: import_zod8.z.array(kbAnchorWriteSchema).min(1).describe(
|
|
3314
|
+
"The complete new anchor set. Carry an anchor's hash forward to keep drift visible until the new code is read."
|
|
3315
|
+
)
|
|
3316
|
+
}).strict();
|
|
3317
|
+
var anchorSetCommandInput = import_zod8.z.object({
|
|
3318
|
+
bundlePath,
|
|
3319
|
+
conceptId,
|
|
3320
|
+
input: anchorSetInputSchema,
|
|
3321
|
+
resolve: import_zod8.z.boolean().optional().describe(
|
|
3322
|
+
"Also resolve and stamp every anchor against the current code, as anchor-resolve --rebaseline does."
|
|
3323
|
+
),
|
|
3324
|
+
repoRoot: import_zod8.z.string().min(1).optional().describe(
|
|
3325
|
+
"Where the anchored source lives, for resolve. Defaults to the working directory."
|
|
3326
|
+
),
|
|
3327
|
+
offline: import_zod8.z.boolean().optional().describe("With resolve, read foreign anchors from the repo cache only.")
|
|
3328
|
+
});
|
|
3329
|
+
|
|
3330
|
+
// src/commands/anchor-set/command.ts
|
|
3331
|
+
var NOTE = "pointers only: nothing was resolved or verified. Run anchor-resolve to check the new pointers, --rebaseline to accept the code, or pass resolve to do both here.";
|
|
3332
|
+
var STAMPED_NOTE = "pointers set and stamped against the current code. Not verification: run verify separately if someone reviewed it.";
|
|
3333
|
+
var anchorSetCommand = define({
|
|
3334
|
+
name: "anchor-set",
|
|
3335
|
+
tool: "kb_anchor_set",
|
|
3336
|
+
usage: "anchor-set <concept-id> [--resolve] [--repo-root <path>] [--offline] < anchors.json",
|
|
3337
|
+
description: "Set a record's code anchors after a reviewed refactor, with a reason. The array is the whole set. With resolve, every anchor is stamped against the current code in the same call; without it, run kb_anchor_resolve next. Recorded in the log, never verification.",
|
|
3338
|
+
input: anchorSetCommandInput,
|
|
3339
|
+
fromArgv: async (argv, path, stdin) => ({
|
|
3340
|
+
bundlePath: path,
|
|
3341
|
+
conceptId: argv[1],
|
|
3342
|
+
input: JSON.parse(await stdin()),
|
|
3343
|
+
resolve: argv.includes("--resolve"),
|
|
3344
|
+
repoRoot: argvFlag(argv, "--repo-root"),
|
|
3345
|
+
offline: argv.includes("--offline")
|
|
3346
|
+
}),
|
|
3347
|
+
run: async (ctx, { bundlePath: path, conceptId: id, input, resolve: resolve7, repoRoot, offline }) => {
|
|
3348
|
+
const { store, actor } = ctx;
|
|
3349
|
+
await assertBaseNotFrozen(process.cwd(), path);
|
|
3350
|
+
let applied;
|
|
3351
|
+
const record = await store.updateAnchors(
|
|
3352
|
+
path,
|
|
3353
|
+
id,
|
|
3354
|
+
(current) => {
|
|
3355
|
+
applied = applyAnchorSet(current, input.anchors);
|
|
3356
|
+
return {
|
|
3357
|
+
anchors: applied.anchors,
|
|
3358
|
+
log: {
|
|
3359
|
+
operation: "anchor-set",
|
|
3360
|
+
reason: input.reason,
|
|
3361
|
+
anchors: applied.changes
|
|
3362
|
+
}
|
|
3363
|
+
};
|
|
3364
|
+
},
|
|
3365
|
+
actor
|
|
3366
|
+
);
|
|
3367
|
+
const changes = applied?.changes ?? [];
|
|
3368
|
+
if (!resolve7) {
|
|
3369
|
+
return {
|
|
3370
|
+
conceptId: id,
|
|
3371
|
+
reason: input.reason,
|
|
3372
|
+
changes,
|
|
3373
|
+
anchors: record.frontmatter.strauss_anchors ?? [],
|
|
3374
|
+
baseline: "unchanged",
|
|
3375
|
+
note: NOTE
|
|
3376
|
+
};
|
|
3377
|
+
}
|
|
3378
|
+
const resolved = await anchorResolveCommand.run(
|
|
3379
|
+
ctx,
|
|
3380
|
+
anchorResolveCommand.input.parse({
|
|
3381
|
+
bundlePath: path,
|
|
3382
|
+
conceptId: id,
|
|
3383
|
+
rebaseline: true,
|
|
3384
|
+
...repoRoot ? { repoRoot } : {},
|
|
3385
|
+
...offline ? { offline } : {}
|
|
3386
|
+
})
|
|
3387
|
+
);
|
|
3388
|
+
const after = await store.read(path, id);
|
|
3389
|
+
return {
|
|
3390
|
+
conceptId: id,
|
|
3391
|
+
reason: input.reason,
|
|
3392
|
+
changes,
|
|
3393
|
+
anchors: after?.frontmatter.strauss_anchors ?? [],
|
|
3394
|
+
baseline: "stamped",
|
|
3395
|
+
resolved: resolved.results,
|
|
3396
|
+
note: STAMPED_NOTE
|
|
3397
|
+
};
|
|
3398
|
+
},
|
|
3399
|
+
// With `resolve`, a pointer that names nothing is a failed set, not a
|
|
3400
|
+
// finding to read later. A remote nothing could reach was never checked, so
|
|
3401
|
+
// it does not fail — the same line anchor-resolve draws.
|
|
3402
|
+
failsWhen: (result) => (result.resolved ?? []).some((entry) => {
|
|
3403
|
+
const { state, reason } = entry;
|
|
3404
|
+
return state === "unresolved" && !isUncheckedReason(reason);
|
|
3405
|
+
})
|
|
3406
|
+
});
|
|
3407
|
+
|
|
3408
|
+
// src/commands/answer.ts
|
|
3409
|
+
var import_zod9 = require("zod");
|
|
3188
3410
|
var answerCommand = define({
|
|
3189
3411
|
name: "answer",
|
|
3190
3412
|
tool: "kb_answer",
|
|
3191
3413
|
usage: "answer <concept-id> <answer...>",
|
|
3192
3414
|
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.",
|
|
3193
|
-
input:
|
|
3415
|
+
input: import_zod9.z.object({ bundlePath, conceptId, answer: import_zod9.z.string().min(1) }),
|
|
3194
3416
|
fromArgv: (argv, path) => ({
|
|
3195
3417
|
bundlePath: path,
|
|
3196
3418
|
conceptId: argv[1],
|
|
@@ -3204,19 +3426,19 @@ var answerCommand = define({
|
|
|
3204
3426
|
});
|
|
3205
3427
|
|
|
3206
3428
|
// src/commands/backlinks.ts
|
|
3207
|
-
var
|
|
3429
|
+
var import_zod10 = require("zod");
|
|
3208
3430
|
var backlinksCommand = define({
|
|
3209
3431
|
name: "backlinks",
|
|
3210
3432
|
tool: "kb_backlinks",
|
|
3211
3433
|
usage: "backlinks <concept-id>",
|
|
3212
3434
|
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.",
|
|
3213
|
-
input:
|
|
3435
|
+
input: import_zod10.z.object({ bundlePath, conceptId }),
|
|
3214
3436
|
fromArgv: (argv, path) => ({ bundlePath: path, conceptId: argv[1] }),
|
|
3215
3437
|
run: async ({ store }, { bundlePath: path, conceptId: id }) => store.backlinks(path, id)
|
|
3216
3438
|
});
|
|
3217
3439
|
|
|
3218
3440
|
// src/commands/catalog.ts
|
|
3219
|
-
var
|
|
3441
|
+
var import_zod11 = require("zod");
|
|
3220
3442
|
|
|
3221
3443
|
// src/adjudicate.ts
|
|
3222
3444
|
var STANDING = {
|
|
@@ -3395,9 +3617,9 @@ var catalogCommand = define({
|
|
|
3395
3617
|
tool: "kb_catalog",
|
|
3396
3618
|
usage: "catalog [type] [--tag T]...",
|
|
3397
3619
|
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.",
|
|
3398
|
-
input:
|
|
3620
|
+
input: import_zod11.z.object({
|
|
3399
3621
|
bundlePath,
|
|
3400
|
-
type:
|
|
3622
|
+
type: import_zod11.z.enum(KB_RECORD_TYPES).optional(),
|
|
3401
3623
|
tags: TAGS
|
|
3402
3624
|
}),
|
|
3403
3625
|
fromArgv: (argv, path) => {
|
|
@@ -3469,7 +3691,7 @@ function count(value, noun) {
|
|
|
3469
3691
|
var import_node_buffer = require("buffer");
|
|
3470
3692
|
var import_promises7 = require("fs/promises");
|
|
3471
3693
|
var import_node_path10 = require("path");
|
|
3472
|
-
var
|
|
3694
|
+
var import_zod14 = require("zod");
|
|
3473
3695
|
|
|
3474
3696
|
// src/match-diff.ts
|
|
3475
3697
|
function matchToDiff(files, records, options = {}) {
|
|
@@ -4073,7 +4295,7 @@ function claimOf(record) {
|
|
|
4073
4295
|
}
|
|
4074
4296
|
|
|
4075
4297
|
// src/commands/match/command.ts
|
|
4076
|
-
var
|
|
4298
|
+
var import_zod13 = require("zod");
|
|
4077
4299
|
|
|
4078
4300
|
// src/commands/match/errors.ts
|
|
4079
4301
|
var KbMatchInputError = class extends BaseError {
|
|
@@ -4093,21 +4315,21 @@ var KbMatchInputError = class extends BaseError {
|
|
|
4093
4315
|
};
|
|
4094
4316
|
|
|
4095
4317
|
// src/commands/match/model.ts
|
|
4096
|
-
var
|
|
4097
|
-
var diffHunkSchema =
|
|
4098
|
-
startLine:
|
|
4099
|
-
endLine:
|
|
4100
|
-
side:
|
|
4318
|
+
var import_zod12 = require("zod");
|
|
4319
|
+
var diffHunkSchema = import_zod12.z.object({
|
|
4320
|
+
startLine: import_zod12.z.number().int().positive(),
|
|
4321
|
+
endLine: import_zod12.z.number().int().positive(),
|
|
4322
|
+
side: import_zod12.z.enum(["old", "new"]).optional()
|
|
4101
4323
|
}).passthrough();
|
|
4102
|
-
var diffFileSchema =
|
|
4103
|
-
filePath:
|
|
4104
|
-
hunks:
|
|
4324
|
+
var diffFileSchema = import_zod12.z.object({
|
|
4325
|
+
filePath: import_zod12.z.string().min(1).describe("Repo-relative, spelled the way anchors are."),
|
|
4326
|
+
hunks: import_zod12.z.array(diffHunkSchema)
|
|
4105
4327
|
});
|
|
4106
|
-
var symbolRangeSchema =
|
|
4107
|
-
file:
|
|
4108
|
-
symbol:
|
|
4109
|
-
startLine:
|
|
4110
|
-
endLine:
|
|
4328
|
+
var symbolRangeSchema = import_zod12.z.object({
|
|
4329
|
+
file: import_zod12.z.string().min(1),
|
|
4330
|
+
symbol: import_zod12.z.string().min(1),
|
|
4331
|
+
startLine: import_zod12.z.number().int().positive(),
|
|
4332
|
+
endLine: import_zod12.z.number().int().positive()
|
|
4111
4333
|
});
|
|
4112
4334
|
|
|
4113
4335
|
// src/commands/match/parse-unified-diff.ts
|
|
@@ -4356,17 +4578,17 @@ var matchCommand = define({
|
|
|
4356
4578
|
tool: "kb_match",
|
|
4357
4579
|
usage: "match --git <base>..<head> | --stdin [--repo-root <path>] [--offline] [--include-non-current]",
|
|
4358
4580
|
description: "Which records sit on each changed hunk: the anchored records per file range, current first, each with its standing and the anchor that matched. kb_load hands over a whole base; this narrows a diff. Symbol ranges resolve from repoRoot when omitted; non-current records need includeNonCurrent.",
|
|
4359
|
-
input:
|
|
4581
|
+
input: import_zod13.z.object({
|
|
4360
4582
|
bundlePath,
|
|
4361
|
-
files:
|
|
4362
|
-
symbolRanges:
|
|
4583
|
+
files: import_zod13.z.array(diffFileSchema).describe("The changed files, each with its post-change line ranges."),
|
|
4584
|
+
symbolRanges: import_zod13.z.array(symbolRangeSchema).optional().describe(
|
|
4363
4585
|
"Symbol spans the caller already has. Resolved from repoRoot when omitted."
|
|
4364
4586
|
),
|
|
4365
4587
|
repoRoot: REPO_ROOT,
|
|
4366
|
-
offline:
|
|
4588
|
+
offline: import_zod13.z.boolean().optional().describe(
|
|
4367
4589
|
"Resolve symbol ranges from what is already on disk, never fetching a grammar."
|
|
4368
4590
|
),
|
|
4369
|
-
includeNonCurrent:
|
|
4591
|
+
includeNonCurrent: import_zod13.z.boolean().optional().describe(
|
|
4370
4592
|
"Return superseded, rejected and unsettled records too, each carrying its standing."
|
|
4371
4593
|
)
|
|
4372
4594
|
}),
|
|
@@ -4380,11 +4602,11 @@ var matchCommand = define({
|
|
|
4380
4602
|
...argv.includes("--include-non-current") ? { includeNonCurrent: true } : {}
|
|
4381
4603
|
};
|
|
4382
4604
|
if (range !== void 0) {
|
|
4383
|
-
const
|
|
4384
|
-
if (!
|
|
4385
|
-
throw new KbMatchInputError(`--git ${range} ${REFUSED[
|
|
4605
|
+
const diff2 = await readRangeDiff(repoRoot ?? process.cwd(), range);
|
|
4606
|
+
if (!diff2.ok) {
|
|
4607
|
+
throw new KbMatchInputError(`--git ${range} ${REFUSED[diff2.reason]}`);
|
|
4386
4608
|
}
|
|
4387
|
-
return { ...base2, files: parseUnifiedDiff(
|
|
4609
|
+
return { ...base2, files: parseUnifiedDiff(diff2.text) };
|
|
4388
4610
|
}
|
|
4389
4611
|
if (!argv.includes("--stdin")) {
|
|
4390
4612
|
throw new KbMatchInputError(
|
|
@@ -4470,22 +4692,22 @@ function project(match, ranges, all) {
|
|
|
4470
4692
|
|
|
4471
4693
|
// src/commands/classify.ts
|
|
4472
4694
|
var classifyFileSchema = diffFileSchema.extend({
|
|
4473
|
-
hunks:
|
|
4474
|
-
diffHunkSchema.extend({ lines:
|
|
4695
|
+
hunks: import_zod14.z.array(
|
|
4696
|
+
diffHunkSchema.extend({ lines: import_zod14.z.array(import_zod14.z.string()).optional() })
|
|
4475
4697
|
),
|
|
4476
|
-
renamedFrom:
|
|
4477
|
-
similarity:
|
|
4698
|
+
renamedFrom: import_zod14.z.string().min(1).optional().describe("Where `git diff -M` says the path came from."),
|
|
4699
|
+
similarity: import_zod14.z.number().min(0).max(100).optional()
|
|
4478
4700
|
});
|
|
4479
4701
|
var classifyCommand = define({
|
|
4480
4702
|
name: "classify",
|
|
4481
4703
|
tool: "kb_classify",
|
|
4482
4704
|
usage: "classify --git <base>..<head> | --stdin [--repo-root <path>] [--offline]",
|
|
4483
4705
|
description: "What kind of change each file carries: test, config, ci, docs, lockfile, generated, boilerplate, rename or source, with the rule that decided it. Derived from the diff and never stored; a `review:generated`, `review:boilerplate` or `review:move` fact anchored on a file overrides the heuristic. kb_match says what sits on a hunk; this says whether to read it.",
|
|
4484
|
-
input:
|
|
4706
|
+
input: import_zod14.z.object({
|
|
4485
4707
|
bundlePath,
|
|
4486
|
-
files:
|
|
4708
|
+
files: import_zod14.z.array(classifyFileSchema).describe("The changed files, each with its line ranges."),
|
|
4487
4709
|
repoRoot: REPO_ROOT,
|
|
4488
|
-
offline:
|
|
4710
|
+
offline: import_zod14.z.boolean().optional().describe(
|
|
4489
4711
|
"Resolve symbol ranges from what is already on disk, never fetching a grammar."
|
|
4490
4712
|
)
|
|
4491
4713
|
}),
|
|
@@ -4498,15 +4720,15 @@ var classifyCommand = define({
|
|
|
4498
4720
|
...argv.includes("--offline") ? { offline: true } : {}
|
|
4499
4721
|
};
|
|
4500
4722
|
if (range !== void 0) {
|
|
4501
|
-
const
|
|
4502
|
-
if (!
|
|
4723
|
+
const diff2 = await readRangeDiff(repoRoot ?? process.cwd(), range);
|
|
4724
|
+
if (!diff2.ok) {
|
|
4503
4725
|
throw new KbClassifyInputError(
|
|
4504
|
-
`--git ${range} ${REFUSED2[
|
|
4726
|
+
`--git ${range} ${REFUSED2[diff2.reason]}`
|
|
4505
4727
|
);
|
|
4506
4728
|
}
|
|
4507
4729
|
return {
|
|
4508
4730
|
...base2,
|
|
4509
|
-
files: parseUnifiedDiff(
|
|
4731
|
+
files: parseUnifiedDiff(diff2.text, {
|
|
4510
4732
|
keepEmpty: true,
|
|
4511
4733
|
withLines: true
|
|
4512
4734
|
})
|
|
@@ -4596,7 +4818,7 @@ function renderClassify(result) {
|
|
|
4596
4818
|
}
|
|
4597
4819
|
|
|
4598
4820
|
// src/commands/context.ts
|
|
4599
|
-
var
|
|
4821
|
+
var import_zod15 = require("zod");
|
|
4600
4822
|
|
|
4601
4823
|
// src/kb-context.ts
|
|
4602
4824
|
var import_promises8 = require("fs/promises");
|
|
@@ -4867,23 +5089,23 @@ var contextCommand = define({
|
|
|
4867
5089
|
tool: "kb_context",
|
|
4868
5090
|
usage: "context [--profile NAME] [--budget N] [--full-under N] [--exclude-tag T]... [--format json] [--event NAME]",
|
|
4869
5091
|
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.",
|
|
4870
|
-
input:
|
|
4871
|
-
budgetTokens:
|
|
5092
|
+
input: import_zod15.z.object({
|
|
5093
|
+
budgetTokens: import_zod15.z.number().int().positive().optional().describe(
|
|
4872
5094
|
"Ceiling on the whole emitted block; past it the command refuses with a list of bases rather than truncating. Defaults to 4000."
|
|
4873
5095
|
),
|
|
4874
|
-
fullUnderTokens:
|
|
5096
|
+
fullUnderTokens: import_zod15.z.number().int().positive().optional().describe(
|
|
4875
5097
|
"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."
|
|
4876
5098
|
),
|
|
4877
|
-
profile:
|
|
5099
|
+
profile: import_zod15.z.string().optional().describe(
|
|
4878
5100
|
"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."
|
|
4879
5101
|
),
|
|
4880
|
-
excludeTags:
|
|
5102
|
+
excludeTags: import_zod15.z.array(import_zod15.z.string().min(1)).optional().describe(
|
|
4881
5103
|
"Frontmatter tags whose records stay out of the block. The base stays pinned and stays readable by tool; resolved like the budgets."
|
|
4882
5104
|
),
|
|
4883
|
-
format:
|
|
5105
|
+
format: import_zod15.z.enum(["markdown", "json"]).optional().describe(
|
|
4884
5106
|
"CLI envelope for hook protocols that require strict JSON on stdout. MCP callers omit this \u2014 the block itself is identical."
|
|
4885
5107
|
),
|
|
4886
|
-
event:
|
|
5108
|
+
event: import_zod15.z.string().optional().describe(
|
|
4887
5109
|
"hookEventName stamped into the JSON envelope. Only meaningful with format=json."
|
|
4888
5110
|
)
|
|
4889
5111
|
}),
|
|
@@ -4922,7 +5144,7 @@ var contextCommand = define({
|
|
|
4922
5144
|
});
|
|
4923
5145
|
|
|
4924
5146
|
// src/commands/doctor.ts
|
|
4925
|
-
var
|
|
5147
|
+
var import_zod17 = require("zod");
|
|
4926
5148
|
|
|
4927
5149
|
// src/kb-edges.ts
|
|
4928
5150
|
var KB_EDGE_KINDS = [
|
|
@@ -5438,17 +5660,17 @@ function ageInDays(record, now) {
|
|
|
5438
5660
|
}
|
|
5439
5661
|
|
|
5440
5662
|
// src/commands/reassess.ts
|
|
5441
|
-
var
|
|
5663
|
+
var import_zod16 = require("zod");
|
|
5442
5664
|
var reassessCommand = define({
|
|
5443
5665
|
name: "reassess",
|
|
5444
5666
|
tool: "kb_reassess",
|
|
5445
5667
|
usage: "reassess <concept-id> [--repo-root <path>] [--with-diff]",
|
|
5446
5668
|
description: "One drifted record, as something to judge: its claim, each anchor's drift class, the old-vs-new span diff, and the records that depend on it. Formatting-only drift is dropped. Empty when there is nothing to reassess. Writes: relocates moved anchors, keeping their hash; never verifies, supersedes, or changes standing.",
|
|
5447
|
-
input:
|
|
5669
|
+
input: import_zod16.z.object({
|
|
5448
5670
|
bundlePath,
|
|
5449
5671
|
conceptId,
|
|
5450
5672
|
repoRoot: REPO_ROOT,
|
|
5451
|
-
withDiff:
|
|
5673
|
+
withDiff: import_zod16.z.boolean().optional().describe(
|
|
5452
5674
|
"Recover each anchor's committed span and render the diff. Reads git history."
|
|
5453
5675
|
)
|
|
5454
5676
|
}),
|
|
@@ -5593,13 +5815,13 @@ function at(file, symbol) {
|
|
|
5593
5815
|
}
|
|
5594
5816
|
|
|
5595
5817
|
// src/commands/doctor.ts
|
|
5596
|
-
var days = (what, fallback) =>
|
|
5818
|
+
var days = (what, fallback) => import_zod17.z.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
|
|
5597
5819
|
var doctorCommand = define({
|
|
5598
5820
|
name: "doctor",
|
|
5599
5821
|
tool: "kb_doctor",
|
|
5600
5822
|
usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--offline] [--strict] [--drifted [--with-diff]]",
|
|
5601
5823
|
description: "Read-only health sweep: expired, expiring, unverified, aging, orphaned, broken-supersession, superseded-but-cited, drifted and unchecked anchors. Every group is reported even when empty; nothing is written or re-stamped. `drifted` narrows it to a reassessment packet per drifted record, `with_diff` adding each anchor's old-vs-new span.",
|
|
5602
|
-
input:
|
|
5824
|
+
input: import_zod17.z.object({
|
|
5603
5825
|
bundlePath,
|
|
5604
5826
|
repoRoot: REPO_ROOT,
|
|
5605
5827
|
expiringDays: days(
|
|
@@ -5614,16 +5836,16 @@ var doctorCommand = define({
|
|
|
5614
5836
|
"How long a record may stay `open` or `proposed` before `aging` reports it, in days.",
|
|
5615
5837
|
DEFAULT_AGING_DAYS
|
|
5616
5838
|
),
|
|
5617
|
-
offline:
|
|
5839
|
+
offline: import_zod17.z.boolean().optional().describe(
|
|
5618
5840
|
"Read foreign anchors from the local repo cache only, never fetching."
|
|
5619
5841
|
),
|
|
5620
|
-
strict:
|
|
5842
|
+
strict: import_zod17.z.boolean().optional().describe(
|
|
5621
5843
|
"Turn an expired record into a non-zero exit for the CLI. No effect on the report itself."
|
|
5622
5844
|
),
|
|
5623
|
-
drifted:
|
|
5845
|
+
drifted: import_zod17.z.boolean().optional().describe(
|
|
5624
5846
|
"Report only drift, as a reassessment packet per record: claim, per-anchor class, and what depends on it."
|
|
5625
5847
|
),
|
|
5626
|
-
withDiff:
|
|
5848
|
+
withDiff: import_zod17.z.boolean().optional().describe(
|
|
5627
5849
|
"With `drifted`: recover each anchor's committed span and render the old-vs-new diff. Reads git history."
|
|
5628
5850
|
)
|
|
5629
5851
|
}),
|
|
@@ -5800,7 +6022,7 @@ function renderPackets(result) {
|
|
|
5800
6022
|
// src/commands/export.ts
|
|
5801
6023
|
var import_promises9 = require("fs/promises");
|
|
5802
6024
|
var import_node_path11 = require("path");
|
|
5803
|
-
var
|
|
6025
|
+
var import_zod18 = require("zod");
|
|
5804
6026
|
var NUMBERED = /^(\d{4})-(.+)\.md$/;
|
|
5805
6027
|
var MARKER = "<!-- strauss-kb export: ";
|
|
5806
6028
|
var exportCommand = define({
|
|
@@ -5808,10 +6030,10 @@ var exportCommand = define({
|
|
|
5808
6030
|
tool: "kb_export",
|
|
5809
6031
|
usage: "export --format madr --to <dir>",
|
|
5810
6032
|
description: "Write the base's decisions out as numbered MADR files, one per decision, for a repository that keeps ADRs of its own. Numbering is by slug, so a re-run rewrites its own files in place. A superseded decision is exported with what replaced it.",
|
|
5811
|
-
input:
|
|
6033
|
+
input: import_zod18.z.object({
|
|
5812
6034
|
bundlePath,
|
|
5813
|
-
format:
|
|
5814
|
-
to:
|
|
6035
|
+
format: import_zod18.z.enum(["madr"]).describe("Output layout. `madr` is the only one so far."),
|
|
6036
|
+
to: import_zod18.z.string().min(1).describe("Directory the ADR files are written into.")
|
|
5815
6037
|
}),
|
|
5816
6038
|
fromArgv: (argv, path) => ({
|
|
5817
6039
|
bundlePath: path,
|
|
@@ -5933,19 +6155,19 @@ function bodySections(body) {
|
|
|
5933
6155
|
}
|
|
5934
6156
|
|
|
5935
6157
|
// src/commands/impact.ts
|
|
5936
|
-
var
|
|
6158
|
+
var import_zod19 = require("zod");
|
|
5937
6159
|
var impactCommand = define({
|
|
5938
6160
|
name: "impact",
|
|
5939
6161
|
tool: "kb_impact",
|
|
5940
6162
|
usage: "impact <concept-id> [--depth N] [--rels a,b]",
|
|
5941
6163
|
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.",
|
|
5942
|
-
input:
|
|
6164
|
+
input: import_zod19.z.object({
|
|
5943
6165
|
bundlePath,
|
|
5944
6166
|
conceptId,
|
|
5945
|
-
depth:
|
|
6167
|
+
depth: import_zod19.z.number().int().positive().optional().describe(
|
|
5946
6168
|
"Hops out from the record. Unbounded when omitted; a walk this cuts reports truncated: true."
|
|
5947
6169
|
),
|
|
5948
|
-
rels:
|
|
6170
|
+
rels: import_zod19.z.array(import_zod19.z.enum(KB_CAUSAL_LINK_RELS)).optional().describe(
|
|
5949
6171
|
"Narrow which rels the walk follows. Defaults to every rel that carries a dependence \u2014 all but related_to."
|
|
5950
6172
|
)
|
|
5951
6173
|
}),
|
|
@@ -5966,15 +6188,15 @@ var impactCommand = define({
|
|
|
5966
6188
|
});
|
|
5967
6189
|
|
|
5968
6190
|
// src/commands/list.ts
|
|
5969
|
-
var
|
|
6191
|
+
var import_zod20 = require("zod");
|
|
5970
6192
|
var listCommand = define({
|
|
5971
6193
|
name: "list",
|
|
5972
6194
|
tool: "kb_list",
|
|
5973
6195
|
usage: "list [type] [--tag T]...",
|
|
5974
6196
|
description: "Every record, optionally one type or tag. For enumerating; use kb_query for a question.",
|
|
5975
|
-
input:
|
|
6197
|
+
input: import_zod20.z.object({
|
|
5976
6198
|
bundlePath,
|
|
5977
|
-
type:
|
|
6199
|
+
type: import_zod20.z.enum(KB_RECORD_TYPES).optional(),
|
|
5978
6200
|
tags: TAGS
|
|
5979
6201
|
}),
|
|
5980
6202
|
fromArgv: (argv, path) => {
|
|
@@ -5998,17 +6220,17 @@ var listCommand = define({
|
|
|
5998
6220
|
});
|
|
5999
6221
|
|
|
6000
6222
|
// src/commands/load.ts
|
|
6001
|
-
var
|
|
6223
|
+
var import_zod21 = require("zod");
|
|
6002
6224
|
var loadCommand = define({
|
|
6003
6225
|
name: "load",
|
|
6004
6226
|
tool: "kb_load",
|
|
6005
6227
|
usage: "load [type] [--budget N | --all] [--repo-root PATH]",
|
|
6006
6228
|
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.",
|
|
6007
|
-
input:
|
|
6229
|
+
input: import_zod21.z.object({
|
|
6008
6230
|
bundlePath,
|
|
6009
|
-
type:
|
|
6010
|
-
budgetTokens:
|
|
6011
|
-
all:
|
|
6231
|
+
type: import_zod21.z.enum(KB_RECORD_TYPES).optional(),
|
|
6232
|
+
budgetTokens: import_zod21.z.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
|
|
6233
|
+
all: import_zod21.z.boolean().optional().describe(
|
|
6012
6234
|
"Loads the entire base regardless of size, bypassing the token budget; mutually exclusive with budgetTokens."
|
|
6013
6235
|
),
|
|
6014
6236
|
repoRoot: REPO_ROOT
|
|
@@ -6050,25 +6272,25 @@ var loadCommand = define({
|
|
|
6050
6272
|
});
|
|
6051
6273
|
|
|
6052
6274
|
// src/commands/log.ts
|
|
6053
|
-
var
|
|
6275
|
+
var import_zod22 = require("zod");
|
|
6054
6276
|
var logCommand = define({
|
|
6055
6277
|
name: "log",
|
|
6056
6278
|
tool: "kb_log",
|
|
6057
6279
|
usage: "log",
|
|
6058
6280
|
description: "Who touched what, and when. Append-only; malformed lines are reported, never repaired.",
|
|
6059
|
-
input:
|
|
6281
|
+
input: import_zod22.z.object({ bundlePath }),
|
|
6060
6282
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
6061
6283
|
run: ({ store }, { bundlePath: path }) => store.readLog(path)
|
|
6062
6284
|
});
|
|
6063
6285
|
|
|
6064
6286
|
// src/commands/no-decision.ts
|
|
6065
|
-
var
|
|
6287
|
+
var import_zod23 = require("zod");
|
|
6066
6288
|
var noDecisionCommand = define({
|
|
6067
6289
|
name: "no-decision",
|
|
6068
6290
|
tool: "kb_no_decision",
|
|
6069
6291
|
usage: "no-decision <reason...>",
|
|
6070
6292
|
description: "Record in one sentence that a piece of work had nothing to decide. Idempotent.",
|
|
6071
|
-
input:
|
|
6293
|
+
input: import_zod23.z.object({ bundlePath, reason: import_zod23.z.string().min(1) }),
|
|
6072
6294
|
fromArgv: (argv, path) => ({
|
|
6073
6295
|
bundlePath: path,
|
|
6074
6296
|
reason: argv.slice(1).join(" ").trim()
|
|
@@ -6085,20 +6307,20 @@ var noDecisionCommand = define({
|
|
|
6085
6307
|
});
|
|
6086
6308
|
|
|
6087
6309
|
// src/commands/pack.ts
|
|
6088
|
-
var
|
|
6310
|
+
var import_zod24 = require("zod");
|
|
6089
6311
|
var packCommand = define({
|
|
6090
6312
|
name: "pack",
|
|
6091
6313
|
tool: "kb_pack",
|
|
6092
6314
|
usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
|
|
6093
6315
|
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.",
|
|
6094
|
-
input:
|
|
6316
|
+
input: import_zod24.z.object({
|
|
6095
6317
|
bundlePath,
|
|
6096
6318
|
conceptId,
|
|
6097
|
-
hops:
|
|
6098
|
-
maxNodes:
|
|
6319
|
+
hops: import_zod24.z.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
|
|
6320
|
+
maxNodes: import_zod24.z.number().int().positive().optional().describe(
|
|
6099
6321
|
"How many records the pack may hold, root included. Defaults to 20."
|
|
6100
6322
|
),
|
|
6101
|
-
budgetTokens:
|
|
6323
|
+
budgetTokens: import_zod24.z.number().int().positive().optional().describe(
|
|
6102
6324
|
"Approximate token ceiling over what is actually emitted. Defaults to 25000."
|
|
6103
6325
|
)
|
|
6104
6326
|
}),
|
|
@@ -6185,22 +6407,22 @@ function warningLabel(warning) {
|
|
|
6185
6407
|
}
|
|
6186
6408
|
|
|
6187
6409
|
// src/commands/pin.ts
|
|
6188
|
-
var
|
|
6410
|
+
var import_zod25 = require("zod");
|
|
6189
6411
|
var pinCommand = define({
|
|
6190
6412
|
name: "pin",
|
|
6191
6413
|
tool: "kb_pin",
|
|
6192
6414
|
usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
|
|
6193
6415
|
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.",
|
|
6194
|
-
input:
|
|
6416
|
+
input: import_zod25.z.object({
|
|
6195
6417
|
bundlePath,
|
|
6196
|
-
mode:
|
|
6418
|
+
mode: import_zod25.z.enum(["full", "index"]).optional().describe(
|
|
6197
6419
|
"full: always emit this base's records whole (still under the block budget); index: never upgrade. Absent: the profile's full-under threshold decides."
|
|
6198
6420
|
),
|
|
6199
|
-
profiles:
|
|
6200
|
-
layer:
|
|
6421
|
+
profiles: import_zod25.z.array(import_zod25.z.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
|
|
6422
|
+
layer: import_zod25.z.enum(["project", "local", "user"]).optional().describe(
|
|
6201
6423
|
"Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
|
|
6202
6424
|
),
|
|
6203
|
-
frozen:
|
|
6425
|
+
frozen: import_zod25.z.boolean().optional().describe(
|
|
6204
6426
|
"true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
|
|
6205
6427
|
)
|
|
6206
6428
|
}),
|
|
@@ -6229,13 +6451,13 @@ var pinCommand = define({
|
|
|
6229
6451
|
});
|
|
6230
6452
|
|
|
6231
6453
|
// src/commands/pins.ts
|
|
6232
|
-
var
|
|
6454
|
+
var import_zod26 = require("zod");
|
|
6233
6455
|
var pinsCommand = define({
|
|
6234
6456
|
name: "pins",
|
|
6235
6457
|
tool: "kb_pins",
|
|
6236
6458
|
usage: "pins",
|
|
6237
6459
|
description: "Every pinned base across the manifest layers, with its layer and whether it resolves to records. Takes no bundlePath.",
|
|
6238
|
-
input:
|
|
6460
|
+
input: import_zod26.z.object({}),
|
|
6239
6461
|
fromArgv: () => ({}),
|
|
6240
6462
|
run: ({ store }) => listPins(store, process.cwd())
|
|
6241
6463
|
});
|
|
@@ -6506,16 +6728,16 @@ function recordType(conceptId2) {
|
|
|
6506
6728
|
}
|
|
6507
6729
|
|
|
6508
6730
|
// src/commands/promote/model.ts
|
|
6509
|
-
var
|
|
6510
|
-
var promoteInputSchema =
|
|
6731
|
+
var import_zod27 = require("zod");
|
|
6732
|
+
var promoteInputSchema = import_zod27.z.object({
|
|
6511
6733
|
bundlePath,
|
|
6512
|
-
conceptIds:
|
|
6513
|
-
to:
|
|
6514
|
-
source:
|
|
6734
|
+
conceptIds: import_zod27.z.array(conceptId).max(64).optional().describe("Records to copy into the target base. Omit with `list`."),
|
|
6735
|
+
to: import_zod27.z.string().min(1).optional().describe("Absolute path to the base being promoted into."),
|
|
6736
|
+
source: import_zod27.z.string().min(1).optional().describe(
|
|
6515
6737
|
"Where the promotion came from, usually the pull request URL. Recorded on each copy as a source."
|
|
6516
6738
|
),
|
|
6517
|
-
force:
|
|
6518
|
-
list:
|
|
6739
|
+
force: import_zod27.z.boolean().optional().describe("Overwrite a record the target base already holds."),
|
|
6740
|
+
list: import_zod27.z.boolean().optional().describe("List the source base's candidates instead of promoting.")
|
|
6519
6741
|
}).refine((input) => input.list === true || input.to !== void 0, {
|
|
6520
6742
|
message: "promote needs a target base \u2014 pass --to <bundle>, or --list",
|
|
6521
6743
|
path: ["to"]
|
|
@@ -6652,17 +6874,17 @@ function renderPromote(result) {
|
|
|
6652
6874
|
}
|
|
6653
6875
|
|
|
6654
6876
|
// src/commands/query.ts
|
|
6655
|
-
var
|
|
6877
|
+
var import_zod28 = require("zod");
|
|
6656
6878
|
var queryCommand = define({
|
|
6657
6879
|
name: "query",
|
|
6658
6880
|
tool: "kb_query",
|
|
6659
6881
|
usage: "query <text...> [--tag T]... [--repo-root PATH]",
|
|
6660
6882
|
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.",
|
|
6661
|
-
input:
|
|
6883
|
+
input: import_zod28.z.object({
|
|
6662
6884
|
bundlePath,
|
|
6663
|
-
text:
|
|
6664
|
-
type:
|
|
6665
|
-
includeNonCurrent:
|
|
6885
|
+
text: import_zod28.z.string().optional(),
|
|
6886
|
+
type: import_zod28.z.enum(KB_RECORD_TYPES).optional(),
|
|
6887
|
+
includeNonCurrent: import_zod28.z.boolean().optional(),
|
|
6666
6888
|
tags: TAGS,
|
|
6667
6889
|
repoRoot: REPO_ROOT
|
|
6668
6890
|
}),
|
|
@@ -6696,27 +6918,32 @@ var queryCommand = define({
|
|
|
6696
6918
|
});
|
|
6697
6919
|
|
|
6698
6920
|
// src/commands/read-index.ts
|
|
6699
|
-
var
|
|
6921
|
+
var import_zod29 = require("zod");
|
|
6700
6922
|
var readIndexCommand = define({
|
|
6701
6923
|
name: "index",
|
|
6702
6924
|
tool: "kb_index",
|
|
6703
6925
|
usage: "index",
|
|
6704
6926
|
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.",
|
|
6705
|
-
input:
|
|
6927
|
+
input: import_zod29.z.object({ bundlePath }),
|
|
6706
6928
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
6707
6929
|
run: ({ store }, { bundlePath: path }) => store.readIndex(path)
|
|
6708
6930
|
});
|
|
6709
6931
|
|
|
6710
6932
|
// src/commands/schema.ts
|
|
6711
|
-
var
|
|
6933
|
+
var import_zod32 = require("zod");
|
|
6712
6934
|
|
|
6713
6935
|
// src/json-schema.ts
|
|
6714
|
-
var
|
|
6936
|
+
var import_zod31 = require("zod");
|
|
6715
6937
|
|
|
6716
6938
|
// src/kb-log.ts
|
|
6717
|
-
var
|
|
6939
|
+
var import_zod30 = require("zod");
|
|
6718
6940
|
var LOG_FILE = "log.jsonl";
|
|
6719
|
-
var
|
|
6941
|
+
var kbLogAnchorChangeSchema = import_zod30.z.object({
|
|
6942
|
+
op: import_zod30.z.enum(["move", "add", "drop"]),
|
|
6943
|
+
from: kbAnchorLocatorSchema.optional(),
|
|
6944
|
+
to: kbAnchorLocatorSchema.optional()
|
|
6945
|
+
}).strict();
|
|
6946
|
+
var kbLogEntryFields = import_zod30.z.object({
|
|
6720
6947
|
// Validated, not just `min(1)`: `at` is a sort key (see `parseLog`
|
|
6721
6948
|
// below), and a value that isn't actually chronological — a Unix
|
|
6722
6949
|
// timestamp, a human-typed date, garbage — would sort wrong without
|
|
@@ -6725,18 +6952,28 @@ var kbLogEntrySchema = import_zod29.z.object({
|
|
|
6725
6952
|
// and rejects everything else, including a non-`Z` offset — so a
|
|
6726
6953
|
// malformed `at` is reported the same way a malformed line already is,
|
|
6727
6954
|
// rather than silently sorting into the wrong place.
|
|
6728
|
-
at:
|
|
6729
|
-
by:
|
|
6730
|
-
operation:
|
|
6731
|
-
conceptId:
|
|
6955
|
+
at: import_zod30.z.iso.datetime(),
|
|
6956
|
+
by: import_zod30.z.string().min(1),
|
|
6957
|
+
operation: import_zod30.z.string().min(1),
|
|
6958
|
+
conceptId: import_zod30.z.string().min(1),
|
|
6732
6959
|
/**
|
|
6733
6960
|
* The operation's other end, where it has one: a second concept id for
|
|
6734
6961
|
* supersession, the other base's path for promotion.
|
|
6735
6962
|
*/
|
|
6736
|
-
target:
|
|
6737
|
-
|
|
6963
|
+
target: import_zod30.z.string().min(1).optional(),
|
|
6964
|
+
/**
|
|
6965
|
+
* Why the operation was performed, where the operation demands one.
|
|
6966
|
+
* `anchor-set` does: a pointer moved by a reader is only auditable if
|
|
6967
|
+
* the reading is recorded beside it.
|
|
6968
|
+
*/
|
|
6969
|
+
reason: import_zod30.z.string().min(1).optional(),
|
|
6970
|
+
/** What `anchor-set` changed, derived from the record before and after. */
|
|
6971
|
+
anchors: import_zod30.z.array(kbLogAnchorChangeSchema).optional()
|
|
6972
|
+
});
|
|
6973
|
+
var kbLogEntrySchema = kbLogEntryFields.passthrough();
|
|
6974
|
+
var kbLogEntryWriteSchema = kbLogEntryFields.strict();
|
|
6738
6975
|
function renderLogEntry(entry) {
|
|
6739
|
-
return `${JSON.stringify(
|
|
6976
|
+
return `${JSON.stringify(kbLogEntryWriteSchema.parse(entry))}
|
|
6740
6977
|
`;
|
|
6741
6978
|
}
|
|
6742
6979
|
var CONFLICT_MARKER = /^(<{7}|\|{7}|={7}|>{7})/;
|
|
@@ -6777,11 +7014,11 @@ function parseLog(raw) {
|
|
|
6777
7014
|
// src/json-schema.ts
|
|
6778
7015
|
function kbJsonSchemas() {
|
|
6779
7016
|
return {
|
|
6780
|
-
recordFrontmatter:
|
|
7017
|
+
recordFrontmatter: import_zod31.z.toJSONSchema(kbRecordFrontmatterSchema, {
|
|
6781
7018
|
io: "input"
|
|
6782
7019
|
}),
|
|
6783
|
-
composeInput:
|
|
6784
|
-
logEntry:
|
|
7020
|
+
composeInput: import_zod31.z.toJSONSchema(composeInputSchema, { io: "input" }),
|
|
7021
|
+
logEntry: import_zod31.z.toJSONSchema(kbLogEntrySchema, { io: "input" })
|
|
6785
7022
|
};
|
|
6786
7023
|
}
|
|
6787
7024
|
|
|
@@ -6791,25 +7028,25 @@ var schemaCommand = define({
|
|
|
6791
7028
|
tool: "kb_schema",
|
|
6792
7029
|
usage: "schema",
|
|
6793
7030
|
description: "JSON Schema for frontmatter, write input, and log entries, generated from the enforcing code.",
|
|
6794
|
-
input:
|
|
7031
|
+
input: import_zod32.z.object({}),
|
|
6795
7032
|
fromArgv: () => ({}),
|
|
6796
7033
|
run: () => Promise.resolve(kbJsonSchemas())
|
|
6797
7034
|
});
|
|
6798
7035
|
|
|
6799
7036
|
// src/commands/stamp.ts
|
|
6800
7037
|
var import_promises10 = require("fs/promises");
|
|
6801
|
-
var
|
|
7038
|
+
var import_zod33 = require("zod");
|
|
6802
7039
|
var DIGEST = /^[0-9a-f]{64}$/;
|
|
6803
7040
|
var stampCommand = define({
|
|
6804
7041
|
name: "stamp",
|
|
6805
7042
|
tool: "kb_stamp",
|
|
6806
7043
|
usage: "stamp [--bundle PATH] [--since DIGEST|FILE]",
|
|
6807
7044
|
description: "Content stamp of a base \u2014 `load`'s digest, record counts, per-record digests, how many records have drifted anchors \u2014 without any bodies. Takes no bundlePath to stamp every pinned base. With `since`, reports only the bases that moved, naming the changed ids. Reads, never writes.",
|
|
6808
|
-
input:
|
|
6809
|
-
bundlePath:
|
|
7045
|
+
input: import_zod33.z.object({
|
|
7046
|
+
bundlePath: import_zod33.z.string().min(1).optional().describe(
|
|
6810
7047
|
"Absolute path to one knowledge base. Omit to stamp every pinned base."
|
|
6811
7048
|
),
|
|
6812
|
-
since:
|
|
7049
|
+
since: import_zod33.z.string().min(1).optional().describe(
|
|
6813
7050
|
"Prior digest, or path to a prior `stamp --json`; only moved bases return, with changed ids when the baseline is a file."
|
|
6814
7051
|
)
|
|
6815
7052
|
}),
|
|
@@ -6895,16 +7132,16 @@ async function readBaseline(since) {
|
|
|
6895
7132
|
}
|
|
6896
7133
|
|
|
6897
7134
|
// src/commands/status.ts
|
|
6898
|
-
var
|
|
7135
|
+
var import_zod34 = require("zod");
|
|
6899
7136
|
var statusCommand = define({
|
|
6900
7137
|
name: "status",
|
|
6901
7138
|
tool: "kb_status",
|
|
6902
7139
|
usage: "status <concept-id> <status>",
|
|
6903
7140
|
description: "Move a record's status. Compare-and-swap: a concurrent change fails instead of being overwritten.",
|
|
6904
|
-
input:
|
|
7141
|
+
input: import_zod34.z.object({
|
|
6905
7142
|
bundlePath,
|
|
6906
7143
|
conceptId,
|
|
6907
|
-
status:
|
|
7144
|
+
status: import_zod34.z.enum(KB_RECORD_STATUSES)
|
|
6908
7145
|
}),
|
|
6909
7146
|
fromArgv: (argv, path) => ({
|
|
6910
7147
|
bundlePath: path,
|
|
@@ -6919,13 +7156,13 @@ var statusCommand = define({
|
|
|
6919
7156
|
});
|
|
6920
7157
|
|
|
6921
7158
|
// src/commands/supersede.ts
|
|
6922
|
-
var
|
|
7159
|
+
var import_zod35 = require("zod");
|
|
6923
7160
|
var supersedeCommand = define({
|
|
6924
7161
|
name: "supersede",
|
|
6925
7162
|
tool: "kb_supersede",
|
|
6926
7163
|
usage: "supersede <concept-id> <replacement-id>",
|
|
6927
7164
|
description: "Mark a record superseded by another, linked in both directions. Use instead of editing a record whose meaning changed.",
|
|
6928
|
-
input:
|
|
7165
|
+
input: import_zod35.z.object({ bundlePath, conceptId, replacementId: conceptId }),
|
|
6929
7166
|
fromArgv: (argv, path) => ({
|
|
6930
7167
|
bundlePath: path,
|
|
6931
7168
|
conceptId: argv[1],
|
|
@@ -6939,7 +7176,7 @@ var supersedeCommand = define({
|
|
|
6939
7176
|
});
|
|
6940
7177
|
|
|
6941
7178
|
// src/commands/sweep.ts
|
|
6942
|
-
var
|
|
7179
|
+
var import_zod36 = require("zod");
|
|
6943
7180
|
var TERMINAL = [
|
|
6944
7181
|
"resolved",
|
|
6945
7182
|
"rejected",
|
|
@@ -6950,15 +7187,15 @@ var sweepCommand = define({
|
|
|
6950
7187
|
tool: "kb_sweep",
|
|
6951
7188
|
usage: "sweep --tag <tag> --terminal [--dry-run]",
|
|
6952
7189
|
description: "Delete tagged records that are resolved, rejected or superseded. Refuses without --tag, keeps any record a surviving record still points at, and logs each deletion.",
|
|
6953
|
-
input:
|
|
7190
|
+
input: import_zod36.z.object({
|
|
6954
7191
|
bundlePath,
|
|
6955
|
-
tag:
|
|
6956
|
-
terminal:
|
|
7192
|
+
tag: import_zod36.z.string({ error: "sweep needs --tag: it never sweeps a whole base" }).min(1).describe("Only records carrying this tag are considered."),
|
|
7193
|
+
terminal: import_zod36.z.literal(true, {
|
|
6957
7194
|
error: "sweep needs --terminal: it deletes only settled records"
|
|
6958
7195
|
}).describe(
|
|
6959
7196
|
"Required. Names the only scope sweep deletes: resolved, rejected and superseded records."
|
|
6960
7197
|
),
|
|
6961
|
-
dryRun:
|
|
7198
|
+
dryRun: import_zod36.z.boolean().optional().describe("Report what would go, and delete nothing.")
|
|
6962
7199
|
}),
|
|
6963
7200
|
fromArgv: (argv, path) => ({
|
|
6964
7201
|
bundlePath: path,
|
|
@@ -7075,16 +7312,16 @@ function renderSweep(result) {
|
|
|
7075
7312
|
}
|
|
7076
7313
|
|
|
7077
7314
|
// src/commands/sync-instructions.ts
|
|
7078
|
-
var
|
|
7315
|
+
var import_zod37 = require("zod");
|
|
7079
7316
|
var syncInstructionsCommand = define({
|
|
7080
7317
|
name: "sync-instructions",
|
|
7081
7318
|
usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
|
|
7082
7319
|
description: "CLI-only: plant the kb_context block between sentinel comments in AGENTS.md or CLAUDE.md, idempotently.",
|
|
7083
|
-
input:
|
|
7084
|
-
file:
|
|
7085
|
-
budgetTokens:
|
|
7086
|
-
fullUnderTokens:
|
|
7087
|
-
profile:
|
|
7320
|
+
input: import_zod37.z.object({
|
|
7321
|
+
file: import_zod37.z.string().min(1).describe("The instruction file to edit in place."),
|
|
7322
|
+
budgetTokens: import_zod37.z.number().int().positive().optional(),
|
|
7323
|
+
fullUnderTokens: import_zod37.z.number().int().positive().optional(),
|
|
7324
|
+
profile: import_zod37.z.string().optional()
|
|
7088
7325
|
}),
|
|
7089
7326
|
fromArgv: (argv) => {
|
|
7090
7327
|
const budget = argvFlag(argv, "--budget");
|
|
@@ -7110,7 +7347,7 @@ var syncInstructionsCommand = define({
|
|
|
7110
7347
|
});
|
|
7111
7348
|
|
|
7112
7349
|
// src/commands/trace.ts
|
|
7113
|
-
var
|
|
7350
|
+
var import_zod38 = require("zod");
|
|
7114
7351
|
|
|
7115
7352
|
// src/trace.ts
|
|
7116
7353
|
var TRACE_EDGES = [
|
|
@@ -7166,11 +7403,11 @@ var traceCommand = define({
|
|
|
7166
7403
|
tool: "kb_trace",
|
|
7167
7404
|
usage: "trace <concept-id> [edges...]",
|
|
7168
7405
|
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".',
|
|
7169
|
-
input:
|
|
7406
|
+
input: import_zod38.z.object({
|
|
7170
7407
|
bundlePath,
|
|
7171
7408
|
conceptId,
|
|
7172
|
-
edges:
|
|
7173
|
-
depth:
|
|
7409
|
+
edges: import_zod38.z.array(import_zod38.z.enum(TRACE_EDGES)).optional(),
|
|
7410
|
+
depth: import_zod38.z.number().int().positive().optional()
|
|
7174
7411
|
}),
|
|
7175
7412
|
fromArgv: (argv, path) => ({
|
|
7176
7413
|
bundlePath: path,
|
|
@@ -7192,37 +7429,37 @@ var traceCommand = define({
|
|
|
7192
7429
|
});
|
|
7193
7430
|
|
|
7194
7431
|
// src/commands/types.ts
|
|
7195
|
-
var
|
|
7432
|
+
var import_zod39 = require("zod");
|
|
7196
7433
|
var typesCommand = define({
|
|
7197
7434
|
name: "types",
|
|
7198
7435
|
tool: "kb_types",
|
|
7199
7436
|
usage: "types",
|
|
7200
7437
|
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.",
|
|
7201
|
-
input:
|
|
7438
|
+
input: import_zod39.z.object({}),
|
|
7202
7439
|
fromArgv: () => ({}),
|
|
7203
7440
|
run: () => Promise.resolve(RECORD_TYPES)
|
|
7204
7441
|
});
|
|
7205
7442
|
|
|
7206
7443
|
// src/commands/unpin.ts
|
|
7207
|
-
var
|
|
7444
|
+
var import_zod40 = require("zod");
|
|
7208
7445
|
var unpinCommand = define({
|
|
7209
7446
|
name: "unpin",
|
|
7210
7447
|
tool: "kb_unpin",
|
|
7211
7448
|
usage: "unpin [bundle-path]",
|
|
7212
7449
|
description: "Remove a base from every manifest layer that holds it. Reports the layers touched.",
|
|
7213
|
-
input:
|
|
7450
|
+
input: import_zod40.z.object({ bundlePath }),
|
|
7214
7451
|
fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
|
|
7215
7452
|
run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
|
|
7216
7453
|
});
|
|
7217
7454
|
|
|
7218
7455
|
// src/commands/validate.ts
|
|
7219
|
-
var
|
|
7456
|
+
var import_zod41 = require("zod");
|
|
7220
7457
|
var validateCommand = define({
|
|
7221
7458
|
name: "validate",
|
|
7222
7459
|
tool: "kb_validate",
|
|
7223
7460
|
usage: "validate",
|
|
7224
7461
|
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.",
|
|
7225
|
-
input:
|
|
7462
|
+
input: import_zod41.z.object({ bundlePath }),
|
|
7226
7463
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
7227
7464
|
run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
|
|
7228
7465
|
// Warnings never fail the exit code; every other severity does.
|
|
@@ -7232,16 +7469,16 @@ var validateCommand = define({
|
|
|
7232
7469
|
});
|
|
7233
7470
|
|
|
7234
7471
|
// src/commands/verify.ts
|
|
7235
|
-
var
|
|
7472
|
+
var import_zod42 = require("zod");
|
|
7236
7473
|
var verifyCommand = define({
|
|
7237
7474
|
name: "verify",
|
|
7238
7475
|
tool: "kb_verify",
|
|
7239
7476
|
usage: "verify <concept-id> --note <text>",
|
|
7240
7477
|
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.",
|
|
7241
|
-
input:
|
|
7478
|
+
input: import_zod42.z.object({
|
|
7242
7479
|
bundlePath,
|
|
7243
7480
|
conceptId,
|
|
7244
|
-
note:
|
|
7481
|
+
note: import_zod42.z.string().refine((s) => s.trim().length > 0, {
|
|
7245
7482
|
message: "note must say what the check found"
|
|
7246
7483
|
})
|
|
7247
7484
|
}),
|
|
@@ -7261,15 +7498,15 @@ var verifyCommand = define({
|
|
|
7261
7498
|
});
|
|
7262
7499
|
|
|
7263
7500
|
// src/commands/write.ts
|
|
7264
|
-
var
|
|
7501
|
+
var import_zod43 = require("zod");
|
|
7265
7502
|
var writeCommand = define({
|
|
7266
7503
|
name: "write",
|
|
7267
7504
|
tool: "kb_write",
|
|
7268
7505
|
usage: "write <type> < record.json",
|
|
7269
7506
|
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.",
|
|
7270
|
-
input:
|
|
7507
|
+
input: import_zod43.z.object({
|
|
7271
7508
|
bundlePath,
|
|
7272
|
-
type:
|
|
7509
|
+
type: import_zod43.z.enum(KB_RECORD_TYPES),
|
|
7273
7510
|
input: composeInputSchema
|
|
7274
7511
|
}),
|
|
7275
7512
|
fromArgv: async (argv, path, stdin) => ({
|
|
@@ -7293,13 +7530,13 @@ var writeCommand = define({
|
|
|
7293
7530
|
});
|
|
7294
7531
|
|
|
7295
7532
|
// src/commands/write-decision.ts
|
|
7296
|
-
var
|
|
7533
|
+
var import_zod44 = require("zod");
|
|
7297
7534
|
var writeDecisionCommand = define({
|
|
7298
7535
|
name: "write-decision",
|
|
7299
7536
|
tool: "kb_write_decision",
|
|
7300
7537
|
usage: "write-decision < decision.json",
|
|
7301
7538
|
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.",
|
|
7302
|
-
input:
|
|
7539
|
+
input: import_zod44.z.object({ bundlePath, input: decisionInputSchema }),
|
|
7303
7540
|
fromArgv: async (_argv, path, stdin) => ({
|
|
7304
7541
|
bundlePath: path,
|
|
7305
7542
|
input: JSON.parse(await stdin())
|
|
@@ -7329,6 +7566,7 @@ var KB_COMMANDS = [
|
|
|
7329
7566
|
answerCommand,
|
|
7330
7567
|
verifyCommand,
|
|
7331
7568
|
anchorResolveCommand,
|
|
7569
|
+
anchorSetCommand,
|
|
7332
7570
|
reassessCommand,
|
|
7333
7571
|
promoteCommand,
|
|
7334
7572
|
loadCommand,
|
|
@@ -7653,6 +7891,7 @@ var KbStore = class {
|
|
|
7653
7891
|
* its contents.
|
|
7654
7892
|
*/
|
|
7655
7893
|
async write(bundlePath2, input, actor = "unknown") {
|
|
7894
|
+
assertActor(actor);
|
|
7656
7895
|
if (!KB_SLUG_PATTERN.test(input.slug)) {
|
|
7657
7896
|
throw new KbInvalidConceptIdError("slug must be kebab-case", {
|
|
7658
7897
|
slug: input.slug
|
|
@@ -7754,6 +7993,7 @@ var KbStore = class {
|
|
|
7754
7993
|
* timeouts.
|
|
7755
7994
|
*/
|
|
7756
7995
|
async setStatus(bundlePath2, conceptId2, status, actor = "unknown") {
|
|
7996
|
+
assertActor(actor);
|
|
7757
7997
|
return this.mutate(
|
|
7758
7998
|
bundlePath2,
|
|
7759
7999
|
conceptId2,
|
|
@@ -7762,22 +8002,31 @@ var KbStore = class {
|
|
|
7762
8002
|
);
|
|
7763
8003
|
}
|
|
7764
8004
|
/**
|
|
7765
|
-
* Replaces a record's anchors
|
|
7766
|
-
*
|
|
7767
|
-
*
|
|
7768
|
-
*
|
|
7769
|
-
* stale entries the resolution pass deliberately dropped.
|
|
7770
|
-
*
|
|
7771
|
-
* Through the write schema: this is a write, and a defect a hand-edit put in
|
|
7772
|
-
* the frontmatter must not be published back out under an actor stamp.
|
|
8005
|
+
* Replaces a record's anchors, preserving everything else. An array is the
|
|
8006
|
+
* whole set; a function is a patch and runs inside the mutation, against
|
|
8007
|
+
* the anchors the record holds then — see
|
|
8008
|
+
* `decision.anchor-update-patch-inside-mutation`.
|
|
7773
8009
|
*/
|
|
7774
8010
|
async updateAnchors(bundlePath2, conceptId2, anchors, actor = "unknown") {
|
|
7775
|
-
|
|
8011
|
+
assertActor(actor);
|
|
8012
|
+
let entry = {
|
|
8013
|
+
operation: "anchor-resolve",
|
|
8014
|
+
by: actor
|
|
8015
|
+
};
|
|
7776
8016
|
return this.mutate(
|
|
7777
8017
|
bundlePath2,
|
|
7778
8018
|
conceptId2,
|
|
7779
|
-
(frontmatter) =>
|
|
7780
|
-
|
|
8019
|
+
(frontmatter) => {
|
|
8020
|
+
const write = typeof anchors === "function" ? anchors(frontmatter.strauss_anchors ?? []) : { anchors };
|
|
8021
|
+
if (write.log) entry = { ...write.log, by: actor };
|
|
8022
|
+
return {
|
|
8023
|
+
...frontmatter,
|
|
8024
|
+
strauss_anchors: write.anchors.map(
|
|
8025
|
+
(anchor) => kbAnchorWriteSchema.parse(anchor)
|
|
8026
|
+
)
|
|
8027
|
+
};
|
|
8028
|
+
},
|
|
8029
|
+
() => entry
|
|
7781
8030
|
);
|
|
7782
8031
|
}
|
|
7783
8032
|
/**
|
|
@@ -7792,6 +8041,7 @@ var KbStore = class {
|
|
|
7792
8041
|
* logs what it publishes.
|
|
7793
8042
|
*/
|
|
7794
8043
|
async verify(bundlePath2, conceptId2, note, actor = "unknown", at2 = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
8044
|
+
assertActor(actor, { named: true });
|
|
7795
8045
|
const event = kbVerifiedEventSchema.parse({ by: actor, at: at2, note });
|
|
7796
8046
|
const existing = await this.read(bundlePath2, conceptId2);
|
|
7797
8047
|
if (!existing) throw new KbRecordNotFoundError(conceptId2);
|
|
@@ -7822,6 +8072,7 @@ var KbStore = class {
|
|
|
7822
8072
|
* in normal use, and validation drops to catching hand-edits.
|
|
7823
8073
|
*/
|
|
7824
8074
|
async supersede(bundlePath2, conceptId2, replacementId, actor = "unknown") {
|
|
8075
|
+
assertActor(actor);
|
|
7825
8076
|
const replacement = await this.read(bundlePath2, replacementId);
|
|
7826
8077
|
if (!replacement) throw new KbRecordNotFoundError(replacementId);
|
|
7827
8078
|
const superseded = await this.markSuperseded(
|
|
@@ -7845,6 +8096,7 @@ var KbStore = class {
|
|
|
7845
8096
|
}
|
|
7846
8097
|
/** Resolves an open question, stamping who answered and when. */
|
|
7847
8098
|
async answer(bundlePath2, conceptId2, answer, actor = "unknown", at2 = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
8099
|
+
assertActor(actor);
|
|
7848
8100
|
return this.mutate(
|
|
7849
8101
|
bundlePath2,
|
|
7850
8102
|
conceptId2,
|
|
@@ -7871,6 +8123,7 @@ ${answer}
|
|
|
7871
8123
|
* terminal status since the caller listed it is reported, not removed.
|
|
7872
8124
|
*/
|
|
7873
8125
|
async deleteRecord(bundlePath2, conceptId2, expected, actor = "unknown") {
|
|
8126
|
+
assertActor(actor);
|
|
7874
8127
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
7875
8128
|
const witness = await this.read(bundlePath2, conceptId2);
|
|
7876
8129
|
if (!witness) throw new KbRecordNotFoundError(conceptId2);
|
|
@@ -8174,6 +8427,7 @@ ${answer}
|
|
|
8174
8427
|
* base too, where nothing was written.
|
|
8175
8428
|
*/
|
|
8176
8429
|
async note(bundlePath2, entry) {
|
|
8430
|
+
assertActor(entry.by);
|
|
8177
8431
|
await this.record(this.root(bundlePath2), entry);
|
|
8178
8432
|
}
|
|
8179
8433
|
/**
|
|
@@ -8226,7 +8480,10 @@ ${answer}
|
|
|
8226
8480
|
throw new KbWriteConflictError(conceptId2);
|
|
8227
8481
|
}
|
|
8228
8482
|
await this.publish(target, contents, true, conceptId2);
|
|
8229
|
-
await this.record(this.root(bundlePath2), {
|
|
8483
|
+
await this.record(this.root(bundlePath2), {
|
|
8484
|
+
...typeof entry === "function" ? entry() : entry,
|
|
8485
|
+
conceptId: conceptId2
|
|
8486
|
+
});
|
|
8230
8487
|
return { conceptId: conceptId2, frontmatter, body };
|
|
8231
8488
|
}
|
|
8232
8489
|
/**
|
|
@@ -8431,9 +8688,18 @@ function normalizeActor(id) {
|
|
|
8431
8688
|
if (colon === -1) return id.toLowerCase();
|
|
8432
8689
|
return id.slice(0, colon + 1).toLowerCase() + id.slice(colon + 1);
|
|
8433
8690
|
}
|
|
8691
|
+
var KB_ACTOR_PATTERN = /^[A-Za-z][\w-]*(?::[\p{L}\p{M}\p{N}_.@+/-]+)?$/u;
|
|
8692
|
+
function assertActor(actor, { named = false } = {}) {
|
|
8693
|
+
if (!KB_ACTOR_PATTERN.test(actor)) {
|
|
8694
|
+
throw new KbInvalidActorError(actor, "is not kind or kind:name");
|
|
8695
|
+
}
|
|
8696
|
+
if (named && normalizeActor(actor) === "unknown") {
|
|
8697
|
+
throw new KbInvalidActorError(actor, "cannot verify: name who checked");
|
|
8698
|
+
}
|
|
8699
|
+
}
|
|
8434
8700
|
|
|
8435
8701
|
// src/version.ts
|
|
8436
|
-
var VERSION = true ? "0.1.
|
|
8702
|
+
var VERSION = true ? "0.1.22" : "0.0.0-dev";
|
|
8437
8703
|
|
|
8438
8704
|
// src/mcp.ts
|
|
8439
8705
|
function createKbMcpServer() {
|