@saasontools/strauss-kb 0.1.21 → 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 +10 -1
- package/dist/{chunk-XBZ7G5R5.js → chunk-AEO5U42S.js} +3849 -3602
- package/dist/chunk-AEO5U42S.js.map +1 -0
- package/dist/{chunk-VXWN62HT.js → chunk-DVSPHL4B.js} +2 -2
- package/dist/{chunk-WVJULVGY.js → chunk-TSE4YQEG.js} +2 -2
- package/dist/cli-main.cjs +1433 -1194
- package/dist/cli-main.cjs.map +1 -1
- package/dist/cli-main.js +2 -2
- package/dist/index.cjs +421 -165
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +293 -17
- package/dist/index.d.ts +293 -17
- package/dist/index.js +17 -3
- package/dist/mcp-main.cjs +1433 -1194
- package/dist/mcp-main.cjs.map +1 -1
- package/dist/mcp-main.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-XBZ7G5R5.js.map +0 -1
- /package/dist/{chunk-VXWN62HT.js.map → chunk-DVSPHL4B.js.map} +0 -0
- /package/dist/{chunk-WVJULVGY.js.map → chunk-TSE4YQEG.js.map} +0 -0
package/dist/cli-main.cjs
CHANGED
|
@@ -31,569 +31,91 @@ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
|
31
31
|
var import_node_path15 = require("path");
|
|
32
32
|
|
|
33
33
|
// src/decision-record.ts
|
|
34
|
-
var
|
|
34
|
+
var import_zod4 = require("zod");
|
|
35
35
|
|
|
36
36
|
// src/compose.ts
|
|
37
|
-
var
|
|
37
|
+
var import_zod3 = require("zod");
|
|
38
38
|
|
|
39
|
-
// src/
|
|
40
|
-
var
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
last_modified: import_zod.z.string().min(1).optional()
|
|
47
|
-
}).passthrough();
|
|
48
|
-
var kbActorStampSchema = import_zod.z.object({
|
|
49
|
-
by: import_zod.z.string().min(1),
|
|
50
|
-
at: import_zod.z.string().min(1)
|
|
51
|
-
}).passthrough();
|
|
52
|
-
var kbVerifiedEventSchema = kbActorStampSchema.extend({
|
|
53
|
-
note: import_zod.z.string().refine((s) => s.trim().length > 0, {
|
|
54
|
-
message: "note must say what the check found"
|
|
55
|
-
})
|
|
56
|
-
});
|
|
57
|
-
var kbAnchorSpanSchema = import_zod.z.object({
|
|
58
|
-
start: import_zod.z.number().int().positive(),
|
|
59
|
-
end: import_zod.z.number().int().positive()
|
|
60
|
-
}).strict();
|
|
61
|
-
var kbAnchorSchema = import_zod.z.object({
|
|
62
|
-
file: import_zod.z.string().min(1),
|
|
63
|
-
symbol: import_zod.z.string().min(1).optional(),
|
|
64
|
-
/**
|
|
65
|
-
* The lines the concept names, when no symbol covers them — deleted code,
|
|
66
|
-
* YAML, SQL, Markdown. Alternative to `symbol`, never a refinement of it.
|
|
67
|
-
*/
|
|
68
|
-
span: kbAnchorSpanSchema.optional(),
|
|
69
|
-
/**
|
|
70
|
-
* Which side of the change the anchor describes. `old` is code as it was
|
|
71
|
-
* committed at `ref`, which is the only way to anchor something deleted;
|
|
72
|
-
* absent means the working tree.
|
|
73
|
-
*/
|
|
74
|
-
side: import_zod.z.enum(["old", "new"]).optional(),
|
|
75
|
-
/**
|
|
76
|
-
* Which repository the file lives in — a remote URL
|
|
77
|
-
* (`https://github.com/org/name`) or a short name. Absent means the base's
|
|
78
|
-
* own repository, which is what nearly every anchor means.
|
|
79
|
-
*
|
|
80
|
-
* Unvalidated beyond not-blank: one repository has many spellings, matched
|
|
81
|
-
* after normalisation. Only a full URL can be fetched from, so `validate`
|
|
82
|
-
* warns on a short one; see ARCHITECTURE.
|
|
83
|
-
*/
|
|
84
|
-
repo: import_zod.z.string().trim().min(1).optional(),
|
|
85
|
-
/**
|
|
86
|
-
* The git rev the evidence was taken at. Prefer a commit SHA: a branch
|
|
87
|
-
* name is a moving pointer, so an anchor pinned to one says the evidence
|
|
88
|
-
* came from wherever that branch happens to be now, which is not a
|
|
89
|
-
* baseline. A foreign anchor is checked at this rev, and compared against
|
|
90
|
-
* the remote's default branch on top of it.
|
|
91
|
-
*/
|
|
92
|
-
ref: import_zod.z.string().trim().min(1).optional(),
|
|
93
|
-
hash: import_zod.z.string().regex(/^sha256:[0-9a-f]{64}$/, {
|
|
94
|
-
message: "hash must be sha256:<64 hex chars>"
|
|
95
|
-
}).optional(),
|
|
96
|
-
/**
|
|
97
|
-
* What `hash` was taken over: the span's raw text, or the normalised token
|
|
98
|
-
* stream a parser sees (`ast`). Absent means `raw`, which is what every
|
|
99
|
-
* anchor stamped before this field carries, so old hashes keep comparing
|
|
100
|
-
* the way they were written. An `ast` hash is blind to whitespace and
|
|
101
|
-
* comments, so reformatting the anchored code is not drift.
|
|
102
|
-
*/
|
|
103
|
-
hash_kind: import_zod.z.enum(["raw", "ast"]).optional(),
|
|
104
|
-
/** ISO 8601 timestamp of the last successful resolution. */
|
|
105
|
-
resolved_at: import_zod.z.string().min(1).optional(),
|
|
106
|
-
/** Line count of the text the hash was taken over. */
|
|
107
|
-
lines: import_zod.z.number().int().positive().optional(),
|
|
108
|
-
/**
|
|
109
|
-
* Which resolver produced the hashed span. Absent means an anchor stamped
|
|
110
|
-
* before resolvers were named, which is read as `regex` — the only one
|
|
111
|
-
* there was. A hash from a different resolver is drift, not a match.
|
|
112
|
-
*/
|
|
113
|
-
resolver: import_zod.z.enum(["tree-sitter", "regex", "span"]).optional()
|
|
114
|
-
}).strict();
|
|
115
|
-
var kbAnchorWriteSchema = kbAnchorSchema.superRefine((anchor, ctx) => {
|
|
116
|
-
if (anchor.span && anchor.symbol) {
|
|
117
|
-
ctx.addIssue({
|
|
118
|
-
code: import_zod.z.ZodIssueCode.custom,
|
|
119
|
-
path: ["span"],
|
|
120
|
-
message: "an anchor names a symbol or a span, not both"
|
|
121
|
-
});
|
|
39
|
+
// src/concurrency.ts
|
|
40
|
+
var DEFAULT_IO_CONCURRENCY = 16;
|
|
41
|
+
async function mapLimit(items, limit, fn) {
|
|
42
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
43
|
+
throw new RangeError(
|
|
44
|
+
`mapLimit: "limit" must be a positive integer, got ${limit}`
|
|
45
|
+
);
|
|
122
46
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
47
|
+
const out = new Array(items.length);
|
|
48
|
+
let next = 0;
|
|
49
|
+
let failed = false;
|
|
50
|
+
const runners = Array.from(
|
|
51
|
+
{ length: Math.min(limit, items.length) },
|
|
52
|
+
async () => {
|
|
53
|
+
while (!failed && next < items.length) {
|
|
54
|
+
const at2 = next++;
|
|
55
|
+
try {
|
|
56
|
+
out[at2] = await fn(items[at2], at2);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
failed = true;
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
await Promise.all(runners);
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/drift/git.ts
|
|
69
|
+
var import_node_child_process2 = require("child_process");
|
|
70
|
+
var import_node_util2 = require("util");
|
|
71
|
+
|
|
72
|
+
// src/remote-repo/git.ts
|
|
73
|
+
var import_node_child_process = require("child_process");
|
|
74
|
+
var import_node_util = require("util");
|
|
75
|
+
|
|
76
|
+
// src/anchor-resolver/model.ts
|
|
77
|
+
var MAX_ANCHOR_FILE_BYTES = 1048576;
|
|
78
|
+
|
|
79
|
+
// src/remote-repo/git.ts
|
|
80
|
+
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
81
|
+
function childEnv() {
|
|
82
|
+
const env = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
|
|
83
|
+
for (const name of ["GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE"]) {
|
|
84
|
+
delete env[name];
|
|
129
85
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
86
|
+
return env;
|
|
87
|
+
}
|
|
88
|
+
async function git(args, options = {}) {
|
|
89
|
+
try {
|
|
90
|
+
const { stdout, stderr } = await execFileAsync("git", args, {
|
|
91
|
+
...options.cwd ? { cwd: options.cwd } : {},
|
|
92
|
+
timeout: options.timeoutMs ?? 3e4,
|
|
93
|
+
maxBuffer: options.maxBytes ?? MAX_ANCHOR_FILE_BYTES,
|
|
94
|
+
encoding: "utf8",
|
|
95
|
+
windowsHide: true,
|
|
96
|
+
env: childEnv()
|
|
135
97
|
});
|
|
98
|
+
return { ok: true, stdout, stderr, overflowed: false };
|
|
99
|
+
} catch (error) {
|
|
100
|
+
const failure = error;
|
|
101
|
+
return {
|
|
102
|
+
ok: false,
|
|
103
|
+
stdout: failure.stdout ?? "",
|
|
104
|
+
stderr: failure.stderr ?? "",
|
|
105
|
+
overflowed: failure.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER"
|
|
106
|
+
};
|
|
136
107
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
});
|
|
108
|
+
}
|
|
109
|
+
function transportReason(stderr) {
|
|
110
|
+
const text = stderr.toLowerCase();
|
|
111
|
+
if (text.includes("authentication failed") || text.includes("permission denied") || text.includes("could not read username") || text.includes("403 forbidden") || text.includes("access denied")) {
|
|
112
|
+
return "repo-unauthorized";
|
|
143
113
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
var KB_RECORD_TYPES = [
|
|
150
|
-
"fact",
|
|
151
|
-
"requirement",
|
|
152
|
-
"constraint",
|
|
153
|
-
"decision",
|
|
154
|
-
"assumption",
|
|
155
|
-
"open-question",
|
|
156
|
-
"risk",
|
|
157
|
-
"contract",
|
|
158
|
-
"flow",
|
|
159
|
-
"affected-system",
|
|
160
|
-
"test-obligation",
|
|
161
|
-
"source-note"
|
|
162
|
-
];
|
|
163
|
-
var KB_SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
164
|
-
var KB_CONCEPT_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*\.[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
165
|
-
var kbConceptIdSchema = import_zod.z.string().regex(KB_CONCEPT_ID_PATTERN, {
|
|
166
|
-
message: "concept id must be <type>.<slug>, both kebab-case"
|
|
167
|
-
});
|
|
168
|
-
var KB_RECORD_STATUSES = [
|
|
169
|
-
"draft",
|
|
170
|
-
"proposed",
|
|
171
|
-
"accepted",
|
|
172
|
-
"open",
|
|
173
|
-
"resolved",
|
|
174
|
-
"rejected",
|
|
175
|
-
"superseded"
|
|
176
|
-
];
|
|
177
|
-
var KB_MATERIALITIES = [
|
|
178
|
-
"blocking",
|
|
179
|
-
"important",
|
|
180
|
-
"non-blocking"
|
|
181
|
-
];
|
|
182
|
-
var KB_CONFIDENCES = ["low", "medium", "high"];
|
|
183
|
-
var kbRecordFrontmatterSchema = import_zod.z.object({
|
|
184
|
-
// OKF: the only always-required key. A concept carrying just `type` is
|
|
185
|
-
// fully conformant, so everything below stays optional.
|
|
186
|
-
type: import_zod.z.string().min(1),
|
|
187
|
-
// OKF recommended.
|
|
188
|
-
title: import_zod.z.string().min(1).optional(),
|
|
189
|
-
description: import_zod.z.string().min(1).optional(),
|
|
190
|
-
resource: import_zod.z.string().min(1).optional(),
|
|
191
|
-
tags: import_zod.z.array(import_zod.z.string()).optional(),
|
|
192
|
-
// OKF optional: provenance and freshness.
|
|
193
|
-
sources: import_zod.z.array(kbSourceSchema).optional(),
|
|
194
|
-
generated: kbActorStampSchema.optional(),
|
|
195
|
-
verified: import_zod.z.array(kbActorStampSchema).optional(),
|
|
196
|
-
stale_after: import_zod.z.string().min(1).optional(),
|
|
197
|
-
// strauss extensions — see the module comment.
|
|
198
|
-
strauss_anchors: import_zod.z.array(kbAnchorSchema).optional(),
|
|
199
|
-
strauss_verify: import_zod.z.array(import_zod.z.string().min(1)).optional(),
|
|
200
|
-
// Typed causal edges, source → target, living on the source. `A depends_on
|
|
201
|
-
// B` means A needs B, so `kb_impact` walks these inbound: what breaks if B
|
|
202
|
-
// changes is whatever declared a dependence on it.
|
|
203
|
-
strauss_links: import_zod.z.array(kbLinkSchema).optional(),
|
|
204
|
-
// Total after parsing, tolerant before it. Our producers must supply a
|
|
205
|
-
// status — an absent one would leave every reader inventing its own default
|
|
206
|
-
// — but OKF calls a concept carrying only `type` fully conformant, so
|
|
207
|
-
// rejecting a foreign record for the lack of one would put us outside the
|
|
208
|
-
// spec. The default resolves it in the single place that can: here.
|
|
209
|
-
strauss_status: import_zod.z.enum(KB_RECORD_STATUSES).default("draft"),
|
|
210
|
-
strauss_supersedes: import_zod.z.array(import_zod.z.string().min(1)).optional(),
|
|
211
|
-
strauss_superseded_by: import_zod.z.string().min(1).optional(),
|
|
212
|
-
strauss_answered: kbActorStampSchema.optional(),
|
|
213
|
-
strauss_materiality: import_zod.z.enum(KB_MATERIALITIES).optional(),
|
|
214
|
-
strauss_confidence: import_zod.z.enum(KB_CONFIDENCES).optional(),
|
|
215
|
-
strauss_owner: import_zod.z.string().min(1).optional(),
|
|
216
|
-
// "No source exists" as a field rather than a sentinel entry inside
|
|
217
|
-
// `sources`. A sentinel in a reference list is a value doing work a field
|
|
218
|
-
// should do; as a field, `sources` may be legitimately empty.
|
|
219
|
-
strauss_assumption: import_zod.z.boolean().optional()
|
|
220
|
-
}).passthrough();
|
|
221
|
-
|
|
222
|
-
// src/record-types.ts
|
|
223
|
-
var RECORD_TYPES = {
|
|
224
|
-
fact: {
|
|
225
|
-
purpose: "Observed or sourced fact",
|
|
226
|
-
sections: ["Claim", "Evidence", "Implication"],
|
|
227
|
-
initialStatus: "accepted"
|
|
228
|
-
},
|
|
229
|
-
requirement: {
|
|
230
|
-
purpose: "Required behavior or outcome",
|
|
231
|
-
sections: ["Claim", "Evidence", "Implication"],
|
|
232
|
-
initialStatus: "proposed"
|
|
233
|
-
},
|
|
234
|
-
constraint: {
|
|
235
|
-
purpose: "Limitation, compatibility boundary, policy, or restriction",
|
|
236
|
-
sections: ["Claim", "Evidence", "Implication"],
|
|
237
|
-
initialStatus: "accepted"
|
|
238
|
-
},
|
|
239
|
-
decision: {
|
|
240
|
-
purpose: "Chosen or proposed direction",
|
|
241
|
-
sections: ["Decision", "Rationale", "Rejected", "Impact"],
|
|
242
|
-
initialStatus: "accepted"
|
|
243
|
-
},
|
|
244
|
-
assumption: {
|
|
245
|
-
purpose: "Unsourced or not-yet-confirmed working assumption",
|
|
246
|
-
sections: ["Claim", "Why we think so", "What would falsify it"],
|
|
247
|
-
initialStatus: "draft"
|
|
248
|
-
},
|
|
249
|
-
"open-question": {
|
|
250
|
-
purpose: "Question needing resolution",
|
|
251
|
-
sections: ["Question", "Why it matters", "Default assumption"],
|
|
252
|
-
initialStatus: "open"
|
|
253
|
-
},
|
|
254
|
-
risk: {
|
|
255
|
-
purpose: "Something that can go wrong",
|
|
256
|
-
sections: ["Risk", "Why it matters", "Mitigation", "Verification"],
|
|
257
|
-
initialStatus: "open"
|
|
258
|
-
},
|
|
259
|
-
contract: {
|
|
260
|
-
purpose: "API, data, event, schema, or permission contract",
|
|
261
|
-
sections: ["Contract", "Producer", "Consumer", "Compatibility"],
|
|
262
|
-
initialStatus: "proposed"
|
|
263
|
-
},
|
|
264
|
-
flow: {
|
|
265
|
-
purpose: "Sequence, lifecycle, or state behavior",
|
|
266
|
-
sections: ["Flow", "Trigger", "Steps", "Failure modes"],
|
|
267
|
-
initialStatus: "accepted"
|
|
268
|
-
},
|
|
269
|
-
"affected-system": {
|
|
270
|
-
purpose: "Component, service, package, integration, or external system",
|
|
271
|
-
sections: ["System", "How it is affected", "Blast radius"],
|
|
272
|
-
initialStatus: "accepted"
|
|
273
|
-
},
|
|
274
|
-
"test-obligation": {
|
|
275
|
-
purpose: "Behavior or contract that must be verified",
|
|
276
|
-
sections: ["Obligation", "Why it matters", "How to verify"],
|
|
277
|
-
initialStatus: "open"
|
|
278
|
-
},
|
|
279
|
-
"source-note": {
|
|
280
|
-
purpose: "Extracted note from source material",
|
|
281
|
-
sections: ["Note", "Where it came from"],
|
|
282
|
-
initialStatus: "accepted"
|
|
283
|
-
}
|
|
284
|
-
};
|
|
285
|
-
function isKbRecordType(value) {
|
|
286
|
-
return Object.prototype.hasOwnProperty.call(RECORD_TYPES, value);
|
|
287
|
-
}
|
|
288
|
-
var KB_LINK_RELS = [
|
|
289
|
-
"depends_on",
|
|
290
|
-
"constrains",
|
|
291
|
-
"informs",
|
|
292
|
-
"blocks",
|
|
293
|
-
"invalidates",
|
|
294
|
-
"verified_by",
|
|
295
|
-
"satisfies",
|
|
296
|
-
"related_to"
|
|
297
|
-
];
|
|
298
|
-
var LINK_RELS = {
|
|
299
|
-
depends_on: {
|
|
300
|
-
purpose: "The source needs the target to hold; the source breaks if the target changes",
|
|
301
|
-
phrase: "Depends on",
|
|
302
|
-
dependant: "source"
|
|
303
|
-
},
|
|
304
|
-
constrains: {
|
|
305
|
-
purpose: "The source bounds what the target may do; the target breaks if the constraint changes",
|
|
306
|
-
phrase: "Constrains",
|
|
307
|
-
dependant: "target"
|
|
308
|
-
},
|
|
309
|
-
informs: {
|
|
310
|
-
purpose: "The source shaped the target without binding it; the target is what needs revisiting",
|
|
311
|
-
phrase: "Informs",
|
|
312
|
-
dependant: "target"
|
|
313
|
-
},
|
|
314
|
-
blocks: {
|
|
315
|
-
purpose: "The target cannot proceed until the source is settled; the target is what waits",
|
|
316
|
-
phrase: "Blocks",
|
|
317
|
-
dependant: "target"
|
|
318
|
-
},
|
|
319
|
-
invalidates: {
|
|
320
|
-
purpose: "The source makes the target no longer hold; the target is what stops holding",
|
|
321
|
-
phrase: "Invalidates",
|
|
322
|
-
dependant: "target"
|
|
323
|
-
},
|
|
324
|
-
verified_by: {
|
|
325
|
-
purpose: "The target is the check that confirms the source; the source's confirmation moves with it",
|
|
326
|
-
phrase: "Verified by",
|
|
327
|
-
dependant: "source"
|
|
328
|
-
},
|
|
329
|
-
satisfies: {
|
|
330
|
-
purpose: "The source discharges the target's requirement; the source must change if the requirement does",
|
|
331
|
-
phrase: "Satisfies",
|
|
332
|
-
dependant: "source"
|
|
333
|
-
},
|
|
334
|
-
related_to: {
|
|
335
|
-
purpose: "A pointer worth following, with no claim of dependence",
|
|
336
|
-
phrase: "Relates to",
|
|
337
|
-
dependant: null
|
|
338
|
-
}
|
|
339
|
-
};
|
|
340
|
-
var KB_CAUSAL_LINK_RELS = KB_LINK_RELS.filter(
|
|
341
|
-
(rel) => LINK_RELS[rel].dependant !== null
|
|
342
|
-
);
|
|
343
|
-
function isKbLinkRel(value) {
|
|
344
|
-
return Object.prototype.hasOwnProperty.call(LINK_RELS, value);
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// src/compose.ts
|
|
348
|
-
var composeLinkSchema = import_zod2.z.object({
|
|
349
|
-
target: kbConceptIdSchema,
|
|
350
|
-
rel: import_zod2.z.enum(KB_LINK_RELS)
|
|
351
|
-
}).strict();
|
|
352
|
-
var composeInputSchema = import_zod2.z.object({
|
|
353
|
-
slug: import_zod2.z.string().min(1),
|
|
354
|
-
/** One line, in the reader's terms. Becomes OKF `title`. */
|
|
355
|
-
title: import_zod2.z.string().min(1),
|
|
356
|
-
/** The consequence — what breaks if this is wrong. Becomes `description`. */
|
|
357
|
-
why: import_zod2.z.string().min(1),
|
|
358
|
-
/** Keyed by section heading from the type's spec. Unknown keys rejected. */
|
|
359
|
-
sections: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.string().min(1)).optional(),
|
|
360
|
-
anchors: import_zod2.z.array(kbAnchorWriteSchema).optional(),
|
|
361
|
-
sources: import_zod2.z.array(kbSourceSchema).optional(),
|
|
362
|
-
/** No source exists, as a claim rather than a sentinel in `sources`. */
|
|
363
|
-
assumption: import_zod2.z.boolean().optional(),
|
|
364
|
-
/**
|
|
365
|
-
* OKF `stale_after`: the absolute date this record stops being trusted.
|
|
366
|
-
* Anything the outside world can change — pricing, quotas, versions,
|
|
367
|
-
* reception counts — should carry one.
|
|
368
|
-
*/
|
|
369
|
-
stale_after: import_zod2.z.string().regex(/^\d{4}-\d{2}-\d{2}$/, {
|
|
370
|
-
message: "stale_after must be YYYY-MM-DD"
|
|
371
|
-
}).refine((date) => !Number.isNaN(Date.parse(date)), {
|
|
372
|
-
message: "stale_after must be a real date"
|
|
373
|
-
}).optional(),
|
|
374
|
-
verify: import_zod2.z.array(import_zod2.z.string().min(1)).optional(),
|
|
375
|
-
tags: import_zod2.z.array(import_zod2.z.string().min(1)).optional(),
|
|
376
|
-
/** Concept ids this record relates to; rendered as body links. */
|
|
377
|
-
relatedConceptIds: import_zod2.z.array(kbConceptIdSchema).optional(),
|
|
378
|
-
/**
|
|
379
|
-
* Typed causal edges, source → target: `{ target: "fact.b", rel:
|
|
380
|
-
* "depends_on" }` on record A says A needs B. Stored in frontmatter and
|
|
381
|
-
* also rendered as one prose sentence each, so the meaning survives a
|
|
382
|
-
* reader that knows only OKF. The vocabulary goes into the description from
|
|
383
|
-
* the same table the walk uses, so `kb_schema` emits it.
|
|
384
|
-
*/
|
|
385
|
-
links: import_zod2.z.array(composeLinkSchema).max(64).optional().describe(
|
|
386
|
-
`Typed causal edges, source \u2192 target \u2014 a link on this record says this record <rel> the target. ${KB_LINK_RELS.map(
|
|
387
|
-
(rel) => `${rel}: ${LINK_RELS[rel].purpose}`
|
|
388
|
-
).join("; ")}.`
|
|
389
|
-
),
|
|
390
|
-
/** Concept ids this record replaces. The store settles the backlinks. */
|
|
391
|
-
supersedes: import_zod2.z.array(kbConceptIdSchema).max(32).optional(),
|
|
392
|
-
materiality: import_zod2.z.enum(KB_MATERIALITIES).optional(),
|
|
393
|
-
confidence: import_zod2.z.enum(KB_CONFIDENCES).optional(),
|
|
394
|
-
owner: import_zod2.z.string().min(1).optional()
|
|
395
|
-
}).strict();
|
|
396
|
-
function composeRecord(type, input, writtenBy, writtenAt) {
|
|
397
|
-
const parsed = composeInputSchema.parse(input);
|
|
398
|
-
const spec = RECORD_TYPES[type];
|
|
399
|
-
const sections = parsed.sections ?? {};
|
|
400
|
-
const unknown = Object.keys(sections).filter(
|
|
401
|
-
(heading) => !spec.sections.includes(heading)
|
|
402
|
-
);
|
|
403
|
-
if (unknown.length) {
|
|
404
|
-
throw new Error(
|
|
405
|
-
`kb: ${type} has no section ${unknown.join(", ")} \u2014 expected one of ${spec.sections.join(", ")}`
|
|
406
|
-
);
|
|
407
|
-
}
|
|
408
|
-
const frontmatter = {
|
|
409
|
-
title: parsed.title,
|
|
410
|
-
description: parsed.why,
|
|
411
|
-
generated: { by: writtenBy, at: writtenAt },
|
|
412
|
-
// Empty rather than absent: a later verification pass appends here, and an
|
|
413
|
-
// empty list says "not yet verified" where a missing key would only say
|
|
414
|
-
// "this producer didn't think about it".
|
|
415
|
-
verified: [],
|
|
416
|
-
strauss_status: spec.initialStatus
|
|
417
|
-
};
|
|
418
|
-
if (parsed.stale_after) frontmatter.stale_after = parsed.stale_after;
|
|
419
|
-
if (parsed.anchors?.length) frontmatter.strauss_anchors = parsed.anchors;
|
|
420
|
-
if (parsed.verify?.length) frontmatter.strauss_verify = parsed.verify;
|
|
421
|
-
if (parsed.tags?.length) frontmatter.tags = parsed.tags;
|
|
422
|
-
if (parsed.sources?.length) frontmatter.sources = parsed.sources;
|
|
423
|
-
if (parsed.assumption) frontmatter.strauss_assumption = true;
|
|
424
|
-
if (parsed.materiality) frontmatter.strauss_materiality = parsed.materiality;
|
|
425
|
-
if (parsed.confidence) frontmatter.strauss_confidence = parsed.confidence;
|
|
426
|
-
if (parsed.owner) frontmatter.strauss_owner = parsed.owner;
|
|
427
|
-
if (parsed.supersedes?.length)
|
|
428
|
-
frontmatter.strauss_supersedes = parsed.supersedes;
|
|
429
|
-
const selfLink = parsed.links?.find(
|
|
430
|
-
(link2) => link2.target === `${type}.${parsed.slug}`
|
|
431
|
-
);
|
|
432
|
-
if (selfLink) {
|
|
433
|
-
throw new Error(
|
|
434
|
-
`kb: ${type}.${parsed.slug} cannot ${selfLink.rel} itself \u2014 a link must name another record`
|
|
435
|
-
);
|
|
436
|
-
}
|
|
437
|
-
if (parsed.links?.length) frontmatter.strauss_links = parsed.links;
|
|
438
|
-
const blocks = [];
|
|
439
|
-
for (const heading of spec.sections) {
|
|
440
|
-
const text = sections[heading];
|
|
441
|
-
if (text) blocks.push(`## ${heading}
|
|
442
|
-
|
|
443
|
-
${text}`);
|
|
444
|
-
}
|
|
445
|
-
if (!blocks.length) blocks.push(parsed.why);
|
|
446
|
-
for (const related of parsed.relatedConceptIds ?? []) {
|
|
447
|
-
blocks.push(`Relates to [${related}](${related}.md).`);
|
|
448
|
-
}
|
|
449
|
-
for (const link2 of parsed.links ?? []) {
|
|
450
|
-
blocks.push(
|
|
451
|
-
`${LINK_RELS[link2.rel].phrase} [${link2.target}](${link2.target}.md).`
|
|
452
|
-
);
|
|
453
|
-
}
|
|
454
|
-
if (parsed.sources?.length) {
|
|
455
|
-
blocks.push(
|
|
456
|
-
parsed.sources.map((source) => `[^${source.id}]: ${source.title ?? source.resource}`).join("\n")
|
|
457
|
-
);
|
|
458
|
-
}
|
|
459
|
-
return {
|
|
460
|
-
type,
|
|
461
|
-
slug: parsed.slug,
|
|
462
|
-
frontmatter,
|
|
463
|
-
body: `${blocks.join("\n\n")}
|
|
464
|
-
`
|
|
465
|
-
};
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// src/decision-record.ts
|
|
469
|
-
var DECISION_TYPE = "decision";
|
|
470
|
-
var NO_DECISION_SLUG = "none";
|
|
471
|
-
var decisionInputSchema = composeInputSchema.omit({ sections: true }).extend({
|
|
472
|
-
alternative: import_zod3.z.string().min(1).optional(),
|
|
473
|
-
impact: import_zod3.z.string().min(1).optional()
|
|
474
|
-
}).strict();
|
|
475
|
-
function composeDecisionRecord(input, writtenBy, writtenAt) {
|
|
476
|
-
const { alternative, impact: impact2, ...rest } = input;
|
|
477
|
-
return composeRecord(
|
|
478
|
-
DECISION_TYPE,
|
|
479
|
-
{
|
|
480
|
-
...rest,
|
|
481
|
-
sections: {
|
|
482
|
-
Decision: input.title,
|
|
483
|
-
Rationale: input.why,
|
|
484
|
-
...alternative ? { Rejected: alternative } : {},
|
|
485
|
-
...impact2 ? { Impact: impact2 } : {}
|
|
486
|
-
}
|
|
487
|
-
},
|
|
488
|
-
writtenBy,
|
|
489
|
-
writtenAt
|
|
490
|
-
);
|
|
491
|
-
}
|
|
492
|
-
function composeNoDecisionRecord(reason, writtenBy, writtenAt) {
|
|
493
|
-
return composeRecord(
|
|
494
|
-
DECISION_TYPE,
|
|
495
|
-
{
|
|
496
|
-
slug: NO_DECISION_SLUG,
|
|
497
|
-
title: "No decision to record",
|
|
498
|
-
why: reason,
|
|
499
|
-
sections: { Decision: reason }
|
|
500
|
-
},
|
|
501
|
-
writtenBy,
|
|
502
|
-
writtenAt
|
|
503
|
-
);
|
|
504
|
-
}
|
|
505
|
-
function isNoDecisionRecord(record) {
|
|
506
|
-
return record.conceptId === `${DECISION_TYPE}.${NO_DECISION_SLUG}`;
|
|
507
|
-
}
|
|
508
|
-
function selectDecisions(records) {
|
|
509
|
-
return records.filter(
|
|
510
|
-
(record) => record.conceptId.startsWith(`${DECISION_TYPE}.`) && !isNoDecisionRecord(record)
|
|
511
|
-
);
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
// src/commands/anchor-resolve.ts
|
|
515
|
-
var import_zod7 = require("zod");
|
|
516
|
-
|
|
517
|
-
// src/concurrency.ts
|
|
518
|
-
var DEFAULT_IO_CONCURRENCY = 16;
|
|
519
|
-
async function mapLimit(items, limit, fn) {
|
|
520
|
-
if (!Number.isInteger(limit) || limit < 1) {
|
|
521
|
-
throw new RangeError(
|
|
522
|
-
`mapLimit: "limit" must be a positive integer, got ${limit}`
|
|
523
|
-
);
|
|
524
|
-
}
|
|
525
|
-
const out = new Array(items.length);
|
|
526
|
-
let next = 0;
|
|
527
|
-
let failed = false;
|
|
528
|
-
const runners = Array.from(
|
|
529
|
-
{ length: Math.min(limit, items.length) },
|
|
530
|
-
async () => {
|
|
531
|
-
while (!failed && next < items.length) {
|
|
532
|
-
const at2 = next++;
|
|
533
|
-
try {
|
|
534
|
-
out[at2] = await fn(items[at2], at2);
|
|
535
|
-
} catch (error) {
|
|
536
|
-
failed = true;
|
|
537
|
-
throw error;
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
);
|
|
542
|
-
await Promise.all(runners);
|
|
543
|
-
return out;
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
// src/drift/git.ts
|
|
547
|
-
var import_node_child_process2 = require("child_process");
|
|
548
|
-
var import_node_util2 = require("util");
|
|
549
|
-
|
|
550
|
-
// src/remote-repo/git.ts
|
|
551
|
-
var import_node_child_process = require("child_process");
|
|
552
|
-
var import_node_util = require("util");
|
|
553
|
-
|
|
554
|
-
// src/anchor-resolver/model.ts
|
|
555
|
-
var MAX_ANCHOR_FILE_BYTES = 1048576;
|
|
556
|
-
|
|
557
|
-
// src/remote-repo/git.ts
|
|
558
|
-
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
559
|
-
function childEnv() {
|
|
560
|
-
const env = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
|
|
561
|
-
for (const name of ["GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE"]) {
|
|
562
|
-
delete env[name];
|
|
563
|
-
}
|
|
564
|
-
return env;
|
|
565
|
-
}
|
|
566
|
-
async function git(args, options = {}) {
|
|
567
|
-
try {
|
|
568
|
-
const { stdout, stderr } = await execFileAsync("git", args, {
|
|
569
|
-
...options.cwd ? { cwd: options.cwd } : {},
|
|
570
|
-
timeout: options.timeoutMs ?? 3e4,
|
|
571
|
-
maxBuffer: options.maxBytes ?? MAX_ANCHOR_FILE_BYTES,
|
|
572
|
-
encoding: "utf8",
|
|
573
|
-
windowsHide: true,
|
|
574
|
-
env: childEnv()
|
|
575
|
-
});
|
|
576
|
-
return { ok: true, stdout, stderr, overflowed: false };
|
|
577
|
-
} catch (error) {
|
|
578
|
-
const failure = error;
|
|
579
|
-
return {
|
|
580
|
-
ok: false,
|
|
581
|
-
stdout: failure.stdout ?? "",
|
|
582
|
-
stderr: failure.stderr ?? "",
|
|
583
|
-
overflowed: failure.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER"
|
|
584
|
-
};
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
function transportReason(stderr) {
|
|
588
|
-
const text = stderr.toLowerCase();
|
|
589
|
-
if (text.includes("authentication failed") || text.includes("permission denied") || text.includes("could not read username") || text.includes("403 forbidden") || text.includes("access denied")) {
|
|
590
|
-
return "repo-unauthorized";
|
|
591
|
-
}
|
|
592
|
-
if (text.includes("couldn't find remote ref") || text.includes("unadvertised object") || text.includes("not our ref")) {
|
|
593
|
-
return "ref-not-found";
|
|
594
|
-
}
|
|
595
|
-
return "remote-unreachable";
|
|
596
|
-
}
|
|
114
|
+
if (text.includes("couldn't find remote ref") || text.includes("unadvertised object") || text.includes("not our ref")) {
|
|
115
|
+
return "ref-not-found";
|
|
116
|
+
}
|
|
117
|
+
return "remote-unreachable";
|
|
118
|
+
}
|
|
597
119
|
|
|
598
120
|
// src/remote-repo/validate.ts
|
|
599
121
|
var MAX_REF_LENGTH = 200;
|
|
@@ -1326,26 +848,26 @@ var import_node_path4 = require("path");
|
|
|
1326
848
|
var import_node_url = require("url");
|
|
1327
849
|
|
|
1328
850
|
// src/grammars/model.ts
|
|
1329
|
-
var
|
|
1330
|
-
var sha2562 =
|
|
1331
|
-
var grammarWasmSchema =
|
|
1332
|
-
url:
|
|
851
|
+
var import_zod = require("zod");
|
|
852
|
+
var sha2562 = import_zod.z.string().regex(/^[0-9a-f]{64}$/);
|
|
853
|
+
var grammarWasmSchema = import_zod.z.object({
|
|
854
|
+
url: import_zod.z.string().min(1),
|
|
1333
855
|
sha256: sha2562,
|
|
1334
|
-
bytes:
|
|
856
|
+
bytes: import_zod.z.number().int().positive()
|
|
1335
857
|
});
|
|
1336
|
-
var grammarTagsSchema =
|
|
1337
|
-
var grammarPackSchema =
|
|
1338
|
-
package:
|
|
858
|
+
var grammarTagsSchema = import_zod.z.object({ url: import_zod.z.string().min(1), sha256: sha2562 });
|
|
859
|
+
var grammarPackSchema = import_zod.z.object({
|
|
860
|
+
package: import_zod.z.string().min(1),
|
|
1339
861
|
wasm: grammarWasmSchema,
|
|
1340
|
-
tags:
|
|
1341
|
-
license:
|
|
1342
|
-
extensions:
|
|
862
|
+
tags: import_zod.z.array(grammarTagsSchema),
|
|
863
|
+
license: import_zod.z.string().min(1),
|
|
864
|
+
extensions: import_zod.z.array(import_zod.z.string().min(1))
|
|
1343
865
|
});
|
|
1344
|
-
var grammarManifestSchema =
|
|
866
|
+
var grammarManifestSchema = import_zod.z.object({
|
|
1345
867
|
/** The runtime the packs were proved against. */
|
|
1346
|
-
webTreeSitter:
|
|
1347
|
-
linguist:
|
|
1348
|
-
packs:
|
|
868
|
+
webTreeSitter: import_zod.z.string().min(1),
|
|
869
|
+
linguist: import_zod.z.object({ tag: import_zod.z.string().min(1), commit: import_zod.z.string().min(1) }),
|
|
870
|
+
packs: import_zod.z.record(import_zod.z.string().min(1), grammarPackSchema)
|
|
1349
871
|
});
|
|
1350
872
|
|
|
1351
873
|
// src/grammars/manifest.ts
|
|
@@ -1783,505 +1305,1092 @@ var TreeSitterResolver = class {
|
|
|
1783
1305
|
tree.delete();
|
|
1784
1306
|
}
|
|
1785
1307
|
}
|
|
1786
|
-
/** Drops cached trees. Grammars stay loaded — they are immutable. */
|
|
1787
|
-
reset() {
|
|
1788
|
-
for (const parsed of this.trees.values()) parsed.tree.delete();
|
|
1789
|
-
this.trees.clear();
|
|
1790
|
-
this.stats.parses = 0;
|
|
1791
|
-
this.stats.cacheHits = 0;
|
|
1308
|
+
/** Drops cached trees. Grammars stay loaded — they are immutable. */
|
|
1309
|
+
reset() {
|
|
1310
|
+
for (const parsed of this.trees.values()) parsed.tree.delete();
|
|
1311
|
+
this.trees.clear();
|
|
1312
|
+
this.stats.parses = 0;
|
|
1313
|
+
this.stats.cacheHits = 0;
|
|
1314
|
+
}
|
|
1315
|
+
};
|
|
1316
|
+
function why(error) {
|
|
1317
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
1318
|
+
return text || "no reason given";
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
// src/anchor-resolver/resolver.ts
|
|
1322
|
+
var PARENT_SCOPE_LINES = 50;
|
|
1323
|
+
var CLEAN_STATE = { blockComment: false, template: false };
|
|
1324
|
+
function stripLine(line, state) {
|
|
1325
|
+
let out = "";
|
|
1326
|
+
let index2 = 0;
|
|
1327
|
+
let { blockComment, template } = state;
|
|
1328
|
+
while (index2 < line.length) {
|
|
1329
|
+
const char = line[index2];
|
|
1330
|
+
const next = line[index2 + 1];
|
|
1331
|
+
if (blockComment) {
|
|
1332
|
+
if (char === "*" && next === "/") {
|
|
1333
|
+
blockComment = false;
|
|
1334
|
+
index2 += 2;
|
|
1335
|
+
continue;
|
|
1336
|
+
}
|
|
1337
|
+
index2 += 1;
|
|
1338
|
+
continue;
|
|
1339
|
+
}
|
|
1340
|
+
if (template) {
|
|
1341
|
+
if (char === "\\") {
|
|
1342
|
+
index2 += 2;
|
|
1343
|
+
continue;
|
|
1344
|
+
}
|
|
1345
|
+
if (char === "`") template = false;
|
|
1346
|
+
index2 += 1;
|
|
1347
|
+
continue;
|
|
1348
|
+
}
|
|
1349
|
+
if (char === "/" && next === "*") {
|
|
1350
|
+
blockComment = true;
|
|
1351
|
+
index2 += 2;
|
|
1352
|
+
continue;
|
|
1353
|
+
}
|
|
1354
|
+
if (char === "/" && next === "/") break;
|
|
1355
|
+
if (char === "`") {
|
|
1356
|
+
template = true;
|
|
1357
|
+
index2 += 1;
|
|
1358
|
+
continue;
|
|
1359
|
+
}
|
|
1360
|
+
if (char === "'" || char === '"') {
|
|
1361
|
+
const quote = char;
|
|
1362
|
+
index2 += 1;
|
|
1363
|
+
while (index2 < line.length) {
|
|
1364
|
+
if (line[index2] === "\\") {
|
|
1365
|
+
index2 += 2;
|
|
1366
|
+
continue;
|
|
1367
|
+
}
|
|
1368
|
+
if (line[index2] === quote) {
|
|
1369
|
+
index2 += 1;
|
|
1370
|
+
break;
|
|
1371
|
+
}
|
|
1372
|
+
index2 += 1;
|
|
1373
|
+
}
|
|
1374
|
+
continue;
|
|
1375
|
+
}
|
|
1376
|
+
out += char;
|
|
1377
|
+
index2 += 1;
|
|
1378
|
+
}
|
|
1379
|
+
return { code: out, state: { blockComment, template } };
|
|
1380
|
+
}
|
|
1381
|
+
function span(lines, from, to) {
|
|
1382
|
+
return {
|
|
1383
|
+
text: lines.slice(from, to + 1).join("\n"),
|
|
1384
|
+
startLine: from + 1,
|
|
1385
|
+
endLine: to + 1
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1388
|
+
function captureBraceBlock(lines, matchLine) {
|
|
1389
|
+
let depth = 0;
|
|
1390
|
+
let opened = false;
|
|
1391
|
+
let state = CLEAN_STATE;
|
|
1392
|
+
for (let index2 = matchLine; index2 < lines.length; index2++) {
|
|
1393
|
+
const stripped = stripLine(lines[index2] ?? "", state);
|
|
1394
|
+
state = stripped.state;
|
|
1395
|
+
for (const char of stripped.code) {
|
|
1396
|
+
if (char === "{") {
|
|
1397
|
+
depth += 1;
|
|
1398
|
+
opened = true;
|
|
1399
|
+
} else if (char === "}") {
|
|
1400
|
+
depth = Math.max(0, depth - 1);
|
|
1401
|
+
} else if (char === ";" && !opened) {
|
|
1402
|
+
return span(lines, matchLine, index2);
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
if (opened && depth === 0) return span(lines, matchLine, index2);
|
|
1406
|
+
}
|
|
1407
|
+
return null;
|
|
1408
|
+
}
|
|
1409
|
+
var PYTHON_HEADER = /^\s*(?:async\s+)?(?:def|class)\s+[A-Za-z_]\w*\s*[(:]/;
|
|
1410
|
+
function captureIndentedBlock(lines, matchLine) {
|
|
1411
|
+
const header2 = lines[matchLine] ?? "";
|
|
1412
|
+
const indent = header2.length - header2.trimStart().length;
|
|
1413
|
+
let headerEnd = -1;
|
|
1414
|
+
for (let index2 = matchLine; index2 < lines.length && index2 <= matchLine + 20; index2++) {
|
|
1415
|
+
const code = stripLine(lines[index2] ?? "", CLEAN_STATE).code.trimEnd();
|
|
1416
|
+
if (code.endsWith(":")) {
|
|
1417
|
+
headerEnd = index2;
|
|
1418
|
+
break;
|
|
1419
|
+
}
|
|
1420
|
+
if (code.includes(":")) return span(lines, matchLine, index2);
|
|
1421
|
+
}
|
|
1422
|
+
if (headerEnd === -1) return null;
|
|
1423
|
+
let end = headerEnd;
|
|
1424
|
+
for (let index2 = headerEnd + 1; index2 < lines.length; index2++) {
|
|
1425
|
+
const line = lines[index2] ?? "";
|
|
1426
|
+
if (line.trim() === "") continue;
|
|
1427
|
+
const lineIndent = line.length - line.trimStart().length;
|
|
1428
|
+
if (lineIndent <= indent) break;
|
|
1429
|
+
end = index2;
|
|
1430
|
+
}
|
|
1431
|
+
return end === headerEnd ? null : span(lines, matchLine, end);
|
|
1432
|
+
}
|
|
1433
|
+
var declarationTier = (name) => new RegExp(
|
|
1434
|
+
`(?:function|class|interface|type|enum|const|let|var|def)\\s+${name}\\b`
|
|
1435
|
+
);
|
|
1436
|
+
var assignmentTier = (name) => new RegExp(`\\b${name}\\s*[:=]`);
|
|
1437
|
+
var anchoredAssignmentTier = (name) => new RegExp(
|
|
1438
|
+
`^\\s*(?:export\\s+|readonly\\s+|pub\\s+|static\\s+|private\\s+|public\\s+|protected\\s+)*${name}\\s*[:=]`
|
|
1439
|
+
);
|
|
1440
|
+
var DEFINITION_TIERS = [declarationTier, anchoredAssignmentTier];
|
|
1441
|
+
var MENTION_TIERS = [
|
|
1442
|
+
(name) => new RegExp(`\\b${name}\\s*\\(`),
|
|
1443
|
+
(name) => new RegExp(`\\b${name}\\b`)
|
|
1444
|
+
];
|
|
1445
|
+
var TIERS = [declarationTier, assignmentTier, ...MENTION_TIERS];
|
|
1446
|
+
function resolveWith(tiers, source, symbol) {
|
|
1447
|
+
const segments = symbol.split(".");
|
|
1448
|
+
const name = segments[segments.length - 1];
|
|
1449
|
+
if (!name) return null;
|
|
1450
|
+
const parent = segments.length > 1 ? segments[segments.length - 2] : void 0;
|
|
1451
|
+
const escaped = escapeRegExp(name);
|
|
1452
|
+
const parentPattern = parent ? new RegExp(`\\b${escapeRegExp(parent)}\\b`) : null;
|
|
1453
|
+
const lines = source.split("\n");
|
|
1454
|
+
for (const tier of tiers) {
|
|
1455
|
+
const pattern = tier(escaped);
|
|
1456
|
+
let candidates = lines.map((line, index2) => ({ line, index: index2 })).filter((entry) => pattern.test(entry.line)).map((entry) => entry.index);
|
|
1457
|
+
if (!candidates.length) continue;
|
|
1458
|
+
if (parentPattern && candidates.length > 1) {
|
|
1459
|
+
const distances = candidates.map(
|
|
1460
|
+
(index2) => distanceToParent(lines, index2, parentPattern)
|
|
1461
|
+
);
|
|
1462
|
+
const nearest = Math.min(...distances);
|
|
1463
|
+
if (Number.isFinite(nearest)) {
|
|
1464
|
+
candidates = candidates.filter((_, at2) => distances[at2] === nearest);
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
if (candidates.length !== 1) return null;
|
|
1468
|
+
const matchLine = candidates[0];
|
|
1469
|
+
return PYTHON_HEADER.test(lines[matchLine] ?? "") ? captureIndentedBlock(lines, matchLine) : captureBraceBlock(lines, matchLine);
|
|
1470
|
+
}
|
|
1471
|
+
return null;
|
|
1472
|
+
}
|
|
1473
|
+
var regexResolver = {
|
|
1474
|
+
name: "regex",
|
|
1475
|
+
resolve(source, symbol) {
|
|
1476
|
+
return resolveWith(TIERS, source, symbol);
|
|
1477
|
+
},
|
|
1478
|
+
attempt(source, symbol, _file, options) {
|
|
1479
|
+
const tiers = options?.afterParsedMiss ? DEFINITION_TIERS : TIERS;
|
|
1480
|
+
const span2 = resolveWith(tiers, source, symbol);
|
|
1481
|
+
return span2 ? { kind: "resolved", span: span2 } : { kind: "unresolved", reason: "symbol-not-found" };
|
|
1482
|
+
}
|
|
1483
|
+
};
|
|
1484
|
+
function escapeRegExp(value) {
|
|
1485
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1486
|
+
}
|
|
1487
|
+
function distanceToParent(lines, index2, parent) {
|
|
1488
|
+
const floor = Math.max(0, index2 - PARENT_SCOPE_LINES);
|
|
1489
|
+
for (let at2 = index2; at2 >= floor; at2--) {
|
|
1490
|
+
if (parent.test(lines[at2] ?? "")) return index2 - at2;
|
|
1491
|
+
}
|
|
1492
|
+
return Number.POSITIVE_INFINITY;
|
|
1493
|
+
}
|
|
1494
|
+
function hashAnchorText(text) {
|
|
1495
|
+
return `sha256:${(0, import_node_crypto3.createHash)("sha256").update(text.replace(/\r\n/g, "\n")).digest("hex")}`;
|
|
1496
|
+
}
|
|
1497
|
+
function resolveAnchorSpan(source, anchor, resolvers = [regexResolver]) {
|
|
1498
|
+
const normalized = source.replace(/\r\n/g, "\n");
|
|
1499
|
+
if (anchor.span) return sliceSpan(normalized, anchor.span);
|
|
1500
|
+
if (!anchor.symbol) {
|
|
1501
|
+
const lines = normalized.split("\n");
|
|
1502
|
+
if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
|
|
1503
|
+
return {
|
|
1504
|
+
ok: true,
|
|
1505
|
+
span: {
|
|
1506
|
+
text: normalized,
|
|
1507
|
+
startLine: 1,
|
|
1508
|
+
endLine: Math.max(1, lines.length)
|
|
1509
|
+
}
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
let afterParsedMiss = false;
|
|
1513
|
+
for (const resolver of resolvers) {
|
|
1514
|
+
const attempt = resolver.attempt ? resolver.attempt(normalized, anchor.symbol, anchor.file, {
|
|
1515
|
+
afterParsedMiss
|
|
1516
|
+
}) : fromResolve(resolver, normalized, anchor.symbol, anchor.file);
|
|
1517
|
+
if (attempt.kind === "abstain") continue;
|
|
1518
|
+
if (attempt.kind === "unresolved") {
|
|
1519
|
+
if (attempt.reason === "symbol-not-found") {
|
|
1520
|
+
if (resolver.attempt) afterParsedMiss = true;
|
|
1521
|
+
continue;
|
|
1522
|
+
}
|
|
1523
|
+
return { ok: false, reason: attempt.reason };
|
|
1524
|
+
}
|
|
1525
|
+
const tokens2 = resolver.normalize?.(attempt.span.text, anchor.file);
|
|
1526
|
+
return {
|
|
1527
|
+
ok: true,
|
|
1528
|
+
span: attempt.span,
|
|
1529
|
+
...isResolverName(resolver.name) ? { resolver: resolver.name } : {},
|
|
1530
|
+
...tokens2 ? { normalized: tokens2 } : {}
|
|
1531
|
+
};
|
|
1532
|
+
}
|
|
1533
|
+
return { ok: false, reason: "symbol-not-found" };
|
|
1534
|
+
}
|
|
1535
|
+
function sliceSpan(source, range) {
|
|
1536
|
+
const lines = source.split("\n");
|
|
1537
|
+
if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
|
|
1538
|
+
if (range.end > lines.length) {
|
|
1539
|
+
return { ok: false, reason: "span-out-of-range" };
|
|
1792
1540
|
}
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1541
|
+
return {
|
|
1542
|
+
ok: true,
|
|
1543
|
+
span: {
|
|
1544
|
+
text: lines.slice(range.start - 1, range.end).join("\n"),
|
|
1545
|
+
startLine: range.start,
|
|
1546
|
+
endLine: range.end
|
|
1547
|
+
},
|
|
1548
|
+
resolver: "span"
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1551
|
+
function fromResolve(resolver, source, symbol, file) {
|
|
1552
|
+
const span2 = resolver.resolve(source, symbol, file);
|
|
1553
|
+
return span2 ? { kind: "resolved", span: span2 } : { kind: "unresolved", reason: "symbol-not-found" };
|
|
1554
|
+
}
|
|
1555
|
+
function isResolverName(name) {
|
|
1556
|
+
return name === "tree-sitter" || name === "regex" || name === "span";
|
|
1557
|
+
}
|
|
1558
|
+
async function prepareResolvers(resolvers, files) {
|
|
1559
|
+
for (const resolver of resolvers) await resolver.prepare?.(files);
|
|
1560
|
+
}
|
|
1561
|
+
function defaultAnchorResolvers(grammars = {}) {
|
|
1562
|
+
return [new TreeSitterResolver(grammars), regexResolver];
|
|
1563
|
+
}
|
|
1564
|
+
function resolverChanged(source, anchor, produced) {
|
|
1565
|
+
const previous = anchor.resolver ?? "regex";
|
|
1566
|
+
if (!produced || !anchor.symbol || previous === produced) return false;
|
|
1567
|
+
if (previous !== "regex") return false;
|
|
1568
|
+
const before = regexResolver.resolve(
|
|
1569
|
+
source.replace(/\r\n/g, "\n"),
|
|
1570
|
+
anchor.symbol
|
|
1571
|
+
);
|
|
1572
|
+
return before !== null && hashAnchorText(before.text) === anchor.hash;
|
|
1573
|
+
}
|
|
1574
|
+
function anchorHashOf(anchor, outcome) {
|
|
1575
|
+
if (outcome.resolver === "span") {
|
|
1576
|
+
return { hash: hashAnchorText(outcome.span.text), kind: "raw" };
|
|
1577
|
+
}
|
|
1578
|
+
const stored = anchor.hash ? anchor.hash_kind ?? "raw" : void 0;
|
|
1579
|
+
const wanted = stored ?? (outcome.normalized ? "ast" : "raw");
|
|
1580
|
+
return wanted === "ast" && outcome.normalized ? { hash: hashAnchorText(outcome.normalized), kind: "ast" } : { hash: hashAnchorText(outcome.span.text), kind: "raw" };
|
|
1797
1581
|
}
|
|
1798
1582
|
|
|
1799
|
-
// src/anchor-resolver/
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
index2 += 1;
|
|
1825
|
-
continue;
|
|
1826
|
-
}
|
|
1827
|
-
if (char === "/" && next === "*") {
|
|
1828
|
-
blockComment = true;
|
|
1829
|
-
index2 += 2;
|
|
1830
|
-
continue;
|
|
1583
|
+
// src/anchor-resolver/drift.ts
|
|
1584
|
+
async function detectAnchorDrift(records, options = {}) {
|
|
1585
|
+
const repoRoot = options.repoRoot ?? process.cwd();
|
|
1586
|
+
const resolvers = options.resolvers ?? (options.resolver ? [options.resolver] : defaultAnchorResolvers({
|
|
1587
|
+
offline: options.remote?.offline === true
|
|
1588
|
+
}));
|
|
1589
|
+
const origin = new LazyOrigin(repoRoot);
|
|
1590
|
+
const planned = /* @__PURE__ */ new Map();
|
|
1591
|
+
let declaresRepo = false;
|
|
1592
|
+
for (const record of records) {
|
|
1593
|
+
const anchors = (record.frontmatter.strauss_anchors ?? []).filter(
|
|
1594
|
+
(anchor) => anchor.hash
|
|
1595
|
+
);
|
|
1596
|
+
if (!anchors.length) continue;
|
|
1597
|
+
if (anchors.some((anchor) => anchor.repo)) declaresRepo = true;
|
|
1598
|
+
planned.set(
|
|
1599
|
+
record.conceptId,
|
|
1600
|
+
anchors.map((anchor) => ({ anchor, foreign: false }))
|
|
1601
|
+
);
|
|
1602
|
+
}
|
|
1603
|
+
if (declaresRepo) {
|
|
1604
|
+
await origin.prime();
|
|
1605
|
+
for (const entries of planned.values()) {
|
|
1606
|
+
for (const entry of entries)
|
|
1607
|
+
entry.foreign = origin.isForeign(entry.anchor);
|
|
1831
1608
|
}
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1609
|
+
}
|
|
1610
|
+
const files = [];
|
|
1611
|
+
const committedWants = [];
|
|
1612
|
+
const wants = [];
|
|
1613
|
+
for (const entries of planned.values()) {
|
|
1614
|
+
for (const { anchor, foreign } of entries) {
|
|
1615
|
+
if (foreign) wants.push(...remoteWants(anchor));
|
|
1616
|
+
else if (anchor.side === "old") committedWants.push(anchor);
|
|
1617
|
+
else files.push(anchor.file);
|
|
1837
1618
|
}
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1619
|
+
}
|
|
1620
|
+
const [reads, committed, remote] = await Promise.all([
|
|
1621
|
+
readAnchorFiles(
|
|
1622
|
+
files,
|
|
1623
|
+
options.reader ?? anchorFileReader(repoRoot),
|
|
1624
|
+
options.concurrency ?? DEFAULT_IO_CONCURRENCY
|
|
1625
|
+
),
|
|
1626
|
+
readCommitted(repoRoot, committedWants, options),
|
|
1627
|
+
(options.readRemote ?? readRemoteAnchors)(wants, options.remote ?? {})
|
|
1628
|
+
]);
|
|
1629
|
+
await prepareResolvers(resolvers, [
|
|
1630
|
+
...files,
|
|
1631
|
+
...committedWants.map((anchor) => anchor.file),
|
|
1632
|
+
...wants.map((want) => want.file)
|
|
1633
|
+
]);
|
|
1634
|
+
const drift = /* @__PURE__ */ new Map();
|
|
1635
|
+
for (const record of records) {
|
|
1636
|
+
const entries = [];
|
|
1637
|
+
for (const { anchor, foreign } of planned.get(record.conceptId) ?? []) {
|
|
1638
|
+
if (foreign) {
|
|
1639
|
+
entries.push(remoteEntry(anchor, remote, resolvers));
|
|
1640
|
+
continue;
|
|
1851
1641
|
}
|
|
1852
|
-
|
|
1642
|
+
const read = anchor.side === "old" ? committed.get(atRefKey(anchor)) : reads.get(anchor.file);
|
|
1643
|
+
entries.push(localEntry(anchor, read, resolvers));
|
|
1853
1644
|
}
|
|
1854
|
-
|
|
1855
|
-
index2 += 1;
|
|
1645
|
+
if (entries.length) drift.set(record.conceptId, entries);
|
|
1856
1646
|
}
|
|
1857
|
-
return
|
|
1647
|
+
return drift;
|
|
1858
1648
|
}
|
|
1859
|
-
function
|
|
1649
|
+
function atRefKey(anchor) {
|
|
1650
|
+
return `${anchor.ref ?? ""}\0${anchor.file}`;
|
|
1651
|
+
}
|
|
1652
|
+
async function readCommitted(repoRoot, anchors, options = {}) {
|
|
1653
|
+
if (!anchors.length) return /* @__PURE__ */ new Map();
|
|
1654
|
+
const read = options.readAtRef ?? readFileAtRef;
|
|
1655
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
1656
|
+
for (const anchor of anchors) byKey.set(atRefKey(anchor), anchor);
|
|
1657
|
+
const keys = [...byKey.keys()];
|
|
1658
|
+
const results = await mapLimit(
|
|
1659
|
+
keys,
|
|
1660
|
+
options.concurrency ?? DEFAULT_IO_CONCURRENCY,
|
|
1661
|
+
(key2) => read(repoRoot, byKey.get(key2))
|
|
1662
|
+
);
|
|
1663
|
+
return new Map(keys.map((key2, at2) => [key2, results[at2]]));
|
|
1664
|
+
}
|
|
1665
|
+
function remoteWants(anchor) {
|
|
1666
|
+
const repo = anchor.repo;
|
|
1667
|
+
const wants = [{ repo, file: anchor.file }];
|
|
1668
|
+
if (anchor.ref) wants.unshift({ repo, ref: anchor.ref, file: anchor.file });
|
|
1669
|
+
return wants;
|
|
1670
|
+
}
|
|
1671
|
+
function base(anchor) {
|
|
1672
|
+
return {
|
|
1673
|
+
file: anchor.file,
|
|
1674
|
+
...anchor.symbol ? { symbol: anchor.symbol } : {},
|
|
1675
|
+
...anchor.side === "old" ? { side: "old" } : {},
|
|
1676
|
+
storedHash: anchor.hash
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
function unresolved(anchor, reason, repo) {
|
|
1680
|
+
return {
|
|
1681
|
+
...base(anchor),
|
|
1682
|
+
state: "unresolved",
|
|
1683
|
+
diffSize: null,
|
|
1684
|
+
...reason ? { reason } : {},
|
|
1685
|
+
...repo ? { repo } : {},
|
|
1686
|
+
...classOf(reason)
|
|
1687
|
+
};
|
|
1688
|
+
}
|
|
1689
|
+
var GONE_REASONS = /* @__PURE__ */ new Set([
|
|
1690
|
+
"file-missing",
|
|
1691
|
+
"symbol-not-found",
|
|
1692
|
+
"span-out-of-range",
|
|
1693
|
+
"ref-unreadable"
|
|
1694
|
+
]);
|
|
1695
|
+
function provisionalDriftClass(entry) {
|
|
1696
|
+
if (entry.state === "unresolved") {
|
|
1697
|
+
return GONE_REASONS.has(entry.reason) ? "gone" : void 0;
|
|
1698
|
+
}
|
|
1699
|
+
return entry.state === "drifted" ? "changed" : void 0;
|
|
1700
|
+
}
|
|
1701
|
+
function classOf(reason) {
|
|
1702
|
+
const settled = provisionalDriftClass({ state: "unresolved", reason });
|
|
1703
|
+
return settled ? { class: settled } : {};
|
|
1704
|
+
}
|
|
1705
|
+
function hashIn(source, anchor, resolvers) {
|
|
1706
|
+
const outcome = resolveAnchorSpan(source, anchor, resolvers);
|
|
1707
|
+
if (!outcome.ok) return { ok: false, reason: outcome.reason };
|
|
1708
|
+
const { hash, kind } = anchorHashOf(anchor, outcome);
|
|
1709
|
+
return {
|
|
1710
|
+
ok: true,
|
|
1711
|
+
current: {
|
|
1712
|
+
hash,
|
|
1713
|
+
kind,
|
|
1714
|
+
lines: outcome.span.endLine - outcome.span.startLine + 1,
|
|
1715
|
+
...outcome.resolver ? { resolver: outcome.resolver } : {}
|
|
1716
|
+
}
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1719
|
+
function resolverExtras(source, anchor, current) {
|
|
1720
|
+
return {
|
|
1721
|
+
...current.resolver ? { resolver: current.resolver } : {},
|
|
1722
|
+
...current.hash !== anchor.hash && resolverChanged(source, anchor, current.resolver) ? { reason: "resolver-changed" } : {}
|
|
1723
|
+
};
|
|
1724
|
+
}
|
|
1725
|
+
function compared(anchor, current, extra = {}) {
|
|
1726
|
+
const matched = current.hash === anchor.hash;
|
|
1860
1727
|
return {
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1728
|
+
...base(anchor),
|
|
1729
|
+
state: matched ? "match" : "drifted",
|
|
1730
|
+
currentHash: current.hash,
|
|
1731
|
+
hashKind: current.kind,
|
|
1732
|
+
diffSize: anchor.lines === void 0 ? null : Math.abs(current.lines - anchor.lines),
|
|
1733
|
+
...matched ? {} : { class: "changed" },
|
|
1734
|
+
...extra
|
|
1864
1735
|
};
|
|
1865
1736
|
}
|
|
1866
|
-
function
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
depth += 1;
|
|
1876
|
-
opened = true;
|
|
1877
|
-
} else if (char === "}") {
|
|
1878
|
-
depth = Math.max(0, depth - 1);
|
|
1879
|
-
} else if (char === ";" && !opened) {
|
|
1880
|
-
return span(lines, matchLine, index2);
|
|
1881
|
-
}
|
|
1882
|
-
}
|
|
1883
|
-
if (opened && depth === 0) return span(lines, matchLine, index2);
|
|
1884
|
-
}
|
|
1885
|
-
return null;
|
|
1737
|
+
function localEntry(anchor, read, resolvers) {
|
|
1738
|
+
if (!read.ok) return unresolved(anchor, read.reason);
|
|
1739
|
+
const found = hashIn(read.source, anchor, resolvers);
|
|
1740
|
+
if (!found.ok) return unresolved(anchor, found.reason);
|
|
1741
|
+
return compared(
|
|
1742
|
+
anchor,
|
|
1743
|
+
found.current,
|
|
1744
|
+
resolverExtras(read.source, anchor, found.current)
|
|
1745
|
+
);
|
|
1886
1746
|
}
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
const
|
|
1890
|
-
const
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1747
|
+
function remoteEntry(anchor, remote, resolvers) {
|
|
1748
|
+
const repo = anchor.repo;
|
|
1749
|
+
const key2 = normalizeRepoUrl(repo);
|
|
1750
|
+
const atDefault = remote.get(wantKey(key2, void 0, anchor.file));
|
|
1751
|
+
const primary = anchor.ref ? remote.get(wantKey(key2, anchor.ref, anchor.file)) : atDefault;
|
|
1752
|
+
if (!primary) return unresolved(anchor, "remote-unreachable", repo);
|
|
1753
|
+
if (!primary.ok) return unresolved(anchor, primary.reason, repo);
|
|
1754
|
+
const found = hashIn(primary.source, anchor, resolvers);
|
|
1755
|
+
if (!found.ok) return unresolved(anchor, found.reason, repo);
|
|
1756
|
+
const current = found.current;
|
|
1757
|
+
const extras = resolverExtras(primary.source, anchor, current);
|
|
1758
|
+
if (!anchor.ref) return compared(anchor, current, { repo, ...extras });
|
|
1759
|
+
if (current.hash !== anchor.hash) {
|
|
1760
|
+
return compared(anchor, current, {
|
|
1761
|
+
repo,
|
|
1762
|
+
...extras,
|
|
1763
|
+
remoteState: "drifted-from-ref"
|
|
1764
|
+
});
|
|
1899
1765
|
}
|
|
1900
|
-
if (
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
if (lineIndent <= indent) break;
|
|
1907
|
-
end = index2;
|
|
1766
|
+
if (anchor.side === "old") {
|
|
1767
|
+
return compared(anchor, current, {
|
|
1768
|
+
repo,
|
|
1769
|
+
...extras,
|
|
1770
|
+
remoteState: "matches-ref"
|
|
1771
|
+
});
|
|
1908
1772
|
}
|
|
1909
|
-
|
|
1773
|
+
const head = atDefault?.ok ? hashIn(atDefault.source, anchor, resolvers) : null;
|
|
1774
|
+
return head?.ok && head.current.hash !== anchor.hash ? {
|
|
1775
|
+
...compared(anchor, head.current, {
|
|
1776
|
+
repo,
|
|
1777
|
+
...head.current.resolver ? { resolver: head.current.resolver } : {}
|
|
1778
|
+
}),
|
|
1779
|
+
state: "drifted",
|
|
1780
|
+
remoteState: "drifted-on-default"
|
|
1781
|
+
} : compared(anchor, current, {
|
|
1782
|
+
repo,
|
|
1783
|
+
...extras,
|
|
1784
|
+
remoteState: "matches-ref"
|
|
1785
|
+
});
|
|
1910
1786
|
}
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
);
|
|
1914
|
-
var
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
)
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1787
|
+
|
|
1788
|
+
// src/kb-record.schema.ts
|
|
1789
|
+
var import_zod2 = require("zod");
|
|
1790
|
+
var kbSourceSchema = import_zod2.z.object({
|
|
1791
|
+
id: import_zod2.z.string().min(1),
|
|
1792
|
+
resource: import_zod2.z.string().min(1),
|
|
1793
|
+
title: import_zod2.z.string().min(1).optional(),
|
|
1794
|
+
author: import_zod2.z.string().min(1).optional(),
|
|
1795
|
+
last_modified: import_zod2.z.string().min(1).optional()
|
|
1796
|
+
}).passthrough();
|
|
1797
|
+
var kbActorStampSchema = import_zod2.z.object({
|
|
1798
|
+
by: import_zod2.z.string().min(1),
|
|
1799
|
+
at: import_zod2.z.string().min(1)
|
|
1800
|
+
}).passthrough();
|
|
1801
|
+
var kbVerifiedEventSchema = kbActorStampSchema.extend({
|
|
1802
|
+
note: import_zod2.z.string().refine((s) => s.trim().length > 0, {
|
|
1803
|
+
message: "note must say what the check found"
|
|
1804
|
+
})
|
|
1805
|
+
});
|
|
1806
|
+
var kbAnchorSpanSchema = import_zod2.z.object({
|
|
1807
|
+
start: import_zod2.z.number().int().positive(),
|
|
1808
|
+
end: import_zod2.z.number().int().positive()
|
|
1809
|
+
}).strict();
|
|
1810
|
+
var kbAnchorSchema = import_zod2.z.object({
|
|
1811
|
+
file: import_zod2.z.string().min(1),
|
|
1812
|
+
symbol: import_zod2.z.string().min(1).optional(),
|
|
1813
|
+
/**
|
|
1814
|
+
* The lines the concept names, when no symbol covers them — deleted code,
|
|
1815
|
+
* YAML, SQL, Markdown. Alternative to `symbol`, never a refinement of it.
|
|
1816
|
+
*/
|
|
1817
|
+
span: kbAnchorSpanSchema.optional(),
|
|
1818
|
+
/**
|
|
1819
|
+
* Which side of the change the anchor describes. `old` is code as it was
|
|
1820
|
+
* committed at `ref`, which is the only way to anchor something deleted;
|
|
1821
|
+
* absent means the working tree.
|
|
1822
|
+
*/
|
|
1823
|
+
side: import_zod2.z.enum(["old", "new"]).optional(),
|
|
1824
|
+
/**
|
|
1825
|
+
* Which repository the file lives in — a remote URL
|
|
1826
|
+
* (`https://github.com/org/name`) or a short name. Absent means the base's
|
|
1827
|
+
* own repository, which is what nearly every anchor means.
|
|
1828
|
+
*
|
|
1829
|
+
* Unvalidated beyond not-blank: one repository has many spellings, matched
|
|
1830
|
+
* after normalisation. Only a full URL can be fetched from, so `validate`
|
|
1831
|
+
* warns on a short one; see ARCHITECTURE.
|
|
1832
|
+
*/
|
|
1833
|
+
repo: import_zod2.z.string().trim().min(1).optional(),
|
|
1834
|
+
/**
|
|
1835
|
+
* The git rev the evidence was taken at. Prefer a commit SHA: a branch
|
|
1836
|
+
* name is a moving pointer, so an anchor pinned to one says the evidence
|
|
1837
|
+
* came from wherever that branch happens to be now, which is not a
|
|
1838
|
+
* baseline. A foreign anchor is checked at this rev, and compared against
|
|
1839
|
+
* the remote's default branch on top of it.
|
|
1840
|
+
*/
|
|
1841
|
+
ref: import_zod2.z.string().trim().min(1).optional(),
|
|
1842
|
+
hash: import_zod2.z.string().regex(/^sha256:[0-9a-f]{64}$/, {
|
|
1843
|
+
message: "hash must be sha256:<64 hex chars>"
|
|
1844
|
+
}).optional(),
|
|
1845
|
+
/**
|
|
1846
|
+
* What `hash` was taken over: the span's raw text, or the normalised token
|
|
1847
|
+
* stream a parser sees (`ast`). Absent means `raw`, which is what every
|
|
1848
|
+
* anchor stamped before this field carries, so old hashes keep comparing
|
|
1849
|
+
* the way they were written. An `ast` hash is blind to whitespace and
|
|
1850
|
+
* comments, so reformatting the anchored code is not drift.
|
|
1851
|
+
*/
|
|
1852
|
+
hash_kind: import_zod2.z.enum(["raw", "ast"]).optional(),
|
|
1853
|
+
/** ISO 8601 timestamp of the last successful resolution. */
|
|
1854
|
+
resolved_at: import_zod2.z.string().min(1).optional(),
|
|
1855
|
+
/** Line count of the text the hash was taken over. */
|
|
1856
|
+
lines: import_zod2.z.number().int().positive().optional(),
|
|
1857
|
+
/**
|
|
1858
|
+
* Which resolver produced the hashed span. Absent means an anchor stamped
|
|
1859
|
+
* before resolvers were named, which is read as `regex` — the only one
|
|
1860
|
+
* there was. A hash from a different resolver is drift, not a match.
|
|
1861
|
+
*/
|
|
1862
|
+
resolver: import_zod2.z.enum(["tree-sitter", "regex", "span"]).optional()
|
|
1863
|
+
}).strict();
|
|
1864
|
+
var kbAnchorWriteSchema = kbAnchorSchema.superRefine((anchor, ctx) => {
|
|
1865
|
+
if (anchor.span && anchor.symbol) {
|
|
1866
|
+
ctx.addIssue({
|
|
1867
|
+
code: import_zod2.z.ZodIssueCode.custom,
|
|
1868
|
+
path: ["span"],
|
|
1869
|
+
message: "an anchor names a symbol or a span, not both"
|
|
1870
|
+
});
|
|
1948
1871
|
}
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
}
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1872
|
+
if (anchor.span && anchor.span.end < anchor.span.start) {
|
|
1873
|
+
ctx.addIssue({
|
|
1874
|
+
code: import_zod2.z.ZodIssueCode.custom,
|
|
1875
|
+
path: ["span", "end"],
|
|
1876
|
+
message: "span end must not precede start"
|
|
1877
|
+
});
|
|
1878
|
+
}
|
|
1879
|
+
if (anchor.span && anchor.hash_kind === "ast") {
|
|
1880
|
+
ctx.addIssue({
|
|
1881
|
+
code: import_zod2.z.ZodIssueCode.custom,
|
|
1882
|
+
path: ["hash_kind"],
|
|
1883
|
+
message: "a span is hashed raw, never ast"
|
|
1884
|
+
});
|
|
1885
|
+
}
|
|
1886
|
+
if (anchor.side === "old" && !anchor.ref) {
|
|
1887
|
+
ctx.addIssue({
|
|
1888
|
+
code: import_zod2.z.ZodIssueCode.custom,
|
|
1889
|
+
path: ["ref"],
|
|
1890
|
+
message: 'side: "old" needs a ref \u2014 committed code has no other address'
|
|
1891
|
+
});
|
|
1892
|
+
}
|
|
1893
|
+
});
|
|
1894
|
+
var kbAnchorLocatorSchema = kbAnchorSchema.pick({
|
|
1895
|
+
file: true,
|
|
1896
|
+
symbol: true,
|
|
1897
|
+
span: true,
|
|
1898
|
+
side: true,
|
|
1899
|
+
repo: true,
|
|
1900
|
+
ref: true
|
|
1901
|
+
});
|
|
1902
|
+
var kbLinkSchema = import_zod2.z.object({
|
|
1903
|
+
target: import_zod2.z.string().min(1),
|
|
1904
|
+
rel: import_zod2.z.string().min(1)
|
|
1905
|
+
}).passthrough();
|
|
1906
|
+
var KB_RECORD_TYPES = [
|
|
1907
|
+
"fact",
|
|
1908
|
+
"requirement",
|
|
1909
|
+
"constraint",
|
|
1910
|
+
"decision",
|
|
1911
|
+
"assumption",
|
|
1912
|
+
"open-question",
|
|
1913
|
+
"risk",
|
|
1914
|
+
"contract",
|
|
1915
|
+
"flow",
|
|
1916
|
+
"affected-system",
|
|
1917
|
+
"test-obligation",
|
|
1918
|
+
"source-note"
|
|
1919
|
+
];
|
|
1920
|
+
var KB_SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
1921
|
+
var KB_CONCEPT_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*\.[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
1922
|
+
var kbConceptIdSchema = import_zod2.z.string().regex(KB_CONCEPT_ID_PATTERN, {
|
|
1923
|
+
message: "concept id must be <type>.<slug>, both kebab-case"
|
|
1924
|
+
});
|
|
1925
|
+
var KB_RECORD_STATUSES = [
|
|
1926
|
+
"draft",
|
|
1927
|
+
"proposed",
|
|
1928
|
+
"accepted",
|
|
1929
|
+
"open",
|
|
1930
|
+
"resolved",
|
|
1931
|
+
"rejected",
|
|
1932
|
+
"superseded"
|
|
1933
|
+
];
|
|
1934
|
+
var KB_MATERIALITIES = [
|
|
1935
|
+
"blocking",
|
|
1936
|
+
"important",
|
|
1937
|
+
"non-blocking"
|
|
1938
|
+
];
|
|
1939
|
+
var KB_CONFIDENCES = ["low", "medium", "high"];
|
|
1940
|
+
var kbRecordFrontmatterSchema = import_zod2.z.object({
|
|
1941
|
+
// OKF: the only always-required key. A concept carrying just `type` is
|
|
1942
|
+
// fully conformant, so everything below stays optional.
|
|
1943
|
+
type: import_zod2.z.string().min(1),
|
|
1944
|
+
// OKF recommended.
|
|
1945
|
+
title: import_zod2.z.string().min(1).optional(),
|
|
1946
|
+
description: import_zod2.z.string().min(1).optional(),
|
|
1947
|
+
resource: import_zod2.z.string().min(1).optional(),
|
|
1948
|
+
tags: import_zod2.z.array(import_zod2.z.string()).optional(),
|
|
1949
|
+
// OKF optional: provenance and freshness.
|
|
1950
|
+
sources: import_zod2.z.array(kbSourceSchema).optional(),
|
|
1951
|
+
generated: kbActorStampSchema.optional(),
|
|
1952
|
+
verified: import_zod2.z.array(kbActorStampSchema).optional(),
|
|
1953
|
+
stale_after: import_zod2.z.string().min(1).optional(),
|
|
1954
|
+
// strauss extensions — see the module comment.
|
|
1955
|
+
strauss_anchors: import_zod2.z.array(kbAnchorSchema).optional(),
|
|
1956
|
+
strauss_verify: import_zod2.z.array(import_zod2.z.string().min(1)).optional(),
|
|
1957
|
+
// Typed causal edges, source → target, living on the source. `A depends_on
|
|
1958
|
+
// B` means A needs B, so `kb_impact` walks these inbound: what breaks if B
|
|
1959
|
+
// changes is whatever declared a dependence on it.
|
|
1960
|
+
strauss_links: import_zod2.z.array(kbLinkSchema).optional(),
|
|
1961
|
+
// Total after parsing, tolerant before it. Our producers must supply a
|
|
1962
|
+
// status — an absent one would leave every reader inventing its own default
|
|
1963
|
+
// — but OKF calls a concept carrying only `type` fully conformant, so
|
|
1964
|
+
// rejecting a foreign record for the lack of one would put us outside the
|
|
1965
|
+
// spec. The default resolves it in the single place that can: here.
|
|
1966
|
+
strauss_status: import_zod2.z.enum(KB_RECORD_STATUSES).default("draft"),
|
|
1967
|
+
strauss_supersedes: import_zod2.z.array(import_zod2.z.string().min(1)).optional(),
|
|
1968
|
+
strauss_superseded_by: import_zod2.z.string().min(1).optional(),
|
|
1969
|
+
strauss_answered: kbActorStampSchema.optional(),
|
|
1970
|
+
strauss_materiality: import_zod2.z.enum(KB_MATERIALITIES).optional(),
|
|
1971
|
+
strauss_confidence: import_zod2.z.enum(KB_CONFIDENCES).optional(),
|
|
1972
|
+
strauss_owner: import_zod2.z.string().min(1).optional(),
|
|
1973
|
+
// "No source exists" as a field rather than a sentinel entry inside
|
|
1974
|
+
// `sources`. A sentinel in a reference list is a value doing work a field
|
|
1975
|
+
// should do; as a field, `sources` may be legitimately empty.
|
|
1976
|
+
strauss_assumption: import_zod2.z.boolean().optional()
|
|
1977
|
+
}).passthrough();
|
|
1978
|
+
|
|
1979
|
+
// src/errors.ts
|
|
1980
|
+
var BaseError = class extends Error {
|
|
1981
|
+
code;
|
|
1982
|
+
errorType;
|
|
1983
|
+
fault;
|
|
1984
|
+
retriable;
|
|
1985
|
+
reportToUser;
|
|
1986
|
+
details;
|
|
1987
|
+
constructor(props) {
|
|
1988
|
+
super(props.message);
|
|
1989
|
+
this.name = props.name ?? this.constructor.name;
|
|
1990
|
+
this.code = props.code ?? 500;
|
|
1991
|
+
this.errorType = props.errorType;
|
|
1992
|
+
this.fault = props.fault;
|
|
1993
|
+
this.retriable = props.retriable ?? true;
|
|
1994
|
+
this.reportToUser = props.reportToUser ?? false;
|
|
1995
|
+
this.details = props.details;
|
|
1960
1996
|
}
|
|
1961
1997
|
};
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
const
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
}
|
|
1970
|
-
return Number.POSITIVE_INFINITY;
|
|
1998
|
+
|
|
1999
|
+
// src/anchors/errors.ts
|
|
2000
|
+
function locatorText(locator) {
|
|
2001
|
+
const span2 = locator.span ? `:${locator.span.start}-${locator.span.end}` : "";
|
|
2002
|
+
const symbol = locator.symbol ? `:${cap(locator.symbol)}` : "";
|
|
2003
|
+
const repo = locator.repo ? `${cap(locator.repo)}@` : "";
|
|
2004
|
+
const ref = locator.ref ? `@${cap(locator.ref)}` : "";
|
|
2005
|
+
return `${repo}${cap(locator.file)}${symbol}${span2}${ref}`;
|
|
1971
2006
|
}
|
|
1972
|
-
|
|
1973
|
-
|
|
2007
|
+
var FIELD_CAP = 120;
|
|
2008
|
+
function cap(value) {
|
|
2009
|
+
return value.length > FIELD_CAP ? `${value.slice(0, FIELD_CAP - 1)}\u2026` : value;
|
|
1974
2010
|
}
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
}
|
|
1988
|
-
};
|
|
2011
|
+
var KbAnchorSetDuplicateError = class extends BaseError {
|
|
2012
|
+
constructor(locator) {
|
|
2013
|
+
super({
|
|
2014
|
+
message: `kb: ${locator} appears twice in this set \u2014 a record holds each pointer once`,
|
|
2015
|
+
errorType: "KbAnchorSetDuplicate" /* KbAnchorSetDuplicate */,
|
|
2016
|
+
code: 400,
|
|
2017
|
+
fault: "User" /* User */,
|
|
2018
|
+
retriable: false,
|
|
2019
|
+
reportToUser: true,
|
|
2020
|
+
details: { locator, action: "refused" }
|
|
2021
|
+
});
|
|
2022
|
+
this.locator = locator;
|
|
1989
2023
|
}
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2024
|
+
locator;
|
|
2025
|
+
};
|
|
2026
|
+
|
|
2027
|
+
// src/anchors/apply.ts
|
|
2028
|
+
var LOCATOR_FIELDS = [
|
|
2029
|
+
"file",
|
|
2030
|
+
"symbol",
|
|
2031
|
+
"span",
|
|
2032
|
+
"side",
|
|
2033
|
+
"repo",
|
|
2034
|
+
"ref"
|
|
2035
|
+
];
|
|
2036
|
+
function applyAnchorSet(current, incoming) {
|
|
2037
|
+
const anchors = incoming.map((anchor) => ({ ...anchor }));
|
|
2038
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2039
|
+
for (const anchor of anchors) {
|
|
2040
|
+
const key2 = locatorKey(anchor);
|
|
2041
|
+
if (seen.has(key2)) {
|
|
2042
|
+
throw new KbAnchorSetDuplicateError(locatorText(locatorOf(anchor)));
|
|
2002
2043
|
}
|
|
2003
|
-
|
|
2004
|
-
return {
|
|
2005
|
-
ok: true,
|
|
2006
|
-
span: attempt.span,
|
|
2007
|
-
...isResolverName(resolver.name) ? { resolver: resolver.name } : {},
|
|
2008
|
-
...tokens2 ? { normalized: tokens2 } : {}
|
|
2009
|
-
};
|
|
2044
|
+
seen.add(key2);
|
|
2010
2045
|
}
|
|
2011
|
-
return {
|
|
2046
|
+
return { anchors, changes: diff(current, anchors) };
|
|
2012
2047
|
}
|
|
2013
|
-
function
|
|
2014
|
-
const
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2048
|
+
function diff(current, next) {
|
|
2049
|
+
const before = new Map(
|
|
2050
|
+
current.filter((anchor) => anchor.hash).map((a) => [a.hash, a])
|
|
2051
|
+
);
|
|
2052
|
+
const beforeLocators = new Map(current.map((a) => [locatorKey(a), a]));
|
|
2053
|
+
const afterLocators = new Set(next.map((anchor) => locatorKey(anchor)));
|
|
2054
|
+
const moved = /* @__PURE__ */ new Set();
|
|
2055
|
+
const changes = [];
|
|
2056
|
+
for (const anchor of next) {
|
|
2057
|
+
const source = anchor.hash ? before.get(anchor.hash) : void 0;
|
|
2058
|
+
if (source) {
|
|
2059
|
+
if (locatorKey(source) === locatorKey(anchor)) continue;
|
|
2060
|
+
moved.add(locatorKey(source));
|
|
2061
|
+
changes.push({
|
|
2062
|
+
op: "move",
|
|
2063
|
+
from: locatorOf(source),
|
|
2064
|
+
to: locatorOf(anchor)
|
|
2065
|
+
});
|
|
2066
|
+
continue;
|
|
2067
|
+
}
|
|
2068
|
+
if (beforeLocators.has(locatorKey(anchor))) continue;
|
|
2069
|
+
changes.push({ op: "add", to: locatorOf(anchor) });
|
|
2018
2070
|
}
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
},
|
|
2026
|
-
resolver: "span"
|
|
2027
|
-
};
|
|
2028
|
-
}
|
|
2029
|
-
function fromResolve(resolver, source, symbol, file) {
|
|
2030
|
-
const span2 = resolver.resolve(source, symbol, file);
|
|
2031
|
-
return span2 ? { kind: "resolved", span: span2 } : { kind: "unresolved", reason: "symbol-not-found" };
|
|
2032
|
-
}
|
|
2033
|
-
function isResolverName(name) {
|
|
2034
|
-
return name === "tree-sitter" || name === "regex" || name === "span";
|
|
2071
|
+
for (const anchor of current) {
|
|
2072
|
+
const key2 = locatorKey(anchor);
|
|
2073
|
+
if (afterLocators.has(key2) || moved.has(key2)) continue;
|
|
2074
|
+
changes.push({ op: "drop", from: locatorOf(anchor) });
|
|
2075
|
+
}
|
|
2076
|
+
return changes;
|
|
2035
2077
|
}
|
|
2036
|
-
|
|
2037
|
-
|
|
2078
|
+
function locatorOf(anchor) {
|
|
2079
|
+
return kbAnchorLocatorSchema.parse(
|
|
2080
|
+
Object.fromEntries(
|
|
2081
|
+
LOCATOR_FIELDS.flatMap(
|
|
2082
|
+
(field) => anchor[field] === void 0 ? [] : [[field, anchor[field]]]
|
|
2083
|
+
)
|
|
2084
|
+
)
|
|
2085
|
+
);
|
|
2038
2086
|
}
|
|
2039
|
-
function
|
|
2040
|
-
return [
|
|
2087
|
+
function locatorKey(anchor) {
|
|
2088
|
+
return JSON.stringify([
|
|
2089
|
+
anchor.file,
|
|
2090
|
+
anchor.symbol ?? "",
|
|
2091
|
+
anchor.span ? `${anchor.span.start}-${anchor.span.end}` : "",
|
|
2092
|
+
anchor.side ?? "new",
|
|
2093
|
+
anchor.repo === void 0 ? "" : normalizeRepoUrl(anchor.repo),
|
|
2094
|
+
anchor.ref ?? ""
|
|
2095
|
+
]);
|
|
2041
2096
|
}
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2097
|
+
|
|
2098
|
+
// src/record-types.ts
|
|
2099
|
+
var RECORD_TYPES = {
|
|
2100
|
+
fact: {
|
|
2101
|
+
purpose: "Observed or sourced fact",
|
|
2102
|
+
sections: ["Claim", "Evidence", "Implication"],
|
|
2103
|
+
initialStatus: "accepted"
|
|
2104
|
+
},
|
|
2105
|
+
requirement: {
|
|
2106
|
+
purpose: "Required behavior or outcome",
|
|
2107
|
+
sections: ["Claim", "Evidence", "Implication"],
|
|
2108
|
+
initialStatus: "proposed"
|
|
2109
|
+
},
|
|
2110
|
+
constraint: {
|
|
2111
|
+
purpose: "Limitation, compatibility boundary, policy, or restriction",
|
|
2112
|
+
sections: ["Claim", "Evidence", "Implication"],
|
|
2113
|
+
initialStatus: "accepted"
|
|
2114
|
+
},
|
|
2115
|
+
decision: {
|
|
2116
|
+
purpose: "Chosen or proposed direction",
|
|
2117
|
+
sections: ["Decision", "Rationale", "Rejected", "Impact"],
|
|
2118
|
+
initialStatus: "accepted"
|
|
2119
|
+
},
|
|
2120
|
+
assumption: {
|
|
2121
|
+
purpose: "Unsourced or not-yet-confirmed working assumption",
|
|
2122
|
+
sections: ["Claim", "Why we think so", "What would falsify it"],
|
|
2123
|
+
initialStatus: "draft"
|
|
2124
|
+
},
|
|
2125
|
+
"open-question": {
|
|
2126
|
+
purpose: "Question needing resolution",
|
|
2127
|
+
sections: ["Question", "Why it matters", "Default assumption"],
|
|
2128
|
+
initialStatus: "open"
|
|
2129
|
+
},
|
|
2130
|
+
risk: {
|
|
2131
|
+
purpose: "Something that can go wrong",
|
|
2132
|
+
sections: ["Risk", "Why it matters", "Mitigation", "Verification"],
|
|
2133
|
+
initialStatus: "open"
|
|
2134
|
+
},
|
|
2135
|
+
contract: {
|
|
2136
|
+
purpose: "API, data, event, schema, or permission contract",
|
|
2137
|
+
sections: ["Contract", "Producer", "Consumer", "Compatibility"],
|
|
2138
|
+
initialStatus: "proposed"
|
|
2139
|
+
},
|
|
2140
|
+
flow: {
|
|
2141
|
+
purpose: "Sequence, lifecycle, or state behavior",
|
|
2142
|
+
sections: ["Flow", "Trigger", "Steps", "Failure modes"],
|
|
2143
|
+
initialStatus: "accepted"
|
|
2144
|
+
},
|
|
2145
|
+
"affected-system": {
|
|
2146
|
+
purpose: "Component, service, package, integration, or external system",
|
|
2147
|
+
sections: ["System", "How it is affected", "Blast radius"],
|
|
2148
|
+
initialStatus: "accepted"
|
|
2149
|
+
},
|
|
2150
|
+
"test-obligation": {
|
|
2151
|
+
purpose: "Behavior or contract that must be verified",
|
|
2152
|
+
sections: ["Obligation", "Why it matters", "How to verify"],
|
|
2153
|
+
initialStatus: "open"
|
|
2154
|
+
},
|
|
2155
|
+
"source-note": {
|
|
2156
|
+
purpose: "Extracted note from source material",
|
|
2157
|
+
sections: ["Note", "Where it came from"],
|
|
2158
|
+
initialStatus: "accepted"
|
|
2159
|
+
}
|
|
2160
|
+
};
|
|
2161
|
+
function isKbRecordType(value) {
|
|
2162
|
+
return Object.prototype.hasOwnProperty.call(RECORD_TYPES, value);
|
|
2051
2163
|
}
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2164
|
+
var KB_LINK_RELS = [
|
|
2165
|
+
"depends_on",
|
|
2166
|
+
"constrains",
|
|
2167
|
+
"informs",
|
|
2168
|
+
"blocks",
|
|
2169
|
+
"invalidates",
|
|
2170
|
+
"verified_by",
|
|
2171
|
+
"satisfies",
|
|
2172
|
+
"related_to"
|
|
2173
|
+
];
|
|
2174
|
+
var LINK_RELS = {
|
|
2175
|
+
depends_on: {
|
|
2176
|
+
purpose: "The source needs the target to hold; the source breaks if the target changes",
|
|
2177
|
+
phrase: "Depends on",
|
|
2178
|
+
dependant: "source"
|
|
2179
|
+
},
|
|
2180
|
+
constrains: {
|
|
2181
|
+
purpose: "The source bounds what the target may do; the target breaks if the constraint changes",
|
|
2182
|
+
phrase: "Constrains",
|
|
2183
|
+
dependant: "target"
|
|
2184
|
+
},
|
|
2185
|
+
informs: {
|
|
2186
|
+
purpose: "The source shaped the target without binding it; the target is what needs revisiting",
|
|
2187
|
+
phrase: "Informs",
|
|
2188
|
+
dependant: "target"
|
|
2189
|
+
},
|
|
2190
|
+
blocks: {
|
|
2191
|
+
purpose: "The target cannot proceed until the source is settled; the target is what waits",
|
|
2192
|
+
phrase: "Blocks",
|
|
2193
|
+
dependant: "target"
|
|
2194
|
+
},
|
|
2195
|
+
invalidates: {
|
|
2196
|
+
purpose: "The source makes the target no longer hold; the target is what stops holding",
|
|
2197
|
+
phrase: "Invalidates",
|
|
2198
|
+
dependant: "target"
|
|
2199
|
+
},
|
|
2200
|
+
verified_by: {
|
|
2201
|
+
purpose: "The target is the check that confirms the source; the source's confirmation moves with it",
|
|
2202
|
+
phrase: "Verified by",
|
|
2203
|
+
dependant: "source"
|
|
2204
|
+
},
|
|
2205
|
+
satisfies: {
|
|
2206
|
+
purpose: "The source discharges the target's requirement; the source must change if the requirement does",
|
|
2207
|
+
phrase: "Satisfies",
|
|
2208
|
+
dependant: "source"
|
|
2209
|
+
},
|
|
2210
|
+
related_to: {
|
|
2211
|
+
purpose: "A pointer worth following, with no claim of dependence",
|
|
2212
|
+
phrase: "Relates to",
|
|
2213
|
+
dependant: null
|
|
2055
2214
|
}
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2215
|
+
};
|
|
2216
|
+
var KB_CAUSAL_LINK_RELS = KB_LINK_RELS.filter(
|
|
2217
|
+
(rel) => LINK_RELS[rel].dependant !== null
|
|
2218
|
+
);
|
|
2219
|
+
function isKbLinkRel(value) {
|
|
2220
|
+
return Object.prototype.hasOwnProperty.call(LINK_RELS, value);
|
|
2059
2221
|
}
|
|
2060
2222
|
|
|
2061
|
-
// src/
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2223
|
+
// src/compose.ts
|
|
2224
|
+
var composeLinkSchema = import_zod3.z.object({
|
|
2225
|
+
target: kbConceptIdSchema,
|
|
2226
|
+
rel: import_zod3.z.enum(KB_LINK_RELS)
|
|
2227
|
+
}).strict();
|
|
2228
|
+
var composeInputSchema = import_zod3.z.object({
|
|
2229
|
+
slug: import_zod3.z.string().min(1),
|
|
2230
|
+
/** One line, in the reader's terms. Becomes OKF `title`. */
|
|
2231
|
+
title: import_zod3.z.string().min(1),
|
|
2232
|
+
/** The consequence — what breaks if this is wrong. Becomes `description`. */
|
|
2233
|
+
why: import_zod3.z.string().min(1),
|
|
2234
|
+
/** Keyed by section heading from the type's spec. Unknown keys rejected. */
|
|
2235
|
+
sections: import_zod3.z.record(import_zod3.z.string(), import_zod3.z.string().min(1)).optional(),
|
|
2236
|
+
anchors: import_zod3.z.array(kbAnchorWriteSchema).optional(),
|
|
2237
|
+
sources: import_zod3.z.array(kbSourceSchema).optional(),
|
|
2238
|
+
/** No source exists, as a claim rather than a sentinel in `sources`. */
|
|
2239
|
+
assumption: import_zod3.z.boolean().optional(),
|
|
2240
|
+
/**
|
|
2241
|
+
* OKF `stale_after`: the absolute date this record stops being trusted.
|
|
2242
|
+
* Anything the outside world can change — pricing, quotas, versions,
|
|
2243
|
+
* reception counts — should carry one.
|
|
2244
|
+
*/
|
|
2245
|
+
stale_after: import_zod3.z.string().regex(/^\d{4}-\d{2}-\d{2}$/, {
|
|
2246
|
+
message: "stale_after must be YYYY-MM-DD"
|
|
2247
|
+
}).refine((date) => !Number.isNaN(Date.parse(date)), {
|
|
2248
|
+
message: "stale_after must be a real date"
|
|
2249
|
+
}).optional(),
|
|
2250
|
+
verify: import_zod3.z.array(import_zod3.z.string().min(1)).optional(),
|
|
2251
|
+
tags: import_zod3.z.array(import_zod3.z.string().min(1)).optional(),
|
|
2252
|
+
/** Concept ids this record relates to; rendered as body links. */
|
|
2253
|
+
relatedConceptIds: import_zod3.z.array(kbConceptIdSchema).optional(),
|
|
2254
|
+
/**
|
|
2255
|
+
* Typed causal edges, source → target: `{ target: "fact.b", rel:
|
|
2256
|
+
* "depends_on" }` on record A says A needs B. Stored in frontmatter and
|
|
2257
|
+
* also rendered as one prose sentence each, so the meaning survives a
|
|
2258
|
+
* reader that knows only OKF. The vocabulary goes into the description from
|
|
2259
|
+
* the same table the walk uses, so `kb_schema` emits it.
|
|
2260
|
+
*/
|
|
2261
|
+
links: import_zod3.z.array(composeLinkSchema).max(64).optional().describe(
|
|
2262
|
+
`Typed causal edges, source \u2192 target \u2014 a link on this record says this record <rel> the target. ${KB_LINK_RELS.map(
|
|
2263
|
+
(rel) => `${rel}: ${LINK_RELS[rel].purpose}`
|
|
2264
|
+
).join("; ")}.`
|
|
2265
|
+
),
|
|
2266
|
+
/** Concept ids this record replaces. The store settles the backlinks. */
|
|
2267
|
+
supersedes: import_zod3.z.array(kbConceptIdSchema).max(32).optional(),
|
|
2268
|
+
materiality: import_zod3.z.enum(KB_MATERIALITIES).optional(),
|
|
2269
|
+
confidence: import_zod3.z.enum(KB_CONFIDENCES).optional(),
|
|
2270
|
+
owner: import_zod3.z.string().min(1).optional()
|
|
2271
|
+
}).strict();
|
|
2272
|
+
function composeRecord(type, input, writtenBy, writtenAt) {
|
|
2273
|
+
const parsed = composeInputSchema.parse(input);
|
|
2274
|
+
const spec = RECORD_TYPES[type];
|
|
2275
|
+
const sections = parsed.sections ?? {};
|
|
2276
|
+
const unknown = Object.keys(sections).filter(
|
|
2277
|
+
(heading) => !spec.sections.includes(heading)
|
|
2278
|
+
);
|
|
2279
|
+
if (unknown.length) {
|
|
2280
|
+
throw new Error(
|
|
2281
|
+
`kb: ${type} has no section ${unknown.join(", ")} \u2014 expected one of ${spec.sections.join(", ")}`
|
|
2073
2282
|
);
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2283
|
+
}
|
|
2284
|
+
const frontmatter = {
|
|
2285
|
+
title: parsed.title,
|
|
2286
|
+
description: parsed.why,
|
|
2287
|
+
generated: { by: writtenBy, at: writtenAt },
|
|
2288
|
+
// Empty rather than absent: a later verification pass appends here, and an
|
|
2289
|
+
// empty list says "not yet verified" where a missing key would only say
|
|
2290
|
+
// "this producer didn't think about it".
|
|
2291
|
+
verified: [],
|
|
2292
|
+
strauss_status: spec.initialStatus
|
|
2293
|
+
};
|
|
2294
|
+
if (parsed.stale_after) frontmatter.stale_after = parsed.stale_after;
|
|
2295
|
+
if (parsed.anchors?.length) {
|
|
2296
|
+
frontmatter.strauss_anchors = applyAnchorSet([], parsed.anchors).anchors;
|
|
2297
|
+
}
|
|
2298
|
+
if (parsed.verify?.length) frontmatter.strauss_verify = parsed.verify;
|
|
2299
|
+
if (parsed.tags?.length) frontmatter.tags = parsed.tags;
|
|
2300
|
+
if (parsed.sources?.length) frontmatter.sources = parsed.sources;
|
|
2301
|
+
if (parsed.assumption) frontmatter.strauss_assumption = true;
|
|
2302
|
+
if (parsed.materiality) frontmatter.strauss_materiality = parsed.materiality;
|
|
2303
|
+
if (parsed.confidence) frontmatter.strauss_confidence = parsed.confidence;
|
|
2304
|
+
if (parsed.owner) frontmatter.strauss_owner = parsed.owner;
|
|
2305
|
+
if (parsed.supersedes?.length)
|
|
2306
|
+
frontmatter.strauss_supersedes = parsed.supersedes;
|
|
2307
|
+
const selfLink = parsed.links?.find(
|
|
2308
|
+
(link2) => link2.target === `${type}.${parsed.slug}`
|
|
2309
|
+
);
|
|
2310
|
+
if (selfLink) {
|
|
2311
|
+
throw new Error(
|
|
2312
|
+
`kb: ${type}.${parsed.slug} cannot ${selfLink.rel} itself \u2014 a link must name another record`
|
|
2079
2313
|
);
|
|
2080
2314
|
}
|
|
2081
|
-
if (
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2315
|
+
if (parsed.links?.length) frontmatter.strauss_links = parsed.links;
|
|
2316
|
+
const blocks = [];
|
|
2317
|
+
for (const heading of spec.sections) {
|
|
2318
|
+
const text = sections[heading];
|
|
2319
|
+
if (text) blocks.push(`## ${heading}
|
|
2320
|
+
|
|
2321
|
+
${text}`);
|
|
2087
2322
|
}
|
|
2088
|
-
|
|
2089
|
-
const
|
|
2090
|
-
|
|
2091
|
-
for (const entries of planned.values()) {
|
|
2092
|
-
for (const { anchor, foreign } of entries) {
|
|
2093
|
-
if (foreign) wants.push(...remoteWants(anchor));
|
|
2094
|
-
else if (anchor.side === "old") committedWants.push(anchor);
|
|
2095
|
-
else files.push(anchor.file);
|
|
2096
|
-
}
|
|
2323
|
+
if (!blocks.length) blocks.push(parsed.why);
|
|
2324
|
+
for (const related of parsed.relatedConceptIds ?? []) {
|
|
2325
|
+
blocks.push(`Relates to [${related}](${related}.md).`);
|
|
2097
2326
|
}
|
|
2098
|
-
const
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
options.concurrency ?? DEFAULT_IO_CONCURRENCY
|
|
2103
|
-
),
|
|
2104
|
-
readCommitted(repoRoot, committedWants, options),
|
|
2105
|
-
(options.readRemote ?? readRemoteAnchors)(wants, options.remote ?? {})
|
|
2106
|
-
]);
|
|
2107
|
-
await prepareResolvers(resolvers, [
|
|
2108
|
-
...files,
|
|
2109
|
-
...committedWants.map((anchor) => anchor.file),
|
|
2110
|
-
...wants.map((want) => want.file)
|
|
2111
|
-
]);
|
|
2112
|
-
const drift = /* @__PURE__ */ new Map();
|
|
2113
|
-
for (const record of records) {
|
|
2114
|
-
const entries = [];
|
|
2115
|
-
for (const { anchor, foreign } of planned.get(record.conceptId) ?? []) {
|
|
2116
|
-
if (foreign) {
|
|
2117
|
-
entries.push(remoteEntry(anchor, remote, resolvers));
|
|
2118
|
-
continue;
|
|
2119
|
-
}
|
|
2120
|
-
const read = anchor.side === "old" ? committed.get(atRefKey(anchor)) : reads.get(anchor.file);
|
|
2121
|
-
entries.push(localEntry(anchor, read, resolvers));
|
|
2122
|
-
}
|
|
2123
|
-
if (entries.length) drift.set(record.conceptId, entries);
|
|
2327
|
+
for (const link2 of parsed.links ?? []) {
|
|
2328
|
+
blocks.push(
|
|
2329
|
+
`${LINK_RELS[link2.rel].phrase} [${link2.target}](${link2.target}.md).`
|
|
2330
|
+
);
|
|
2124
2331
|
}
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
}
|
|
2130
|
-
async function readCommitted(repoRoot, anchors, options = {}) {
|
|
2131
|
-
if (!anchors.length) return /* @__PURE__ */ new Map();
|
|
2132
|
-
const read = options.readAtRef ?? readFileAtRef;
|
|
2133
|
-
const byKey = /* @__PURE__ */ new Map();
|
|
2134
|
-
for (const anchor of anchors) byKey.set(atRefKey(anchor), anchor);
|
|
2135
|
-
const keys = [...byKey.keys()];
|
|
2136
|
-
const results = await mapLimit(
|
|
2137
|
-
keys,
|
|
2138
|
-
options.concurrency ?? DEFAULT_IO_CONCURRENCY,
|
|
2139
|
-
(key2) => read(repoRoot, byKey.get(key2))
|
|
2140
|
-
);
|
|
2141
|
-
return new Map(keys.map((key2, at2) => [key2, results[at2]]));
|
|
2142
|
-
}
|
|
2143
|
-
function remoteWants(anchor) {
|
|
2144
|
-
const repo = anchor.repo;
|
|
2145
|
-
const wants = [{ repo, file: anchor.file }];
|
|
2146
|
-
if (anchor.ref) wants.unshift({ repo, ref: anchor.ref, file: anchor.file });
|
|
2147
|
-
return wants;
|
|
2148
|
-
}
|
|
2149
|
-
function base(anchor) {
|
|
2150
|
-
return {
|
|
2151
|
-
file: anchor.file,
|
|
2152
|
-
...anchor.symbol ? { symbol: anchor.symbol } : {},
|
|
2153
|
-
...anchor.side === "old" ? { side: "old" } : {},
|
|
2154
|
-
storedHash: anchor.hash
|
|
2155
|
-
};
|
|
2156
|
-
}
|
|
2157
|
-
function unresolved(anchor, reason, repo) {
|
|
2158
|
-
return {
|
|
2159
|
-
...base(anchor),
|
|
2160
|
-
state: "unresolved",
|
|
2161
|
-
diffSize: null,
|
|
2162
|
-
...reason ? { reason } : {},
|
|
2163
|
-
...repo ? { repo } : {},
|
|
2164
|
-
...classOf(reason)
|
|
2165
|
-
};
|
|
2166
|
-
}
|
|
2167
|
-
var GONE_REASONS = /* @__PURE__ */ new Set([
|
|
2168
|
-
"file-missing",
|
|
2169
|
-
"symbol-not-found",
|
|
2170
|
-
"span-out-of-range",
|
|
2171
|
-
"ref-unreadable"
|
|
2172
|
-
]);
|
|
2173
|
-
function provisionalDriftClass(entry) {
|
|
2174
|
-
if (entry.state === "unresolved") {
|
|
2175
|
-
return GONE_REASONS.has(entry.reason) ? "gone" : void 0;
|
|
2332
|
+
if (parsed.sources?.length) {
|
|
2333
|
+
blocks.push(
|
|
2334
|
+
parsed.sources.map((source) => `[^${source.id}]: ${source.title ?? source.resource}`).join("\n")
|
|
2335
|
+
);
|
|
2176
2336
|
}
|
|
2177
|
-
return entry.state === "drifted" ? "changed" : void 0;
|
|
2178
|
-
}
|
|
2179
|
-
function classOf(reason) {
|
|
2180
|
-
const settled = provisionalDriftClass({ state: "unresolved", reason });
|
|
2181
|
-
return settled ? { class: settled } : {};
|
|
2182
|
-
}
|
|
2183
|
-
function hashIn(source, anchor, resolvers) {
|
|
2184
|
-
const outcome = resolveAnchorSpan(source, anchor, resolvers);
|
|
2185
|
-
if (!outcome.ok) return { ok: false, reason: outcome.reason };
|
|
2186
|
-
const { hash, kind } = anchorHashOf(anchor, outcome);
|
|
2187
|
-
return {
|
|
2188
|
-
ok: true,
|
|
2189
|
-
current: {
|
|
2190
|
-
hash,
|
|
2191
|
-
kind,
|
|
2192
|
-
lines: outcome.span.endLine - outcome.span.startLine + 1,
|
|
2193
|
-
...outcome.resolver ? { resolver: outcome.resolver } : {}
|
|
2194
|
-
}
|
|
2195
|
-
};
|
|
2196
|
-
}
|
|
2197
|
-
function resolverExtras(source, anchor, current) {
|
|
2198
2337
|
return {
|
|
2199
|
-
|
|
2200
|
-
|
|
2338
|
+
type,
|
|
2339
|
+
slug: parsed.slug,
|
|
2340
|
+
frontmatter,
|
|
2341
|
+
body: `${blocks.join("\n\n")}
|
|
2342
|
+
`
|
|
2201
2343
|
};
|
|
2202
2344
|
}
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2345
|
+
|
|
2346
|
+
// src/decision-record.ts
|
|
2347
|
+
var DECISION_TYPE = "decision";
|
|
2348
|
+
var NO_DECISION_SLUG = "none";
|
|
2349
|
+
var decisionInputSchema = composeInputSchema.omit({ sections: true }).extend({
|
|
2350
|
+
alternative: import_zod4.z.string().min(1).optional(),
|
|
2351
|
+
impact: import_zod4.z.string().min(1).optional()
|
|
2352
|
+
}).strict();
|
|
2353
|
+
function composeDecisionRecord(input, writtenBy, writtenAt) {
|
|
2354
|
+
const { alternative, impact: impact2, ...rest } = input;
|
|
2355
|
+
return composeRecord(
|
|
2356
|
+
DECISION_TYPE,
|
|
2357
|
+
{
|
|
2358
|
+
...rest,
|
|
2359
|
+
sections: {
|
|
2360
|
+
Decision: input.title,
|
|
2361
|
+
Rationale: input.why,
|
|
2362
|
+
...alternative ? { Rejected: alternative } : {},
|
|
2363
|
+
...impact2 ? { Impact: impact2 } : {}
|
|
2364
|
+
}
|
|
2365
|
+
},
|
|
2366
|
+
writtenBy,
|
|
2367
|
+
writtenAt
|
|
2368
|
+
);
|
|
2214
2369
|
}
|
|
2215
|
-
function
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2370
|
+
function composeNoDecisionRecord(reason, writtenBy, writtenAt) {
|
|
2371
|
+
return composeRecord(
|
|
2372
|
+
DECISION_TYPE,
|
|
2373
|
+
{
|
|
2374
|
+
slug: NO_DECISION_SLUG,
|
|
2375
|
+
title: "No decision to record",
|
|
2376
|
+
why: reason,
|
|
2377
|
+
sections: { Decision: reason }
|
|
2378
|
+
},
|
|
2379
|
+
writtenBy,
|
|
2380
|
+
writtenAt
|
|
2223
2381
|
);
|
|
2224
2382
|
}
|
|
2225
|
-
function
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
const found = hashIn(primary.source, anchor, resolvers);
|
|
2233
|
-
if (!found.ok) return unresolved(anchor, found.reason, repo);
|
|
2234
|
-
const current = found.current;
|
|
2235
|
-
const extras = resolverExtras(primary.source, anchor, current);
|
|
2236
|
-
if (!anchor.ref) return compared(anchor, current, { repo, ...extras });
|
|
2237
|
-
if (current.hash !== anchor.hash) {
|
|
2238
|
-
return compared(anchor, current, {
|
|
2239
|
-
repo,
|
|
2240
|
-
...extras,
|
|
2241
|
-
remoteState: "drifted-from-ref"
|
|
2242
|
-
});
|
|
2243
|
-
}
|
|
2244
|
-
if (anchor.side === "old") {
|
|
2245
|
-
return compared(anchor, current, {
|
|
2246
|
-
repo,
|
|
2247
|
-
...extras,
|
|
2248
|
-
remoteState: "matches-ref"
|
|
2249
|
-
});
|
|
2250
|
-
}
|
|
2251
|
-
const head = atDefault?.ok ? hashIn(atDefault.source, anchor, resolvers) : null;
|
|
2252
|
-
return head?.ok && head.current.hash !== anchor.hash ? {
|
|
2253
|
-
...compared(anchor, head.current, {
|
|
2254
|
-
repo,
|
|
2255
|
-
...head.current.resolver ? { resolver: head.current.resolver } : {}
|
|
2256
|
-
}),
|
|
2257
|
-
state: "drifted",
|
|
2258
|
-
remoteState: "drifted-on-default"
|
|
2259
|
-
} : compared(anchor, current, {
|
|
2260
|
-
repo,
|
|
2261
|
-
...extras,
|
|
2262
|
-
remoteState: "matches-ref"
|
|
2263
|
-
});
|
|
2383
|
+
function isNoDecisionRecord(record) {
|
|
2384
|
+
return record.conceptId === `${DECISION_TYPE}.${NO_DECISION_SLUG}`;
|
|
2385
|
+
}
|
|
2386
|
+
function selectDecisions(records) {
|
|
2387
|
+
return records.filter(
|
|
2388
|
+
(record) => record.conceptId.startsWith(`${DECISION_TYPE}.`) && !isNoDecisionRecord(record)
|
|
2389
|
+
);
|
|
2264
2390
|
}
|
|
2265
2391
|
|
|
2266
|
-
// src/
|
|
2267
|
-
var
|
|
2268
|
-
code;
|
|
2269
|
-
errorType;
|
|
2270
|
-
fault;
|
|
2271
|
-
retriable;
|
|
2272
|
-
reportToUser;
|
|
2273
|
-
details;
|
|
2274
|
-
constructor(props) {
|
|
2275
|
-
super(props.message);
|
|
2276
|
-
this.name = props.name ?? this.constructor.name;
|
|
2277
|
-
this.code = props.code ?? 500;
|
|
2278
|
-
this.errorType = props.errorType;
|
|
2279
|
-
this.fault = props.fault;
|
|
2280
|
-
this.retriable = props.retriable ?? true;
|
|
2281
|
-
this.reportToUser = props.reportToUser ?? false;
|
|
2282
|
-
this.details = props.details;
|
|
2283
|
-
}
|
|
2284
|
-
};
|
|
2392
|
+
// src/commands/anchor-resolve.ts
|
|
2393
|
+
var import_zod7 = require("zod");
|
|
2285
2394
|
|
|
2286
2395
|
// src/kb-errors.ts
|
|
2287
2396
|
var KbRecordAlreadyExistsError = class extends BaseError {
|
|
@@ -3192,14 +3301,117 @@ async function readSources(anchors, root, offline) {
|
|
|
3192
3301
|
return sources;
|
|
3193
3302
|
}
|
|
3194
3303
|
|
|
3195
|
-
// src/commands/
|
|
3304
|
+
// src/commands/anchor-set/model.ts
|
|
3196
3305
|
var import_zod8 = require("zod");
|
|
3306
|
+
var anchorSetInputSchema = import_zod8.z.object({
|
|
3307
|
+
reason: import_zod8.z.string().refine((text) => text.trim().length > 0, {
|
|
3308
|
+
message: "reason must say what was reviewed"
|
|
3309
|
+
}).describe(
|
|
3310
|
+
"What the reviewer read that makes these the right pointers. Recorded in the log."
|
|
3311
|
+
),
|
|
3312
|
+
anchors: import_zod8.z.array(kbAnchorWriteSchema).min(1).describe(
|
|
3313
|
+
"The complete new anchor set. Carry an anchor's hash forward to keep drift visible until the new code is read."
|
|
3314
|
+
)
|
|
3315
|
+
}).strict();
|
|
3316
|
+
var anchorSetCommandInput = import_zod8.z.object({
|
|
3317
|
+
bundlePath,
|
|
3318
|
+
conceptId,
|
|
3319
|
+
input: anchorSetInputSchema,
|
|
3320
|
+
resolve: import_zod8.z.boolean().optional().describe(
|
|
3321
|
+
"Also resolve and stamp every anchor against the current code, as anchor-resolve --rebaseline does."
|
|
3322
|
+
),
|
|
3323
|
+
repoRoot: import_zod8.z.string().min(1).optional().describe(
|
|
3324
|
+
"Where the anchored source lives, for resolve. Defaults to the working directory."
|
|
3325
|
+
),
|
|
3326
|
+
offline: import_zod8.z.boolean().optional().describe("With resolve, read foreign anchors from the repo cache only.")
|
|
3327
|
+
});
|
|
3328
|
+
|
|
3329
|
+
// src/commands/anchor-set/command.ts
|
|
3330
|
+
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.";
|
|
3331
|
+
var STAMPED_NOTE = "pointers set and stamped against the current code. Not verification: run verify separately if someone reviewed it.";
|
|
3332
|
+
var anchorSetCommand = define({
|
|
3333
|
+
name: "anchor-set",
|
|
3334
|
+
tool: "kb_anchor_set",
|
|
3335
|
+
usage: "anchor-set <concept-id> [--resolve] [--repo-root <path>] [--offline] < anchors.json",
|
|
3336
|
+
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.",
|
|
3337
|
+
input: anchorSetCommandInput,
|
|
3338
|
+
fromArgv: async (argv, path, stdin) => ({
|
|
3339
|
+
bundlePath: path,
|
|
3340
|
+
conceptId: argv[1],
|
|
3341
|
+
input: JSON.parse(await stdin()),
|
|
3342
|
+
resolve: argv.includes("--resolve"),
|
|
3343
|
+
repoRoot: argvFlag(argv, "--repo-root"),
|
|
3344
|
+
offline: argv.includes("--offline")
|
|
3345
|
+
}),
|
|
3346
|
+
run: async (ctx, { bundlePath: path, conceptId: id, input, resolve: resolve7, repoRoot, offline }) => {
|
|
3347
|
+
const { store, actor } = ctx;
|
|
3348
|
+
await assertBaseNotFrozen(process.cwd(), path);
|
|
3349
|
+
let applied;
|
|
3350
|
+
const record = await store.updateAnchors(
|
|
3351
|
+
path,
|
|
3352
|
+
id,
|
|
3353
|
+
(current) => {
|
|
3354
|
+
applied = applyAnchorSet(current, input.anchors);
|
|
3355
|
+
return {
|
|
3356
|
+
anchors: applied.anchors,
|
|
3357
|
+
log: {
|
|
3358
|
+
operation: "anchor-set",
|
|
3359
|
+
reason: input.reason,
|
|
3360
|
+
anchors: applied.changes
|
|
3361
|
+
}
|
|
3362
|
+
};
|
|
3363
|
+
},
|
|
3364
|
+
actor
|
|
3365
|
+
);
|
|
3366
|
+
const changes = applied?.changes ?? [];
|
|
3367
|
+
if (!resolve7) {
|
|
3368
|
+
return {
|
|
3369
|
+
conceptId: id,
|
|
3370
|
+
reason: input.reason,
|
|
3371
|
+
changes,
|
|
3372
|
+
anchors: record.frontmatter.strauss_anchors ?? [],
|
|
3373
|
+
baseline: "unchanged",
|
|
3374
|
+
note: NOTE
|
|
3375
|
+
};
|
|
3376
|
+
}
|
|
3377
|
+
const resolved = await anchorResolveCommand.run(
|
|
3378
|
+
ctx,
|
|
3379
|
+
anchorResolveCommand.input.parse({
|
|
3380
|
+
bundlePath: path,
|
|
3381
|
+
conceptId: id,
|
|
3382
|
+
rebaseline: true,
|
|
3383
|
+
...repoRoot ? { repoRoot } : {},
|
|
3384
|
+
...offline ? { offline } : {}
|
|
3385
|
+
})
|
|
3386
|
+
);
|
|
3387
|
+
const after = await store.read(path, id);
|
|
3388
|
+
return {
|
|
3389
|
+
conceptId: id,
|
|
3390
|
+
reason: input.reason,
|
|
3391
|
+
changes,
|
|
3392
|
+
anchors: after?.frontmatter.strauss_anchors ?? [],
|
|
3393
|
+
baseline: "stamped",
|
|
3394
|
+
resolved: resolved.results,
|
|
3395
|
+
note: STAMPED_NOTE
|
|
3396
|
+
};
|
|
3397
|
+
},
|
|
3398
|
+
// With `resolve`, a pointer that names nothing is a failed set, not a
|
|
3399
|
+
// finding to read later. A remote nothing could reach was never checked, so
|
|
3400
|
+
// it does not fail — the same line anchor-resolve draws.
|
|
3401
|
+
failsWhen: (result) => (result.resolved ?? []).some((entry) => {
|
|
3402
|
+
const { state, reason } = entry;
|
|
3403
|
+
return state === "unresolved" && !isUncheckedReason(reason);
|
|
3404
|
+
})
|
|
3405
|
+
});
|
|
3406
|
+
|
|
3407
|
+
// src/commands/answer.ts
|
|
3408
|
+
var import_zod9 = require("zod");
|
|
3197
3409
|
var answerCommand = define({
|
|
3198
3410
|
name: "answer",
|
|
3199
3411
|
tool: "kb_answer",
|
|
3200
3412
|
usage: "answer <concept-id> <answer...>",
|
|
3201
3413
|
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.",
|
|
3202
|
-
input:
|
|
3414
|
+
input: import_zod9.z.object({ bundlePath, conceptId, answer: import_zod9.z.string().min(1) }),
|
|
3203
3415
|
fromArgv: (argv, path) => ({
|
|
3204
3416
|
bundlePath: path,
|
|
3205
3417
|
conceptId: argv[1],
|
|
@@ -3213,19 +3425,19 @@ var answerCommand = define({
|
|
|
3213
3425
|
});
|
|
3214
3426
|
|
|
3215
3427
|
// src/commands/backlinks.ts
|
|
3216
|
-
var
|
|
3428
|
+
var import_zod10 = require("zod");
|
|
3217
3429
|
var backlinksCommand = define({
|
|
3218
3430
|
name: "backlinks",
|
|
3219
3431
|
tool: "kb_backlinks",
|
|
3220
3432
|
usage: "backlinks <concept-id>",
|
|
3221
3433
|
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.",
|
|
3222
|
-
input:
|
|
3434
|
+
input: import_zod10.z.object({ bundlePath, conceptId }),
|
|
3223
3435
|
fromArgv: (argv, path) => ({ bundlePath: path, conceptId: argv[1] }),
|
|
3224
3436
|
run: async ({ store }, { bundlePath: path, conceptId: id }) => store.backlinks(path, id)
|
|
3225
3437
|
});
|
|
3226
3438
|
|
|
3227
3439
|
// src/commands/catalog.ts
|
|
3228
|
-
var
|
|
3440
|
+
var import_zod11 = require("zod");
|
|
3229
3441
|
|
|
3230
3442
|
// src/adjudicate.ts
|
|
3231
3443
|
var STANDING = {
|
|
@@ -3404,9 +3616,9 @@ var catalogCommand = define({
|
|
|
3404
3616
|
tool: "kb_catalog",
|
|
3405
3617
|
usage: "catalog [type] [--tag T]...",
|
|
3406
3618
|
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.",
|
|
3407
|
-
input:
|
|
3619
|
+
input: import_zod11.z.object({
|
|
3408
3620
|
bundlePath,
|
|
3409
|
-
type:
|
|
3621
|
+
type: import_zod11.z.enum(KB_RECORD_TYPES).optional(),
|
|
3410
3622
|
tags: TAGS
|
|
3411
3623
|
}),
|
|
3412
3624
|
fromArgv: (argv, path) => {
|
|
@@ -3478,7 +3690,7 @@ function count(value, noun) {
|
|
|
3478
3690
|
var import_node_buffer = require("buffer");
|
|
3479
3691
|
var import_promises7 = require("fs/promises");
|
|
3480
3692
|
var import_node_path10 = require("path");
|
|
3481
|
-
var
|
|
3693
|
+
var import_zod14 = require("zod");
|
|
3482
3694
|
|
|
3483
3695
|
// src/match-diff.ts
|
|
3484
3696
|
function matchToDiff(files, records, options = {}) {
|
|
@@ -4082,7 +4294,7 @@ function claimOf(record) {
|
|
|
4082
4294
|
}
|
|
4083
4295
|
|
|
4084
4296
|
// src/commands/match/command.ts
|
|
4085
|
-
var
|
|
4297
|
+
var import_zod13 = require("zod");
|
|
4086
4298
|
|
|
4087
4299
|
// src/commands/match/errors.ts
|
|
4088
4300
|
var KbMatchInputError = class extends BaseError {
|
|
@@ -4102,21 +4314,21 @@ var KbMatchInputError = class extends BaseError {
|
|
|
4102
4314
|
};
|
|
4103
4315
|
|
|
4104
4316
|
// src/commands/match/model.ts
|
|
4105
|
-
var
|
|
4106
|
-
var diffHunkSchema =
|
|
4107
|
-
startLine:
|
|
4108
|
-
endLine:
|
|
4109
|
-
side:
|
|
4317
|
+
var import_zod12 = require("zod");
|
|
4318
|
+
var diffHunkSchema = import_zod12.z.object({
|
|
4319
|
+
startLine: import_zod12.z.number().int().positive(),
|
|
4320
|
+
endLine: import_zod12.z.number().int().positive(),
|
|
4321
|
+
side: import_zod12.z.enum(["old", "new"]).optional()
|
|
4110
4322
|
}).passthrough();
|
|
4111
|
-
var diffFileSchema =
|
|
4112
|
-
filePath:
|
|
4113
|
-
hunks:
|
|
4323
|
+
var diffFileSchema = import_zod12.z.object({
|
|
4324
|
+
filePath: import_zod12.z.string().min(1).describe("Repo-relative, spelled the way anchors are."),
|
|
4325
|
+
hunks: import_zod12.z.array(diffHunkSchema)
|
|
4114
4326
|
});
|
|
4115
|
-
var symbolRangeSchema =
|
|
4116
|
-
file:
|
|
4117
|
-
symbol:
|
|
4118
|
-
startLine:
|
|
4119
|
-
endLine:
|
|
4327
|
+
var symbolRangeSchema = import_zod12.z.object({
|
|
4328
|
+
file: import_zod12.z.string().min(1),
|
|
4329
|
+
symbol: import_zod12.z.string().min(1),
|
|
4330
|
+
startLine: import_zod12.z.number().int().positive(),
|
|
4331
|
+
endLine: import_zod12.z.number().int().positive()
|
|
4120
4332
|
});
|
|
4121
4333
|
|
|
4122
4334
|
// src/commands/match/parse-unified-diff.ts
|
|
@@ -4365,17 +4577,17 @@ var matchCommand = define({
|
|
|
4365
4577
|
tool: "kb_match",
|
|
4366
4578
|
usage: "match --git <base>..<head> | --stdin [--repo-root <path>] [--offline] [--include-non-current]",
|
|
4367
4579
|
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.",
|
|
4368
|
-
input:
|
|
4580
|
+
input: import_zod13.z.object({
|
|
4369
4581
|
bundlePath,
|
|
4370
|
-
files:
|
|
4371
|
-
symbolRanges:
|
|
4582
|
+
files: import_zod13.z.array(diffFileSchema).describe("The changed files, each with its post-change line ranges."),
|
|
4583
|
+
symbolRanges: import_zod13.z.array(symbolRangeSchema).optional().describe(
|
|
4372
4584
|
"Symbol spans the caller already has. Resolved from repoRoot when omitted."
|
|
4373
4585
|
),
|
|
4374
4586
|
repoRoot: REPO_ROOT,
|
|
4375
|
-
offline:
|
|
4587
|
+
offline: import_zod13.z.boolean().optional().describe(
|
|
4376
4588
|
"Resolve symbol ranges from what is already on disk, never fetching a grammar."
|
|
4377
4589
|
),
|
|
4378
|
-
includeNonCurrent:
|
|
4590
|
+
includeNonCurrent: import_zod13.z.boolean().optional().describe(
|
|
4379
4591
|
"Return superseded, rejected and unsettled records too, each carrying its standing."
|
|
4380
4592
|
)
|
|
4381
4593
|
}),
|
|
@@ -4389,11 +4601,11 @@ var matchCommand = define({
|
|
|
4389
4601
|
...argv.includes("--include-non-current") ? { includeNonCurrent: true } : {}
|
|
4390
4602
|
};
|
|
4391
4603
|
if (range !== void 0) {
|
|
4392
|
-
const
|
|
4393
|
-
if (!
|
|
4394
|
-
throw new KbMatchInputError(`--git ${range} ${REFUSED[
|
|
4604
|
+
const diff2 = await readRangeDiff(repoRoot ?? process.cwd(), range);
|
|
4605
|
+
if (!diff2.ok) {
|
|
4606
|
+
throw new KbMatchInputError(`--git ${range} ${REFUSED[diff2.reason]}`);
|
|
4395
4607
|
}
|
|
4396
|
-
return { ...base2, files: parseUnifiedDiff(
|
|
4608
|
+
return { ...base2, files: parseUnifiedDiff(diff2.text) };
|
|
4397
4609
|
}
|
|
4398
4610
|
if (!argv.includes("--stdin")) {
|
|
4399
4611
|
throw new KbMatchInputError(
|
|
@@ -4479,22 +4691,22 @@ function project(match, ranges, all) {
|
|
|
4479
4691
|
|
|
4480
4692
|
// src/commands/classify.ts
|
|
4481
4693
|
var classifyFileSchema = diffFileSchema.extend({
|
|
4482
|
-
hunks:
|
|
4483
|
-
diffHunkSchema.extend({ lines:
|
|
4694
|
+
hunks: import_zod14.z.array(
|
|
4695
|
+
diffHunkSchema.extend({ lines: import_zod14.z.array(import_zod14.z.string()).optional() })
|
|
4484
4696
|
),
|
|
4485
|
-
renamedFrom:
|
|
4486
|
-
similarity:
|
|
4697
|
+
renamedFrom: import_zod14.z.string().min(1).optional().describe("Where `git diff -M` says the path came from."),
|
|
4698
|
+
similarity: import_zod14.z.number().min(0).max(100).optional()
|
|
4487
4699
|
});
|
|
4488
4700
|
var classifyCommand = define({
|
|
4489
4701
|
name: "classify",
|
|
4490
4702
|
tool: "kb_classify",
|
|
4491
4703
|
usage: "classify --git <base>..<head> | --stdin [--repo-root <path>] [--offline]",
|
|
4492
4704
|
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.",
|
|
4493
|
-
input:
|
|
4705
|
+
input: import_zod14.z.object({
|
|
4494
4706
|
bundlePath,
|
|
4495
|
-
files:
|
|
4707
|
+
files: import_zod14.z.array(classifyFileSchema).describe("The changed files, each with its line ranges."),
|
|
4496
4708
|
repoRoot: REPO_ROOT,
|
|
4497
|
-
offline:
|
|
4709
|
+
offline: import_zod14.z.boolean().optional().describe(
|
|
4498
4710
|
"Resolve symbol ranges from what is already on disk, never fetching a grammar."
|
|
4499
4711
|
)
|
|
4500
4712
|
}),
|
|
@@ -4507,15 +4719,15 @@ var classifyCommand = define({
|
|
|
4507
4719
|
...argv.includes("--offline") ? { offline: true } : {}
|
|
4508
4720
|
};
|
|
4509
4721
|
if (range !== void 0) {
|
|
4510
|
-
const
|
|
4511
|
-
if (!
|
|
4722
|
+
const diff2 = await readRangeDiff(repoRoot ?? process.cwd(), range);
|
|
4723
|
+
if (!diff2.ok) {
|
|
4512
4724
|
throw new KbClassifyInputError(
|
|
4513
|
-
`--git ${range} ${REFUSED2[
|
|
4725
|
+
`--git ${range} ${REFUSED2[diff2.reason]}`
|
|
4514
4726
|
);
|
|
4515
4727
|
}
|
|
4516
4728
|
return {
|
|
4517
4729
|
...base2,
|
|
4518
|
-
files: parseUnifiedDiff(
|
|
4730
|
+
files: parseUnifiedDiff(diff2.text, {
|
|
4519
4731
|
keepEmpty: true,
|
|
4520
4732
|
withLines: true
|
|
4521
4733
|
})
|
|
@@ -4605,7 +4817,7 @@ function renderClassify(result) {
|
|
|
4605
4817
|
}
|
|
4606
4818
|
|
|
4607
4819
|
// src/commands/context.ts
|
|
4608
|
-
var
|
|
4820
|
+
var import_zod15 = require("zod");
|
|
4609
4821
|
|
|
4610
4822
|
// src/kb-context.ts
|
|
4611
4823
|
var import_promises8 = require("fs/promises");
|
|
@@ -4876,23 +5088,23 @@ var contextCommand = define({
|
|
|
4876
5088
|
tool: "kb_context",
|
|
4877
5089
|
usage: "context [--profile NAME] [--budget N] [--full-under N] [--exclude-tag T]... [--format json] [--event NAME]",
|
|
4878
5090
|
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.",
|
|
4879
|
-
input:
|
|
4880
|
-
budgetTokens:
|
|
5091
|
+
input: import_zod15.z.object({
|
|
5092
|
+
budgetTokens: import_zod15.z.number().int().positive().optional().describe(
|
|
4881
5093
|
"Ceiling on the whole emitted block; past it the command refuses with a list of bases rather than truncating. Defaults to 4000."
|
|
4882
5094
|
),
|
|
4883
|
-
fullUnderTokens:
|
|
5095
|
+
fullUnderTokens: import_zod15.z.number().int().positive().optional().describe(
|
|
4884
5096
|
"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."
|
|
4885
5097
|
),
|
|
4886
|
-
profile:
|
|
5098
|
+
profile: import_zod15.z.string().optional().describe(
|
|
4887
5099
|
"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."
|
|
4888
5100
|
),
|
|
4889
|
-
excludeTags:
|
|
5101
|
+
excludeTags: import_zod15.z.array(import_zod15.z.string().min(1)).optional().describe(
|
|
4890
5102
|
"Frontmatter tags whose records stay out of the block. The base stays pinned and stays readable by tool; resolved like the budgets."
|
|
4891
5103
|
),
|
|
4892
|
-
format:
|
|
5104
|
+
format: import_zod15.z.enum(["markdown", "json"]).optional().describe(
|
|
4893
5105
|
"CLI envelope for hook protocols that require strict JSON on stdout. MCP callers omit this \u2014 the block itself is identical."
|
|
4894
5106
|
),
|
|
4895
|
-
event:
|
|
5107
|
+
event: import_zod15.z.string().optional().describe(
|
|
4896
5108
|
"hookEventName stamped into the JSON envelope. Only meaningful with format=json."
|
|
4897
5109
|
)
|
|
4898
5110
|
}),
|
|
@@ -4931,7 +5143,7 @@ var contextCommand = define({
|
|
|
4931
5143
|
});
|
|
4932
5144
|
|
|
4933
5145
|
// src/commands/doctor.ts
|
|
4934
|
-
var
|
|
5146
|
+
var import_zod17 = require("zod");
|
|
4935
5147
|
|
|
4936
5148
|
// src/kb-edges.ts
|
|
4937
5149
|
var KB_EDGE_KINDS = [
|
|
@@ -5447,17 +5659,17 @@ function ageInDays(record, now) {
|
|
|
5447
5659
|
}
|
|
5448
5660
|
|
|
5449
5661
|
// src/commands/reassess.ts
|
|
5450
|
-
var
|
|
5662
|
+
var import_zod16 = require("zod");
|
|
5451
5663
|
var reassessCommand = define({
|
|
5452
5664
|
name: "reassess",
|
|
5453
5665
|
tool: "kb_reassess",
|
|
5454
5666
|
usage: "reassess <concept-id> [--repo-root <path>] [--with-diff]",
|
|
5455
5667
|
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.",
|
|
5456
|
-
input:
|
|
5668
|
+
input: import_zod16.z.object({
|
|
5457
5669
|
bundlePath,
|
|
5458
5670
|
conceptId,
|
|
5459
5671
|
repoRoot: REPO_ROOT,
|
|
5460
|
-
withDiff:
|
|
5672
|
+
withDiff: import_zod16.z.boolean().optional().describe(
|
|
5461
5673
|
"Recover each anchor's committed span and render the diff. Reads git history."
|
|
5462
5674
|
)
|
|
5463
5675
|
}),
|
|
@@ -5602,13 +5814,13 @@ function at(file, symbol) {
|
|
|
5602
5814
|
}
|
|
5603
5815
|
|
|
5604
5816
|
// src/commands/doctor.ts
|
|
5605
|
-
var days = (what, fallback) =>
|
|
5817
|
+
var days = (what, fallback) => import_zod17.z.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
|
|
5606
5818
|
var doctorCommand = define({
|
|
5607
5819
|
name: "doctor",
|
|
5608
5820
|
tool: "kb_doctor",
|
|
5609
5821
|
usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--offline] [--strict] [--drifted [--with-diff]]",
|
|
5610
5822
|
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.",
|
|
5611
|
-
input:
|
|
5823
|
+
input: import_zod17.z.object({
|
|
5612
5824
|
bundlePath,
|
|
5613
5825
|
repoRoot: REPO_ROOT,
|
|
5614
5826
|
expiringDays: days(
|
|
@@ -5623,16 +5835,16 @@ var doctorCommand = define({
|
|
|
5623
5835
|
"How long a record may stay `open` or `proposed` before `aging` reports it, in days.",
|
|
5624
5836
|
DEFAULT_AGING_DAYS
|
|
5625
5837
|
),
|
|
5626
|
-
offline:
|
|
5838
|
+
offline: import_zod17.z.boolean().optional().describe(
|
|
5627
5839
|
"Read foreign anchors from the local repo cache only, never fetching."
|
|
5628
5840
|
),
|
|
5629
|
-
strict:
|
|
5841
|
+
strict: import_zod17.z.boolean().optional().describe(
|
|
5630
5842
|
"Turn an expired record into a non-zero exit for the CLI. No effect on the report itself."
|
|
5631
5843
|
),
|
|
5632
|
-
drifted:
|
|
5844
|
+
drifted: import_zod17.z.boolean().optional().describe(
|
|
5633
5845
|
"Report only drift, as a reassessment packet per record: claim, per-anchor class, and what depends on it."
|
|
5634
5846
|
),
|
|
5635
|
-
withDiff:
|
|
5847
|
+
withDiff: import_zod17.z.boolean().optional().describe(
|
|
5636
5848
|
"With `drifted`: recover each anchor's committed span and render the old-vs-new diff. Reads git history."
|
|
5637
5849
|
)
|
|
5638
5850
|
}),
|
|
@@ -5809,7 +6021,7 @@ function renderPackets(result) {
|
|
|
5809
6021
|
// src/commands/export.ts
|
|
5810
6022
|
var import_promises9 = require("fs/promises");
|
|
5811
6023
|
var import_node_path11 = require("path");
|
|
5812
|
-
var
|
|
6024
|
+
var import_zod18 = require("zod");
|
|
5813
6025
|
var NUMBERED = /^(\d{4})-(.+)\.md$/;
|
|
5814
6026
|
var MARKER = "<!-- strauss-kb export: ";
|
|
5815
6027
|
var exportCommand = define({
|
|
@@ -5817,10 +6029,10 @@ var exportCommand = define({
|
|
|
5817
6029
|
tool: "kb_export",
|
|
5818
6030
|
usage: "export --format madr --to <dir>",
|
|
5819
6031
|
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.",
|
|
5820
|
-
input:
|
|
6032
|
+
input: import_zod18.z.object({
|
|
5821
6033
|
bundlePath,
|
|
5822
|
-
format:
|
|
5823
|
-
to:
|
|
6034
|
+
format: import_zod18.z.enum(["madr"]).describe("Output layout. `madr` is the only one so far."),
|
|
6035
|
+
to: import_zod18.z.string().min(1).describe("Directory the ADR files are written into.")
|
|
5824
6036
|
}),
|
|
5825
6037
|
fromArgv: (argv, path) => ({
|
|
5826
6038
|
bundlePath: path,
|
|
@@ -5942,19 +6154,19 @@ function bodySections(body) {
|
|
|
5942
6154
|
}
|
|
5943
6155
|
|
|
5944
6156
|
// src/commands/impact.ts
|
|
5945
|
-
var
|
|
6157
|
+
var import_zod19 = require("zod");
|
|
5946
6158
|
var impactCommand = define({
|
|
5947
6159
|
name: "impact",
|
|
5948
6160
|
tool: "kb_impact",
|
|
5949
6161
|
usage: "impact <concept-id> [--depth N] [--rels a,b]",
|
|
5950
6162
|
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.",
|
|
5951
|
-
input:
|
|
6163
|
+
input: import_zod19.z.object({
|
|
5952
6164
|
bundlePath,
|
|
5953
6165
|
conceptId,
|
|
5954
|
-
depth:
|
|
6166
|
+
depth: import_zod19.z.number().int().positive().optional().describe(
|
|
5955
6167
|
"Hops out from the record. Unbounded when omitted; a walk this cuts reports truncated: true."
|
|
5956
6168
|
),
|
|
5957
|
-
rels:
|
|
6169
|
+
rels: import_zod19.z.array(import_zod19.z.enum(KB_CAUSAL_LINK_RELS)).optional().describe(
|
|
5958
6170
|
"Narrow which rels the walk follows. Defaults to every rel that carries a dependence \u2014 all but related_to."
|
|
5959
6171
|
)
|
|
5960
6172
|
}),
|
|
@@ -5975,15 +6187,15 @@ var impactCommand = define({
|
|
|
5975
6187
|
});
|
|
5976
6188
|
|
|
5977
6189
|
// src/commands/list.ts
|
|
5978
|
-
var
|
|
6190
|
+
var import_zod20 = require("zod");
|
|
5979
6191
|
var listCommand = define({
|
|
5980
6192
|
name: "list",
|
|
5981
6193
|
tool: "kb_list",
|
|
5982
6194
|
usage: "list [type] [--tag T]...",
|
|
5983
6195
|
description: "Every record, optionally one type or tag. For enumerating; use kb_query for a question.",
|
|
5984
|
-
input:
|
|
6196
|
+
input: import_zod20.z.object({
|
|
5985
6197
|
bundlePath,
|
|
5986
|
-
type:
|
|
6198
|
+
type: import_zod20.z.enum(KB_RECORD_TYPES).optional(),
|
|
5987
6199
|
tags: TAGS
|
|
5988
6200
|
}),
|
|
5989
6201
|
fromArgv: (argv, path) => {
|
|
@@ -6007,17 +6219,17 @@ var listCommand = define({
|
|
|
6007
6219
|
});
|
|
6008
6220
|
|
|
6009
6221
|
// src/commands/load.ts
|
|
6010
|
-
var
|
|
6222
|
+
var import_zod21 = require("zod");
|
|
6011
6223
|
var loadCommand = define({
|
|
6012
6224
|
name: "load",
|
|
6013
6225
|
tool: "kb_load",
|
|
6014
6226
|
usage: "load [type] [--budget N | --all] [--repo-root PATH]",
|
|
6015
6227
|
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.",
|
|
6016
|
-
input:
|
|
6228
|
+
input: import_zod21.z.object({
|
|
6017
6229
|
bundlePath,
|
|
6018
|
-
type:
|
|
6019
|
-
budgetTokens:
|
|
6020
|
-
all:
|
|
6230
|
+
type: import_zod21.z.enum(KB_RECORD_TYPES).optional(),
|
|
6231
|
+
budgetTokens: import_zod21.z.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
|
|
6232
|
+
all: import_zod21.z.boolean().optional().describe(
|
|
6021
6233
|
"Loads the entire base regardless of size, bypassing the token budget; mutually exclusive with budgetTokens."
|
|
6022
6234
|
),
|
|
6023
6235
|
repoRoot: REPO_ROOT
|
|
@@ -6059,25 +6271,25 @@ var loadCommand = define({
|
|
|
6059
6271
|
});
|
|
6060
6272
|
|
|
6061
6273
|
// src/commands/log.ts
|
|
6062
|
-
var
|
|
6274
|
+
var import_zod22 = require("zod");
|
|
6063
6275
|
var logCommand = define({
|
|
6064
6276
|
name: "log",
|
|
6065
6277
|
tool: "kb_log",
|
|
6066
6278
|
usage: "log",
|
|
6067
6279
|
description: "Who touched what, and when. Append-only; malformed lines are reported, never repaired.",
|
|
6068
|
-
input:
|
|
6280
|
+
input: import_zod22.z.object({ bundlePath }),
|
|
6069
6281
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
6070
6282
|
run: ({ store }, { bundlePath: path }) => store.readLog(path)
|
|
6071
6283
|
});
|
|
6072
6284
|
|
|
6073
6285
|
// src/commands/no-decision.ts
|
|
6074
|
-
var
|
|
6286
|
+
var import_zod23 = require("zod");
|
|
6075
6287
|
var noDecisionCommand = define({
|
|
6076
6288
|
name: "no-decision",
|
|
6077
6289
|
tool: "kb_no_decision",
|
|
6078
6290
|
usage: "no-decision <reason...>",
|
|
6079
6291
|
description: "Record in one sentence that a piece of work had nothing to decide. Idempotent.",
|
|
6080
|
-
input:
|
|
6292
|
+
input: import_zod23.z.object({ bundlePath, reason: import_zod23.z.string().min(1) }),
|
|
6081
6293
|
fromArgv: (argv, path) => ({
|
|
6082
6294
|
bundlePath: path,
|
|
6083
6295
|
reason: argv.slice(1).join(" ").trim()
|
|
@@ -6094,20 +6306,20 @@ var noDecisionCommand = define({
|
|
|
6094
6306
|
});
|
|
6095
6307
|
|
|
6096
6308
|
// src/commands/pack.ts
|
|
6097
|
-
var
|
|
6309
|
+
var import_zod24 = require("zod");
|
|
6098
6310
|
var packCommand = define({
|
|
6099
6311
|
name: "pack",
|
|
6100
6312
|
tool: "kb_pack",
|
|
6101
6313
|
usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
|
|
6102
6314
|
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.",
|
|
6103
|
-
input:
|
|
6315
|
+
input: import_zod24.z.object({
|
|
6104
6316
|
bundlePath,
|
|
6105
6317
|
conceptId,
|
|
6106
|
-
hops:
|
|
6107
|
-
maxNodes:
|
|
6318
|
+
hops: import_zod24.z.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
|
|
6319
|
+
maxNodes: import_zod24.z.number().int().positive().optional().describe(
|
|
6108
6320
|
"How many records the pack may hold, root included. Defaults to 20."
|
|
6109
6321
|
),
|
|
6110
|
-
budgetTokens:
|
|
6322
|
+
budgetTokens: import_zod24.z.number().int().positive().optional().describe(
|
|
6111
6323
|
"Approximate token ceiling over what is actually emitted. Defaults to 25000."
|
|
6112
6324
|
)
|
|
6113
6325
|
}),
|
|
@@ -6194,22 +6406,22 @@ function warningLabel(warning) {
|
|
|
6194
6406
|
}
|
|
6195
6407
|
|
|
6196
6408
|
// src/commands/pin.ts
|
|
6197
|
-
var
|
|
6409
|
+
var import_zod25 = require("zod");
|
|
6198
6410
|
var pinCommand = define({
|
|
6199
6411
|
name: "pin",
|
|
6200
6412
|
tool: "kb_pin",
|
|
6201
6413
|
usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
|
|
6202
6414
|
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.",
|
|
6203
|
-
input:
|
|
6415
|
+
input: import_zod25.z.object({
|
|
6204
6416
|
bundlePath,
|
|
6205
|
-
mode:
|
|
6417
|
+
mode: import_zod25.z.enum(["full", "index"]).optional().describe(
|
|
6206
6418
|
"full: always emit this base's records whole (still under the block budget); index: never upgrade. Absent: the profile's full-under threshold decides."
|
|
6207
6419
|
),
|
|
6208
|
-
profiles:
|
|
6209
|
-
layer:
|
|
6420
|
+
profiles: import_zod25.z.array(import_zod25.z.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
|
|
6421
|
+
layer: import_zod25.z.enum(["project", "local", "user"]).optional().describe(
|
|
6210
6422
|
"Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
|
|
6211
6423
|
),
|
|
6212
|
-
frozen:
|
|
6424
|
+
frozen: import_zod25.z.boolean().optional().describe(
|
|
6213
6425
|
"true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
|
|
6214
6426
|
)
|
|
6215
6427
|
}),
|
|
@@ -6238,13 +6450,13 @@ var pinCommand = define({
|
|
|
6238
6450
|
});
|
|
6239
6451
|
|
|
6240
6452
|
// src/commands/pins.ts
|
|
6241
|
-
var
|
|
6453
|
+
var import_zod26 = require("zod");
|
|
6242
6454
|
var pinsCommand = define({
|
|
6243
6455
|
name: "pins",
|
|
6244
6456
|
tool: "kb_pins",
|
|
6245
6457
|
usage: "pins",
|
|
6246
6458
|
description: "Every pinned base across the manifest layers, with its layer and whether it resolves to records. Takes no bundlePath.",
|
|
6247
|
-
input:
|
|
6459
|
+
input: import_zod26.z.object({}),
|
|
6248
6460
|
fromArgv: () => ({}),
|
|
6249
6461
|
run: ({ store }) => listPins(store, process.cwd())
|
|
6250
6462
|
});
|
|
@@ -6515,16 +6727,16 @@ function recordType(conceptId2) {
|
|
|
6515
6727
|
}
|
|
6516
6728
|
|
|
6517
6729
|
// src/commands/promote/model.ts
|
|
6518
|
-
var
|
|
6519
|
-
var promoteInputSchema =
|
|
6730
|
+
var import_zod27 = require("zod");
|
|
6731
|
+
var promoteInputSchema = import_zod27.z.object({
|
|
6520
6732
|
bundlePath,
|
|
6521
|
-
conceptIds:
|
|
6522
|
-
to:
|
|
6523
|
-
source:
|
|
6733
|
+
conceptIds: import_zod27.z.array(conceptId).max(64).optional().describe("Records to copy into the target base. Omit with `list`."),
|
|
6734
|
+
to: import_zod27.z.string().min(1).optional().describe("Absolute path to the base being promoted into."),
|
|
6735
|
+
source: import_zod27.z.string().min(1).optional().describe(
|
|
6524
6736
|
"Where the promotion came from, usually the pull request URL. Recorded on each copy as a source."
|
|
6525
6737
|
),
|
|
6526
|
-
force:
|
|
6527
|
-
list:
|
|
6738
|
+
force: import_zod27.z.boolean().optional().describe("Overwrite a record the target base already holds."),
|
|
6739
|
+
list: import_zod27.z.boolean().optional().describe("List the source base's candidates instead of promoting.")
|
|
6528
6740
|
}).refine((input) => input.list === true || input.to !== void 0, {
|
|
6529
6741
|
message: "promote needs a target base \u2014 pass --to <bundle>, or --list",
|
|
6530
6742
|
path: ["to"]
|
|
@@ -6661,17 +6873,17 @@ function renderPromote(result) {
|
|
|
6661
6873
|
}
|
|
6662
6874
|
|
|
6663
6875
|
// src/commands/query.ts
|
|
6664
|
-
var
|
|
6876
|
+
var import_zod28 = require("zod");
|
|
6665
6877
|
var queryCommand = define({
|
|
6666
6878
|
name: "query",
|
|
6667
6879
|
tool: "kb_query",
|
|
6668
6880
|
usage: "query <text...> [--tag T]... [--repo-root PATH]",
|
|
6669
6881
|
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.",
|
|
6670
|
-
input:
|
|
6882
|
+
input: import_zod28.z.object({
|
|
6671
6883
|
bundlePath,
|
|
6672
|
-
text:
|
|
6673
|
-
type:
|
|
6674
|
-
includeNonCurrent:
|
|
6884
|
+
text: import_zod28.z.string().optional(),
|
|
6885
|
+
type: import_zod28.z.enum(KB_RECORD_TYPES).optional(),
|
|
6886
|
+
includeNonCurrent: import_zod28.z.boolean().optional(),
|
|
6675
6887
|
tags: TAGS,
|
|
6676
6888
|
repoRoot: REPO_ROOT
|
|
6677
6889
|
}),
|
|
@@ -6705,27 +6917,32 @@ var queryCommand = define({
|
|
|
6705
6917
|
});
|
|
6706
6918
|
|
|
6707
6919
|
// src/commands/read-index.ts
|
|
6708
|
-
var
|
|
6920
|
+
var import_zod29 = require("zod");
|
|
6709
6921
|
var readIndexCommand = define({
|
|
6710
6922
|
name: "index",
|
|
6711
6923
|
tool: "kb_index",
|
|
6712
6924
|
usage: "index",
|
|
6713
6925
|
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.",
|
|
6714
|
-
input:
|
|
6926
|
+
input: import_zod29.z.object({ bundlePath }),
|
|
6715
6927
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
6716
6928
|
run: ({ store }, { bundlePath: path }) => store.readIndex(path)
|
|
6717
6929
|
});
|
|
6718
6930
|
|
|
6719
6931
|
// src/commands/schema.ts
|
|
6720
|
-
var
|
|
6932
|
+
var import_zod32 = require("zod");
|
|
6721
6933
|
|
|
6722
6934
|
// src/json-schema.ts
|
|
6723
|
-
var
|
|
6935
|
+
var import_zod31 = require("zod");
|
|
6724
6936
|
|
|
6725
6937
|
// src/kb-log.ts
|
|
6726
|
-
var
|
|
6938
|
+
var import_zod30 = require("zod");
|
|
6727
6939
|
var LOG_FILE = "log.jsonl";
|
|
6728
|
-
var
|
|
6940
|
+
var kbLogAnchorChangeSchema = import_zod30.z.object({
|
|
6941
|
+
op: import_zod30.z.enum(["move", "add", "drop"]),
|
|
6942
|
+
from: kbAnchorLocatorSchema.optional(),
|
|
6943
|
+
to: kbAnchorLocatorSchema.optional()
|
|
6944
|
+
}).strict();
|
|
6945
|
+
var kbLogEntryFields = import_zod30.z.object({
|
|
6729
6946
|
// Validated, not just `min(1)`: `at` is a sort key (see `parseLog`
|
|
6730
6947
|
// below), and a value that isn't actually chronological — a Unix
|
|
6731
6948
|
// timestamp, a human-typed date, garbage — would sort wrong without
|
|
@@ -6734,18 +6951,28 @@ var kbLogEntrySchema = import_zod29.z.object({
|
|
|
6734
6951
|
// and rejects everything else, including a non-`Z` offset — so a
|
|
6735
6952
|
// malformed `at` is reported the same way a malformed line already is,
|
|
6736
6953
|
// rather than silently sorting into the wrong place.
|
|
6737
|
-
at:
|
|
6738
|
-
by:
|
|
6739
|
-
operation:
|
|
6740
|
-
conceptId:
|
|
6954
|
+
at: import_zod30.z.iso.datetime(),
|
|
6955
|
+
by: import_zod30.z.string().min(1),
|
|
6956
|
+
operation: import_zod30.z.string().min(1),
|
|
6957
|
+
conceptId: import_zod30.z.string().min(1),
|
|
6741
6958
|
/**
|
|
6742
6959
|
* The operation's other end, where it has one: a second concept id for
|
|
6743
6960
|
* supersession, the other base's path for promotion.
|
|
6744
6961
|
*/
|
|
6745
|
-
target:
|
|
6746
|
-
|
|
6962
|
+
target: import_zod30.z.string().min(1).optional(),
|
|
6963
|
+
/**
|
|
6964
|
+
* Why the operation was performed, where the operation demands one.
|
|
6965
|
+
* `anchor-set` does: a pointer moved by a reader is only auditable if
|
|
6966
|
+
* the reading is recorded beside it.
|
|
6967
|
+
*/
|
|
6968
|
+
reason: import_zod30.z.string().min(1).optional(),
|
|
6969
|
+
/** What `anchor-set` changed, derived from the record before and after. */
|
|
6970
|
+
anchors: import_zod30.z.array(kbLogAnchorChangeSchema).optional()
|
|
6971
|
+
});
|
|
6972
|
+
var kbLogEntrySchema = kbLogEntryFields.passthrough();
|
|
6973
|
+
var kbLogEntryWriteSchema = kbLogEntryFields.strict();
|
|
6747
6974
|
function renderLogEntry(entry) {
|
|
6748
|
-
return `${JSON.stringify(
|
|
6975
|
+
return `${JSON.stringify(kbLogEntryWriteSchema.parse(entry))}
|
|
6749
6976
|
`;
|
|
6750
6977
|
}
|
|
6751
6978
|
var CONFLICT_MARKER = /^(<{7}|\|{7}|={7}|>{7})/;
|
|
@@ -6786,11 +7013,11 @@ function parseLog(raw) {
|
|
|
6786
7013
|
// src/json-schema.ts
|
|
6787
7014
|
function kbJsonSchemas() {
|
|
6788
7015
|
return {
|
|
6789
|
-
recordFrontmatter:
|
|
7016
|
+
recordFrontmatter: import_zod31.z.toJSONSchema(kbRecordFrontmatterSchema, {
|
|
6790
7017
|
io: "input"
|
|
6791
7018
|
}),
|
|
6792
|
-
composeInput:
|
|
6793
|
-
logEntry:
|
|
7019
|
+
composeInput: import_zod31.z.toJSONSchema(composeInputSchema, { io: "input" }),
|
|
7020
|
+
logEntry: import_zod31.z.toJSONSchema(kbLogEntrySchema, { io: "input" })
|
|
6794
7021
|
};
|
|
6795
7022
|
}
|
|
6796
7023
|
|
|
@@ -6800,25 +7027,25 @@ var schemaCommand = define({
|
|
|
6800
7027
|
tool: "kb_schema",
|
|
6801
7028
|
usage: "schema",
|
|
6802
7029
|
description: "JSON Schema for frontmatter, write input, and log entries, generated from the enforcing code.",
|
|
6803
|
-
input:
|
|
7030
|
+
input: import_zod32.z.object({}),
|
|
6804
7031
|
fromArgv: () => ({}),
|
|
6805
7032
|
run: () => Promise.resolve(kbJsonSchemas())
|
|
6806
7033
|
});
|
|
6807
7034
|
|
|
6808
7035
|
// src/commands/stamp.ts
|
|
6809
7036
|
var import_promises10 = require("fs/promises");
|
|
6810
|
-
var
|
|
7037
|
+
var import_zod33 = require("zod");
|
|
6811
7038
|
var DIGEST = /^[0-9a-f]{64}$/;
|
|
6812
7039
|
var stampCommand = define({
|
|
6813
7040
|
name: "stamp",
|
|
6814
7041
|
tool: "kb_stamp",
|
|
6815
7042
|
usage: "stamp [--bundle PATH] [--since DIGEST|FILE]",
|
|
6816
7043
|
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.",
|
|
6817
|
-
input:
|
|
6818
|
-
bundlePath:
|
|
7044
|
+
input: import_zod33.z.object({
|
|
7045
|
+
bundlePath: import_zod33.z.string().min(1).optional().describe(
|
|
6819
7046
|
"Absolute path to one knowledge base. Omit to stamp every pinned base."
|
|
6820
7047
|
),
|
|
6821
|
-
since:
|
|
7048
|
+
since: import_zod33.z.string().min(1).optional().describe(
|
|
6822
7049
|
"Prior digest, or path to a prior `stamp --json`; only moved bases return, with changed ids when the baseline is a file."
|
|
6823
7050
|
)
|
|
6824
7051
|
}),
|
|
@@ -6904,16 +7131,16 @@ async function readBaseline(since) {
|
|
|
6904
7131
|
}
|
|
6905
7132
|
|
|
6906
7133
|
// src/commands/status.ts
|
|
6907
|
-
var
|
|
7134
|
+
var import_zod34 = require("zod");
|
|
6908
7135
|
var statusCommand = define({
|
|
6909
7136
|
name: "status",
|
|
6910
7137
|
tool: "kb_status",
|
|
6911
7138
|
usage: "status <concept-id> <status>",
|
|
6912
7139
|
description: "Move a record's status. Compare-and-swap: a concurrent change fails instead of being overwritten.",
|
|
6913
|
-
input:
|
|
7140
|
+
input: import_zod34.z.object({
|
|
6914
7141
|
bundlePath,
|
|
6915
7142
|
conceptId,
|
|
6916
|
-
status:
|
|
7143
|
+
status: import_zod34.z.enum(KB_RECORD_STATUSES)
|
|
6917
7144
|
}),
|
|
6918
7145
|
fromArgv: (argv, path) => ({
|
|
6919
7146
|
bundlePath: path,
|
|
@@ -6928,13 +7155,13 @@ var statusCommand = define({
|
|
|
6928
7155
|
});
|
|
6929
7156
|
|
|
6930
7157
|
// src/commands/supersede.ts
|
|
6931
|
-
var
|
|
7158
|
+
var import_zod35 = require("zod");
|
|
6932
7159
|
var supersedeCommand = define({
|
|
6933
7160
|
name: "supersede",
|
|
6934
7161
|
tool: "kb_supersede",
|
|
6935
7162
|
usage: "supersede <concept-id> <replacement-id>",
|
|
6936
7163
|
description: "Mark a record superseded by another, linked in both directions. Use instead of editing a record whose meaning changed.",
|
|
6937
|
-
input:
|
|
7164
|
+
input: import_zod35.z.object({ bundlePath, conceptId, replacementId: conceptId }),
|
|
6938
7165
|
fromArgv: (argv, path) => ({
|
|
6939
7166
|
bundlePath: path,
|
|
6940
7167
|
conceptId: argv[1],
|
|
@@ -6948,7 +7175,7 @@ var supersedeCommand = define({
|
|
|
6948
7175
|
});
|
|
6949
7176
|
|
|
6950
7177
|
// src/commands/sweep.ts
|
|
6951
|
-
var
|
|
7178
|
+
var import_zod36 = require("zod");
|
|
6952
7179
|
var TERMINAL = [
|
|
6953
7180
|
"resolved",
|
|
6954
7181
|
"rejected",
|
|
@@ -6959,15 +7186,15 @@ var sweepCommand = define({
|
|
|
6959
7186
|
tool: "kb_sweep",
|
|
6960
7187
|
usage: "sweep --tag <tag> --terminal [--dry-run]",
|
|
6961
7188
|
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.",
|
|
6962
|
-
input:
|
|
7189
|
+
input: import_zod36.z.object({
|
|
6963
7190
|
bundlePath,
|
|
6964
|
-
tag:
|
|
6965
|
-
terminal:
|
|
7191
|
+
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."),
|
|
7192
|
+
terminal: import_zod36.z.literal(true, {
|
|
6966
7193
|
error: "sweep needs --terminal: it deletes only settled records"
|
|
6967
7194
|
}).describe(
|
|
6968
7195
|
"Required. Names the only scope sweep deletes: resolved, rejected and superseded records."
|
|
6969
7196
|
),
|
|
6970
|
-
dryRun:
|
|
7197
|
+
dryRun: import_zod36.z.boolean().optional().describe("Report what would go, and delete nothing.")
|
|
6971
7198
|
}),
|
|
6972
7199
|
fromArgv: (argv, path) => ({
|
|
6973
7200
|
bundlePath: path,
|
|
@@ -7084,16 +7311,16 @@ function renderSweep(result) {
|
|
|
7084
7311
|
}
|
|
7085
7312
|
|
|
7086
7313
|
// src/commands/sync-instructions.ts
|
|
7087
|
-
var
|
|
7314
|
+
var import_zod37 = require("zod");
|
|
7088
7315
|
var syncInstructionsCommand = define({
|
|
7089
7316
|
name: "sync-instructions",
|
|
7090
7317
|
usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
|
|
7091
7318
|
description: "CLI-only: plant the kb_context block between sentinel comments in AGENTS.md or CLAUDE.md, idempotently.",
|
|
7092
|
-
input:
|
|
7093
|
-
file:
|
|
7094
|
-
budgetTokens:
|
|
7095
|
-
fullUnderTokens:
|
|
7096
|
-
profile:
|
|
7319
|
+
input: import_zod37.z.object({
|
|
7320
|
+
file: import_zod37.z.string().min(1).describe("The instruction file to edit in place."),
|
|
7321
|
+
budgetTokens: import_zod37.z.number().int().positive().optional(),
|
|
7322
|
+
fullUnderTokens: import_zod37.z.number().int().positive().optional(),
|
|
7323
|
+
profile: import_zod37.z.string().optional()
|
|
7097
7324
|
}),
|
|
7098
7325
|
fromArgv: (argv) => {
|
|
7099
7326
|
const budget = argvFlag(argv, "--budget");
|
|
@@ -7119,7 +7346,7 @@ var syncInstructionsCommand = define({
|
|
|
7119
7346
|
});
|
|
7120
7347
|
|
|
7121
7348
|
// src/commands/trace.ts
|
|
7122
|
-
var
|
|
7349
|
+
var import_zod38 = require("zod");
|
|
7123
7350
|
|
|
7124
7351
|
// src/trace.ts
|
|
7125
7352
|
var TRACE_EDGES = [
|
|
@@ -7175,11 +7402,11 @@ var traceCommand = define({
|
|
|
7175
7402
|
tool: "kb_trace",
|
|
7176
7403
|
usage: "trace <concept-id> [edges...]",
|
|
7177
7404
|
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".',
|
|
7178
|
-
input:
|
|
7405
|
+
input: import_zod38.z.object({
|
|
7179
7406
|
bundlePath,
|
|
7180
7407
|
conceptId,
|
|
7181
|
-
edges:
|
|
7182
|
-
depth:
|
|
7408
|
+
edges: import_zod38.z.array(import_zod38.z.enum(TRACE_EDGES)).optional(),
|
|
7409
|
+
depth: import_zod38.z.number().int().positive().optional()
|
|
7183
7410
|
}),
|
|
7184
7411
|
fromArgv: (argv, path) => ({
|
|
7185
7412
|
bundlePath: path,
|
|
@@ -7201,37 +7428,37 @@ var traceCommand = define({
|
|
|
7201
7428
|
});
|
|
7202
7429
|
|
|
7203
7430
|
// src/commands/types.ts
|
|
7204
|
-
var
|
|
7431
|
+
var import_zod39 = require("zod");
|
|
7205
7432
|
var typesCommand = define({
|
|
7206
7433
|
name: "types",
|
|
7207
7434
|
tool: "kb_types",
|
|
7208
7435
|
usage: "types",
|
|
7209
7436
|
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.",
|
|
7210
|
-
input:
|
|
7437
|
+
input: import_zod39.z.object({}),
|
|
7211
7438
|
fromArgv: () => ({}),
|
|
7212
7439
|
run: () => Promise.resolve(RECORD_TYPES)
|
|
7213
7440
|
});
|
|
7214
7441
|
|
|
7215
7442
|
// src/commands/unpin.ts
|
|
7216
|
-
var
|
|
7443
|
+
var import_zod40 = require("zod");
|
|
7217
7444
|
var unpinCommand = define({
|
|
7218
7445
|
name: "unpin",
|
|
7219
7446
|
tool: "kb_unpin",
|
|
7220
7447
|
usage: "unpin [bundle-path]",
|
|
7221
7448
|
description: "Remove a base from every manifest layer that holds it. Reports the layers touched.",
|
|
7222
|
-
input:
|
|
7449
|
+
input: import_zod40.z.object({ bundlePath }),
|
|
7223
7450
|
fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
|
|
7224
7451
|
run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
|
|
7225
7452
|
});
|
|
7226
7453
|
|
|
7227
7454
|
// src/commands/validate.ts
|
|
7228
|
-
var
|
|
7455
|
+
var import_zod41 = require("zod");
|
|
7229
7456
|
var validateCommand = define({
|
|
7230
7457
|
name: "validate",
|
|
7231
7458
|
tool: "kb_validate",
|
|
7232
7459
|
usage: "validate",
|
|
7233
7460
|
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.",
|
|
7234
|
-
input:
|
|
7461
|
+
input: import_zod41.z.object({ bundlePath }),
|
|
7235
7462
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
7236
7463
|
run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
|
|
7237
7464
|
// Warnings never fail the exit code; every other severity does.
|
|
@@ -7241,16 +7468,16 @@ var validateCommand = define({
|
|
|
7241
7468
|
});
|
|
7242
7469
|
|
|
7243
7470
|
// src/commands/verify.ts
|
|
7244
|
-
var
|
|
7471
|
+
var import_zod42 = require("zod");
|
|
7245
7472
|
var verifyCommand = define({
|
|
7246
7473
|
name: "verify",
|
|
7247
7474
|
tool: "kb_verify",
|
|
7248
7475
|
usage: "verify <concept-id> --note <text>",
|
|
7249
7476
|
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.",
|
|
7250
|
-
input:
|
|
7477
|
+
input: import_zod42.z.object({
|
|
7251
7478
|
bundlePath,
|
|
7252
7479
|
conceptId,
|
|
7253
|
-
note:
|
|
7480
|
+
note: import_zod42.z.string().refine((s) => s.trim().length > 0, {
|
|
7254
7481
|
message: "note must say what the check found"
|
|
7255
7482
|
})
|
|
7256
7483
|
}),
|
|
@@ -7270,15 +7497,15 @@ var verifyCommand = define({
|
|
|
7270
7497
|
});
|
|
7271
7498
|
|
|
7272
7499
|
// src/commands/write.ts
|
|
7273
|
-
var
|
|
7500
|
+
var import_zod43 = require("zod");
|
|
7274
7501
|
var writeCommand = define({
|
|
7275
7502
|
name: "write",
|
|
7276
7503
|
tool: "kb_write",
|
|
7277
7504
|
usage: "write <type> < record.json",
|
|
7278
7505
|
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.",
|
|
7279
|
-
input:
|
|
7506
|
+
input: import_zod43.z.object({
|
|
7280
7507
|
bundlePath,
|
|
7281
|
-
type:
|
|
7508
|
+
type: import_zod43.z.enum(KB_RECORD_TYPES),
|
|
7282
7509
|
input: composeInputSchema
|
|
7283
7510
|
}),
|
|
7284
7511
|
fromArgv: async (argv, path, stdin) => ({
|
|
@@ -7302,13 +7529,13 @@ var writeCommand = define({
|
|
|
7302
7529
|
});
|
|
7303
7530
|
|
|
7304
7531
|
// src/commands/write-decision.ts
|
|
7305
|
-
var
|
|
7532
|
+
var import_zod44 = require("zod");
|
|
7306
7533
|
var writeDecisionCommand = define({
|
|
7307
7534
|
name: "write-decision",
|
|
7308
7535
|
tool: "kb_write_decision",
|
|
7309
7536
|
usage: "write-decision < decision.json",
|
|
7310
7537
|
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.",
|
|
7311
|
-
input:
|
|
7538
|
+
input: import_zod44.z.object({ bundlePath, input: decisionInputSchema }),
|
|
7312
7539
|
fromArgv: async (_argv, path, stdin) => ({
|
|
7313
7540
|
bundlePath: path,
|
|
7314
7541
|
input: JSON.parse(await stdin())
|
|
@@ -7338,6 +7565,7 @@ var KB_COMMANDS = [
|
|
|
7338
7565
|
answerCommand,
|
|
7339
7566
|
verifyCommand,
|
|
7340
7567
|
anchorResolveCommand,
|
|
7568
|
+
anchorSetCommand,
|
|
7341
7569
|
reassessCommand,
|
|
7342
7570
|
promoteCommand,
|
|
7343
7571
|
loadCommand,
|
|
@@ -7773,23 +8001,31 @@ var KbStore = class {
|
|
|
7773
8001
|
);
|
|
7774
8002
|
}
|
|
7775
8003
|
/**
|
|
7776
|
-
* Replaces a record's anchors
|
|
7777
|
-
*
|
|
7778
|
-
*
|
|
7779
|
-
*
|
|
7780
|
-
* stale entries the resolution pass deliberately dropped.
|
|
7781
|
-
*
|
|
7782
|
-
* Through the write schema: this is a write, and a defect a hand-edit put in
|
|
7783
|
-
* the frontmatter must not be published back out under an actor stamp.
|
|
8004
|
+
* Replaces a record's anchors, preserving everything else. An array is the
|
|
8005
|
+
* whole set; a function is a patch and runs inside the mutation, against
|
|
8006
|
+
* the anchors the record holds then — see
|
|
8007
|
+
* `decision.anchor-update-patch-inside-mutation`.
|
|
7784
8008
|
*/
|
|
7785
8009
|
async updateAnchors(bundlePath2, conceptId2, anchors, actor = "unknown") {
|
|
7786
8010
|
assertActor(actor);
|
|
7787
|
-
|
|
8011
|
+
let entry = {
|
|
8012
|
+
operation: "anchor-resolve",
|
|
8013
|
+
by: actor
|
|
8014
|
+
};
|
|
7788
8015
|
return this.mutate(
|
|
7789
8016
|
bundlePath2,
|
|
7790
8017
|
conceptId2,
|
|
7791
|
-
(frontmatter) =>
|
|
7792
|
-
|
|
8018
|
+
(frontmatter) => {
|
|
8019
|
+
const write = typeof anchors === "function" ? anchors(frontmatter.strauss_anchors ?? []) : { anchors };
|
|
8020
|
+
if (write.log) entry = { ...write.log, by: actor };
|
|
8021
|
+
return {
|
|
8022
|
+
...frontmatter,
|
|
8023
|
+
strauss_anchors: write.anchors.map(
|
|
8024
|
+
(anchor) => kbAnchorWriteSchema.parse(anchor)
|
|
8025
|
+
)
|
|
8026
|
+
};
|
|
8027
|
+
},
|
|
8028
|
+
() => entry
|
|
7793
8029
|
);
|
|
7794
8030
|
}
|
|
7795
8031
|
/**
|
|
@@ -8243,7 +8479,10 @@ ${answer}
|
|
|
8243
8479
|
throw new KbWriteConflictError(conceptId2);
|
|
8244
8480
|
}
|
|
8245
8481
|
await this.publish(target, contents, true, conceptId2);
|
|
8246
|
-
await this.record(this.root(bundlePath2), {
|
|
8482
|
+
await this.record(this.root(bundlePath2), {
|
|
8483
|
+
...typeof entry === "function" ? entry() : entry,
|
|
8484
|
+
conceptId: conceptId2
|
|
8485
|
+
});
|
|
8247
8486
|
return { conceptId: conceptId2, frontmatter, body };
|
|
8248
8487
|
}
|
|
8249
8488
|
/**
|
|
@@ -8459,7 +8698,7 @@ function assertActor(actor, { named = false } = {}) {
|
|
|
8459
8698
|
}
|
|
8460
8699
|
|
|
8461
8700
|
// src/version.ts
|
|
8462
|
-
var VERSION = true ? "0.1.
|
|
8701
|
+
var VERSION = true ? "0.1.22" : "0.0.0-dev";
|
|
8463
8702
|
|
|
8464
8703
|
// src/cli.ts
|
|
8465
8704
|
async function runKbCli(argv) {
|