@saasontools/strauss-kb 0.1.5 → 0.1.6

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/dist/index.d.cts CHANGED
@@ -31,6 +31,17 @@ declare const kbActorStampSchema: z.ZodObject<{
31
31
  by: z.ZodString;
32
32
  at: z.ZodString;
33
33
  }, z.core.$loose>;
34
+ /**
35
+ * A `verified[]` event as this package writes one: the actor stamp plus a
36
+ * required note saying what the check found. Write-side only — the frontmatter
37
+ * keeps reading `verified` with `kbActorStampSchema`, because OKF-native
38
+ * entries carry no note and a consumer must not reject conformant records.
39
+ */
40
+ declare const kbVerifiedEventSchema: z.ZodObject<{
41
+ by: z.ZodString;
42
+ at: z.ZodString;
43
+ note: z.ZodString;
44
+ }, z.core.$loose>;
34
45
  /**
35
46
  * Where a record attaches in the code.
36
47
  *
@@ -124,6 +135,7 @@ declare const kbRecordFrontmatterSchema: z.ZodObject<{
124
135
  }, z.core.$loose>;
125
136
  type KbSource = z.infer<typeof kbSourceSchema>;
126
137
  type KbActorStamp = z.infer<typeof kbActorStampSchema>;
138
+ type KbVerifiedEvent = z.infer<typeof kbVerifiedEventSchema>;
127
139
  type KbAnchor = z.infer<typeof kbAnchorSchema>;
128
140
  type KbRecordFrontmatter = z.infer<typeof kbRecordFrontmatterSchema>;
129
141
  type KbRecord = {
@@ -386,6 +398,18 @@ declare class KbStore {
386
398
  * timeouts.
387
399
  */
388
400
  setStatus(bundlePath: string, conceptId: string, status: KbRecordStatus, actor?: string): Promise<KbRecord>;
401
+ /**
402
+ * Appends one `verified[]` event: who checked the record, when, and what the
403
+ * check found. Append-only — prior events are history, and are spread into
404
+ * the new array untouched rather than reshaped through the write schema.
405
+ *
406
+ * A record's generator cannot verify its own record unless the actor is
407
+ * human: the generator re-reading its own output is not an independent
408
+ * check. The rule runs before the mutation so a refusal never publishes,
409
+ * and the refusal is logged under its own operation name — `mutate` only
410
+ * logs what it publishes.
411
+ */
412
+ verify(bundlePath: string, conceptId: string, note: string, actor?: string, at?: string): Promise<KbRecord>;
389
413
  /**
390
414
  * Marks `conceptId` superseded by `replacementId`, and links both directions.
391
415
  *
@@ -522,6 +546,7 @@ declare enum ErrorTypes {
522
546
  KbRecordAlreadyExists = "KbRecordAlreadyExists",
523
547
  KbInvalidConceptId = "KbInvalidConceptId",
524
548
  KbRecordNotFound = "KbRecordNotFound",
549
+ KbSelfVerification = "KbSelfVerification",
525
550
  KbWriteConflict = "KbWriteConflict"
526
551
  }
527
552
  type ErrorDetails = Record<string, string | boolean | number | string[] | boolean[] | number[]>;
@@ -564,6 +589,13 @@ declare class KbWriteConflictError extends BaseError {
564
589
  readonly conceptId: string;
565
590
  constructor(conceptId: string);
566
591
  }
592
+ /** A generator confirming its own record adds no independent check. */
593
+ declare class KbSelfVerificationError extends BaseError {
594
+ readonly conceptId: string;
595
+ readonly actor: string;
596
+ readonly generatedBy: string;
597
+ constructor(conceptId: string, actor: string, generatedBy: string);
598
+ }
567
599
  declare class KbInvalidConceptIdError extends BaseError {
568
600
  constructor(message: string, details: Record<string, string>);
569
601
  }
@@ -1265,4 +1297,4 @@ declare function parseMarkdownWithFrontmatter<S extends z.ZodType>(text: string,
1265
1297
  frontmatter: ReturnType<S["safeParse"]>;
1266
1298
  };
1267
1299
 
1268
- export { BaseError, CONTEXT_BEGIN, CONTEXT_END, CONTEXT_PROFILES, type ComposeInput, type ComposedRecord, DECISION_TYPE, type DecisionInput, type DiffFile, type DiffHunk, type DiffMatch, type ErrorDetails, type ErrorProps, ErrorTypes, Fault, INDEX_FILE, KB_COMMANDS, KB_COMMANDS_BY_NAME, KB_CONCEPT_ID_PATTERN, KB_CONFIDENCES, KB_DIR, KB_MATERIALITIES, KB_RECORD_STATUSES, KB_RECORD_TYPES, KB_SLUG_PATTERN, type KbActorStamp, type KbAdjudicated, type KbAnchor, KbBaseFrozenError, type KbCommand, type KbCommandContext, type KbContextBudgets, type KbContextOptions, type KbContextResult, KbInvalidConceptIdError, type KbLoadResult, type KbLogEntry, type KbLogReadResult, type KbLogger, type KbMergedPin, type KbMergedPins, type KbPin, type KbPinLayer, type KbPinOptions, type KbPinResult, type KbPinStatus, KbPinsMalformedError, type KbPinsManifest, type KbRecord, KbRecordAlreadyExistsError, type KbRecordFrontmatter, KbRecordNotFoundError, type KbRecordStatus, type KbRecordType, type KbRecordTypeSpec, type KbSearchLogger, type KbSource, type KbStanding, KbStore, type KbSupersededStub, type KbSyncResult, type KbTraceEdge, type KbTraceOptions, type KbTraceStep, type KbValidationProblem, type KbWarning, KbWriteConflictError, type KbWriteInput, LOG_FILE, type MatchOptions, NO_DECISION_SLUG, PINS_FILE, PINS_LOCAL_FILE, PIN_LAYERS, type QmdModule, RECORD_TYPES, SEARCH_INDEX_FILE, type SearchHit, type SearchOptions, type SymbolRange, TRACE_EDGES, adjudicate, assertBaseNotFrozen, buildContext, composeDecisionRecord, composeInputSchema, composeNoDecisionRecord, composeRecord, contextProfileBudgets, createKbMcpServer, decisionInputSchema, indexIsStale, isKbRecordType, isNoDecisionRecord, kbActorStampSchema, kbAnchorSchema, kbConceptIdSchema, kbJsonSchemas, kbLogEntrySchema, kbRecordFrontmatterSchema, kbSourceSchema, listPins, loadQmd, matchToDiff, mergedContextBudgets, parseLog, parseMarkdownWithFrontmatter, pinBase, readMergedPins, readPinsLayer, renderIndex, renderIndexLine, renderLogEntry, resolveHeads, resolveHits, resolvePinPath, runKbCli, runKbMcpServer, searchBase, selectDecisions, splitMarkdownFrontmatter, stringifyMarkdownWithFrontmatter, syncInstructions, toHookJson, trace, unpinBase, validateBundle };
1300
+ export { BaseError, CONTEXT_BEGIN, CONTEXT_END, CONTEXT_PROFILES, type ComposeInput, type ComposedRecord, DECISION_TYPE, type DecisionInput, type DiffFile, type DiffHunk, type DiffMatch, type ErrorDetails, type ErrorProps, ErrorTypes, Fault, INDEX_FILE, KB_COMMANDS, KB_COMMANDS_BY_NAME, KB_CONCEPT_ID_PATTERN, KB_CONFIDENCES, KB_DIR, KB_MATERIALITIES, KB_RECORD_STATUSES, KB_RECORD_TYPES, KB_SLUG_PATTERN, type KbActorStamp, type KbAdjudicated, type KbAnchor, KbBaseFrozenError, type KbCommand, type KbCommandContext, type KbContextBudgets, type KbContextOptions, type KbContextResult, KbInvalidConceptIdError, type KbLoadResult, type KbLogEntry, type KbLogReadResult, type KbLogger, type KbMergedPin, type KbMergedPins, type KbPin, type KbPinLayer, type KbPinOptions, type KbPinResult, type KbPinStatus, KbPinsMalformedError, type KbPinsManifest, type KbRecord, KbRecordAlreadyExistsError, type KbRecordFrontmatter, KbRecordNotFoundError, type KbRecordStatus, type KbRecordType, type KbRecordTypeSpec, type KbSearchLogger, KbSelfVerificationError, type KbSource, type KbStanding, KbStore, type KbSupersededStub, type KbSyncResult, type KbTraceEdge, type KbTraceOptions, type KbTraceStep, type KbValidationProblem, type KbVerifiedEvent, type KbWarning, KbWriteConflictError, type KbWriteInput, LOG_FILE, type MatchOptions, NO_DECISION_SLUG, PINS_FILE, PINS_LOCAL_FILE, PIN_LAYERS, type QmdModule, RECORD_TYPES, SEARCH_INDEX_FILE, type SearchHit, type SearchOptions, type SymbolRange, TRACE_EDGES, adjudicate, assertBaseNotFrozen, buildContext, composeDecisionRecord, composeInputSchema, composeNoDecisionRecord, composeRecord, contextProfileBudgets, createKbMcpServer, decisionInputSchema, indexIsStale, isKbRecordType, isNoDecisionRecord, kbActorStampSchema, kbAnchorSchema, kbConceptIdSchema, kbJsonSchemas, kbLogEntrySchema, kbRecordFrontmatterSchema, kbSourceSchema, kbVerifiedEventSchema, listPins, loadQmd, matchToDiff, mergedContextBudgets, parseLog, parseMarkdownWithFrontmatter, pinBase, readMergedPins, readPinsLayer, renderIndex, renderIndexLine, renderLogEntry, resolveHeads, resolveHits, resolvePinPath, runKbCli, runKbMcpServer, searchBase, selectDecisions, splitMarkdownFrontmatter, stringifyMarkdownWithFrontmatter, syncInstructions, toHookJson, trace, unpinBase, validateBundle };
package/dist/index.d.ts CHANGED
@@ -31,6 +31,17 @@ declare const kbActorStampSchema: z.ZodObject<{
31
31
  by: z.ZodString;
32
32
  at: z.ZodString;
33
33
  }, z.core.$loose>;
34
+ /**
35
+ * A `verified[]` event as this package writes one: the actor stamp plus a
36
+ * required note saying what the check found. Write-side only — the frontmatter
37
+ * keeps reading `verified` with `kbActorStampSchema`, because OKF-native
38
+ * entries carry no note and a consumer must not reject conformant records.
39
+ */
40
+ declare const kbVerifiedEventSchema: z.ZodObject<{
41
+ by: z.ZodString;
42
+ at: z.ZodString;
43
+ note: z.ZodString;
44
+ }, z.core.$loose>;
34
45
  /**
35
46
  * Where a record attaches in the code.
36
47
  *
@@ -124,6 +135,7 @@ declare const kbRecordFrontmatterSchema: z.ZodObject<{
124
135
  }, z.core.$loose>;
125
136
  type KbSource = z.infer<typeof kbSourceSchema>;
126
137
  type KbActorStamp = z.infer<typeof kbActorStampSchema>;
138
+ type KbVerifiedEvent = z.infer<typeof kbVerifiedEventSchema>;
127
139
  type KbAnchor = z.infer<typeof kbAnchorSchema>;
128
140
  type KbRecordFrontmatter = z.infer<typeof kbRecordFrontmatterSchema>;
129
141
  type KbRecord = {
@@ -386,6 +398,18 @@ declare class KbStore {
386
398
  * timeouts.
387
399
  */
388
400
  setStatus(bundlePath: string, conceptId: string, status: KbRecordStatus, actor?: string): Promise<KbRecord>;
401
+ /**
402
+ * Appends one `verified[]` event: who checked the record, when, and what the
403
+ * check found. Append-only — prior events are history, and are spread into
404
+ * the new array untouched rather than reshaped through the write schema.
405
+ *
406
+ * A record's generator cannot verify its own record unless the actor is
407
+ * human: the generator re-reading its own output is not an independent
408
+ * check. The rule runs before the mutation so a refusal never publishes,
409
+ * and the refusal is logged under its own operation name — `mutate` only
410
+ * logs what it publishes.
411
+ */
412
+ verify(bundlePath: string, conceptId: string, note: string, actor?: string, at?: string): Promise<KbRecord>;
389
413
  /**
390
414
  * Marks `conceptId` superseded by `replacementId`, and links both directions.
391
415
  *
@@ -522,6 +546,7 @@ declare enum ErrorTypes {
522
546
  KbRecordAlreadyExists = "KbRecordAlreadyExists",
523
547
  KbInvalidConceptId = "KbInvalidConceptId",
524
548
  KbRecordNotFound = "KbRecordNotFound",
549
+ KbSelfVerification = "KbSelfVerification",
525
550
  KbWriteConflict = "KbWriteConflict"
526
551
  }
527
552
  type ErrorDetails = Record<string, string | boolean | number | string[] | boolean[] | number[]>;
@@ -564,6 +589,13 @@ declare class KbWriteConflictError extends BaseError {
564
589
  readonly conceptId: string;
565
590
  constructor(conceptId: string);
566
591
  }
592
+ /** A generator confirming its own record adds no independent check. */
593
+ declare class KbSelfVerificationError extends BaseError {
594
+ readonly conceptId: string;
595
+ readonly actor: string;
596
+ readonly generatedBy: string;
597
+ constructor(conceptId: string, actor: string, generatedBy: string);
598
+ }
567
599
  declare class KbInvalidConceptIdError extends BaseError {
568
600
  constructor(message: string, details: Record<string, string>);
569
601
  }
@@ -1265,4 +1297,4 @@ declare function parseMarkdownWithFrontmatter<S extends z.ZodType>(text: string,
1265
1297
  frontmatter: ReturnType<S["safeParse"]>;
1266
1298
  };
1267
1299
 
1268
- export { BaseError, CONTEXT_BEGIN, CONTEXT_END, CONTEXT_PROFILES, type ComposeInput, type ComposedRecord, DECISION_TYPE, type DecisionInput, type DiffFile, type DiffHunk, type DiffMatch, type ErrorDetails, type ErrorProps, ErrorTypes, Fault, INDEX_FILE, KB_COMMANDS, KB_COMMANDS_BY_NAME, KB_CONCEPT_ID_PATTERN, KB_CONFIDENCES, KB_DIR, KB_MATERIALITIES, KB_RECORD_STATUSES, KB_RECORD_TYPES, KB_SLUG_PATTERN, type KbActorStamp, type KbAdjudicated, type KbAnchor, KbBaseFrozenError, type KbCommand, type KbCommandContext, type KbContextBudgets, type KbContextOptions, type KbContextResult, KbInvalidConceptIdError, type KbLoadResult, type KbLogEntry, type KbLogReadResult, type KbLogger, type KbMergedPin, type KbMergedPins, type KbPin, type KbPinLayer, type KbPinOptions, type KbPinResult, type KbPinStatus, KbPinsMalformedError, type KbPinsManifest, type KbRecord, KbRecordAlreadyExistsError, type KbRecordFrontmatter, KbRecordNotFoundError, type KbRecordStatus, type KbRecordType, type KbRecordTypeSpec, type KbSearchLogger, type KbSource, type KbStanding, KbStore, type KbSupersededStub, type KbSyncResult, type KbTraceEdge, type KbTraceOptions, type KbTraceStep, type KbValidationProblem, type KbWarning, KbWriteConflictError, type KbWriteInput, LOG_FILE, type MatchOptions, NO_DECISION_SLUG, PINS_FILE, PINS_LOCAL_FILE, PIN_LAYERS, type QmdModule, RECORD_TYPES, SEARCH_INDEX_FILE, type SearchHit, type SearchOptions, type SymbolRange, TRACE_EDGES, adjudicate, assertBaseNotFrozen, buildContext, composeDecisionRecord, composeInputSchema, composeNoDecisionRecord, composeRecord, contextProfileBudgets, createKbMcpServer, decisionInputSchema, indexIsStale, isKbRecordType, isNoDecisionRecord, kbActorStampSchema, kbAnchorSchema, kbConceptIdSchema, kbJsonSchemas, kbLogEntrySchema, kbRecordFrontmatterSchema, kbSourceSchema, listPins, loadQmd, matchToDiff, mergedContextBudgets, parseLog, parseMarkdownWithFrontmatter, pinBase, readMergedPins, readPinsLayer, renderIndex, renderIndexLine, renderLogEntry, resolveHeads, resolveHits, resolvePinPath, runKbCli, runKbMcpServer, searchBase, selectDecisions, splitMarkdownFrontmatter, stringifyMarkdownWithFrontmatter, syncInstructions, toHookJson, trace, unpinBase, validateBundle };
1300
+ export { BaseError, CONTEXT_BEGIN, CONTEXT_END, CONTEXT_PROFILES, type ComposeInput, type ComposedRecord, DECISION_TYPE, type DecisionInput, type DiffFile, type DiffHunk, type DiffMatch, type ErrorDetails, type ErrorProps, ErrorTypes, Fault, INDEX_FILE, KB_COMMANDS, KB_COMMANDS_BY_NAME, KB_CONCEPT_ID_PATTERN, KB_CONFIDENCES, KB_DIR, KB_MATERIALITIES, KB_RECORD_STATUSES, KB_RECORD_TYPES, KB_SLUG_PATTERN, type KbActorStamp, type KbAdjudicated, type KbAnchor, KbBaseFrozenError, type KbCommand, type KbCommandContext, type KbContextBudgets, type KbContextOptions, type KbContextResult, KbInvalidConceptIdError, type KbLoadResult, type KbLogEntry, type KbLogReadResult, type KbLogger, type KbMergedPin, type KbMergedPins, type KbPin, type KbPinLayer, type KbPinOptions, type KbPinResult, type KbPinStatus, KbPinsMalformedError, type KbPinsManifest, type KbRecord, KbRecordAlreadyExistsError, type KbRecordFrontmatter, KbRecordNotFoundError, type KbRecordStatus, type KbRecordType, type KbRecordTypeSpec, type KbSearchLogger, KbSelfVerificationError, type KbSource, type KbStanding, KbStore, type KbSupersededStub, type KbSyncResult, type KbTraceEdge, type KbTraceOptions, type KbTraceStep, type KbValidationProblem, type KbVerifiedEvent, type KbWarning, KbWriteConflictError, type KbWriteInput, LOG_FILE, type MatchOptions, NO_DECISION_SLUG, PINS_FILE, PINS_LOCAL_FILE, PIN_LAYERS, type QmdModule, RECORD_TYPES, SEARCH_INDEX_FILE, type SearchHit, type SearchOptions, type SymbolRange, TRACE_EDGES, adjudicate, assertBaseNotFrozen, buildContext, composeDecisionRecord, composeInputSchema, composeNoDecisionRecord, composeRecord, contextProfileBudgets, createKbMcpServer, decisionInputSchema, indexIsStale, isKbRecordType, isNoDecisionRecord, kbActorStampSchema, kbAnchorSchema, kbConceptIdSchema, kbJsonSchemas, kbLogEntrySchema, kbRecordFrontmatterSchema, kbSourceSchema, kbVerifiedEventSchema, listPins, loadQmd, matchToDiff, mergedContextBudgets, parseLog, parseMarkdownWithFrontmatter, pinBase, readMergedPins, readPinsLayer, renderIndex, renderIndexLine, renderLogEntry, resolveHeads, resolveHits, resolvePinPath, runKbCli, runKbMcpServer, searchBase, selectDecisions, splitMarkdownFrontmatter, stringifyMarkdownWithFrontmatter, syncInstructions, toHookJson, trace, unpinBase, validateBundle };
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  runKbCli
3
- } from "./chunk-KQMGKSPZ.js";
3
+ } from "./chunk-V7TRZ2ER.js";
4
4
  import {
5
5
  createKbMcpServer,
6
6
  runKbMcpServer
7
- } from "./chunk-VOJ6D6OX.js";
7
+ } from "./chunk-BVF7X5VO.js";
8
8
  import {
9
9
  BaseError,
10
10
  CONTEXT_BEGIN,
@@ -28,6 +28,7 @@ import {
28
28
  KbPinsMalformedError,
29
29
  KbRecordAlreadyExistsError,
30
30
  KbRecordNotFoundError,
31
+ KbSelfVerificationError,
31
32
  KbStore,
32
33
  KbWriteConflictError,
33
34
  LOG_FILE,
@@ -57,6 +58,7 @@ import {
57
58
  kbLogEntrySchema,
58
59
  kbRecordFrontmatterSchema,
59
60
  kbSourceSchema,
61
+ kbVerifiedEventSchema,
60
62
  listPins,
61
63
  loadQmd,
62
64
  mergedContextBudgets,
@@ -80,7 +82,7 @@ import {
80
82
  trace,
81
83
  unpinBase,
82
84
  validateBundle
83
- } from "./chunk-FZIMFPGR.js";
85
+ } from "./chunk-PNSRTKYN.js";
84
86
 
85
87
  // src/match-diff.ts
86
88
  function matchToDiff(files, records, options = {}) {
@@ -184,6 +186,7 @@ export {
184
186
  KbPinsMalformedError,
185
187
  KbRecordAlreadyExistsError,
186
188
  KbRecordNotFoundError,
189
+ KbSelfVerificationError,
187
190
  KbStore,
188
191
  KbWriteConflictError,
189
192
  LOG_FILE,
@@ -214,6 +217,7 @@ export {
214
217
  kbLogEntrySchema,
215
218
  kbRecordFrontmatterSchema,
216
219
  kbSourceSchema,
220
+ kbVerifiedEventSchema,
217
221
  listPins,
218
222
  loadQmd,
219
223
  matchToDiff,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/match-diff.ts"],"sourcesContent":["import { adjudicate, type KbAdjudicated } from \"./adjudicate.js\";\nimport type { KbAnchor, KbRecord } from \"./kb-record.schema.js\";\n\n/**\n * Which records apply to which part of a change.\n *\n * Takes a structural description of a diff rather than a patch, so this package\n * carries no diff parser: callers already have one, and a knowledge base has no\n * business preferring a particular flavour of unified diff.\n *\n * Deterministic on purpose. Every step here is mechanical — the one judgment,\n * whether a matched record is worth showing a reviewer, is deliberately absent.\n * A model placed here would sit between the reviewer and their diff on every\n * review, to answer a question nobody has yet shown needs asking.\n *\n * Distinct from `load()`, which hands a reader the whole base. That answers\n * \"does anything address this question\"; this answers \"what is attached to this\n * code\", and an anchor is the author's own statement rather than an inference\n * from one. A reader guessing which record relates to a hunk would be guessing\n * at something already written down — and a diff has dozens of hunks, which is\n * dozens of reader calls against microseconds of matching. Where they compose:\n * this narrows a hunk to a few records, and a reader asked to explain them gets\n * those, not the base.\n */\nexport type DiffHunk = {\n /** 1-based, inclusive, in the file's post-change line numbering. */\n startLine: number;\n endLine: number;\n};\n\nexport type DiffFile = {\n /** Repo-relative, matching how anchors are written. */\n filePath: string;\n hunks: DiffHunk[];\n};\n\n/**\n * A symbol resolved to lines. Supplied by whatever the caller uses to index\n * symbols; absence is tolerated — see `place()`.\n */\nexport type SymbolRange = {\n file: string;\n symbol: string;\n startLine: number;\n endLine: number;\n};\n\nexport type DiffMatch = {\n filePath: string;\n hunk: DiffHunk;\n /** Current records first — what still holds should be read before what does not. */\n records: KbAdjudicated[];\n /**\n * `symbol` when every record here was placed by a resolved symbol range,\n * `file` when at least one fell back to the whole file. Reported rather than\n * hidden: a caller showing a file-level match as though it were pinned to\n * these lines is claiming a precision it does not have.\n */\n precision: \"symbol\" | \"file\";\n};\n\nexport type MatchOptions = {\n /** Without these, symbol anchors degrade to file level rather than vanishing. */\n symbolRanges?: SymbolRange[];\n now?: Date;\n};\n\nexport function matchToDiff(\n files: DiffFile[],\n records: KbRecord[],\n options: MatchOptions = {},\n): DiffMatch[] {\n const ranges = indexRanges(options.symbolRanges ?? []);\n const anchored = records.filter(\n (record) => (record.frontmatter.strauss_anchors ?? []).length > 0,\n );\n const matches: DiffMatch[] = [];\n\n for (const file of files) {\n const candidates = anchored\n .map((record) => ({\n record,\n anchors: (record.frontmatter.strauss_anchors ?? []).filter(\n (anchor) => normalize(anchor.file) === normalize(file.filePath),\n ),\n }))\n .filter(({ anchors }) => anchors.length > 0);\n if (!candidates.length) continue;\n\n for (const hunk of file.hunks) {\n const hits: KbRecord[] = [];\n let precision: DiffMatch[\"precision\"] = \"symbol\";\n\n for (const { record, anchors } of candidates) {\n const placement = place(anchors, file.filePath, hunk, ranges);\n if (placement === \"miss\") continue;\n if (placement === \"file\") precision = \"file\";\n hits.push(record);\n }\n\n if (!hits.length) continue;\n matches.push({\n filePath: file.filePath,\n hunk,\n records: order(adjudicate(hits, records, options.now)),\n precision,\n });\n }\n }\n\n return matches;\n}\n\n/**\n * Whether any of a record's anchors puts it on this hunk.\n *\n * An anchor naming only a file is about the whole file, so it lands on every\n * hunk in it. One naming a symbol lands only where that symbol's lines overlap\n * — unless nothing resolved the symbol, in which case it falls back to the file\n * rather than disappearing. A record silently absent because a resolver was\n * unavailable is worse than one shown imprecisely and labelled as such.\n */\nfunction place(\n anchors: KbAnchor[],\n filePath: string,\n hunk: DiffHunk,\n ranges: Map<string, SymbolRange[]>,\n): \"symbol\" | \"file\" | \"miss\" {\n let fallback: \"file\" | \"miss\" = \"miss\";\n\n for (const anchor of anchors) {\n if (!anchor.symbol) return \"file\";\n\n const resolved = ranges.get(key(filePath, anchor.symbol));\n if (!resolved?.length) {\n fallback = \"file\";\n continue;\n }\n if (resolved.some((range) => overlaps(range, hunk))) return \"symbol\";\n }\n\n return fallback;\n}\n\nfunction overlaps(range: SymbolRange, hunk: DiffHunk): boolean {\n return range.startLine <= hunk.endLine && hunk.startLine <= range.endLine;\n}\n\n/** Current before superseded, then oldest first, so an arc reads in order. */\nfunction order(records: KbAdjudicated[]): KbAdjudicated[] {\n const rank: Record<string, number> = {\n current: 0,\n unsettled: 1,\n open: 2,\n superseded: 3,\n rejected: 4,\n };\n return [...records].sort(\n (left, right) =>\n (rank[left.standing] ?? 9) - (rank[right.standing] ?? 9) ||\n (left.record.frontmatter.generated?.at ?? \"\").localeCompare(\n right.record.frontmatter.generated?.at ?? \"\",\n ),\n );\n}\n\nfunction indexRanges(ranges: SymbolRange[]): Map<string, SymbolRange[]> {\n const byKey = new Map<string, SymbolRange[]>();\n for (const range of ranges) {\n const id = key(range.file, range.symbol);\n byKey.set(id, [...(byKey.get(id) ?? []), range]);\n }\n return byKey;\n}\n\nfunction key(file: string, symbol: string): string {\n return `${normalize(file)}#${symbol}`;\n}\n\n/** Anchors are written by hand often enough that `./` shows up. */\nfunction normalize(path: string): string {\n return path.replace(/^\\.\\//, \"\");\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmEO,SAAS,YACd,OACA,SACA,UAAwB,CAAC,GACZ;AACb,QAAM,SAAS,YAAY,QAAQ,gBAAgB,CAAC,CAAC;AACrD,QAAM,WAAW,QAAQ;AAAA,IACvB,CAAC,YAAY,OAAO,YAAY,mBAAmB,CAAC,GAAG,SAAS;AAAA,EAClE;AACA,QAAM,UAAuB,CAAC;AAE9B,aAAW,QAAQ,OAAO;AACxB,UAAM,aAAa,SAChB,IAAI,CAAC,YAAY;AAAA,MAChB;AAAA,MACA,UAAU,OAAO,YAAY,mBAAmB,CAAC,GAAG;AAAA,QAClD,CAAC,WAAW,UAAU,OAAO,IAAI,MAAM,UAAU,KAAK,QAAQ;AAAA,MAChE;AAAA,IACF,EAAE,EACD,OAAO,CAAC,EAAE,QAAQ,MAAM,QAAQ,SAAS,CAAC;AAC7C,QAAI,CAAC,WAAW,OAAQ;AAExB,eAAW,QAAQ,KAAK,OAAO;AAC7B,YAAM,OAAmB,CAAC;AAC1B,UAAI,YAAoC;AAExC,iBAAW,EAAE,QAAQ,QAAQ,KAAK,YAAY;AAC5C,cAAM,YAAY,MAAM,SAAS,KAAK,UAAU,MAAM,MAAM;AAC5D,YAAI,cAAc,OAAQ;AAC1B,YAAI,cAAc,OAAQ,aAAY;AACtC,aAAK,KAAK,MAAM;AAAA,MAClB;AAEA,UAAI,CAAC,KAAK,OAAQ;AAClB,cAAQ,KAAK;AAAA,QACX,UAAU,KAAK;AAAA,QACf;AAAA,QACA,SAAS,MAAM,WAAW,MAAM,SAAS,QAAQ,GAAG,CAAC;AAAA,QACrD;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AAEA,SAAO;AACT;AAWA,SAAS,MACP,SACA,UACA,MACA,QAC4B;AAC5B,MAAI,WAA4B;AAEhC,aAAW,UAAU,SAAS;AAC5B,QAAI,CAAC,OAAO,OAAQ,QAAO;AAE3B,UAAM,WAAW,OAAO,IAAI,IAAI,UAAU,OAAO,MAAM,CAAC;AACxD,QAAI,CAAC,UAAU,QAAQ;AACrB,iBAAW;AACX;AAAA,IACF;AACA,QAAI,SAAS,KAAK,CAAC,UAAU,SAAS,OAAO,IAAI,CAAC,EAAG,QAAO;AAAA,EAC9D;AAEA,SAAO;AACT;AAEA,SAAS,SAAS,OAAoB,MAAyB;AAC7D,SAAO,MAAM,aAAa,KAAK,WAAW,KAAK,aAAa,MAAM;AACpE;AAGA,SAAS,MAAM,SAA2C;AACxD,QAAM,OAA+B;AAAA,IACnC,SAAS;AAAA,IACT,WAAW;AAAA,IACX,MAAM;AAAA,IACN,YAAY;AAAA,IACZ,UAAU;AAAA,EACZ;AACA,SAAO,CAAC,GAAG,OAAO,EAAE;AAAA,IAClB,CAAC,MAAM,WACJ,KAAK,KAAK,QAAQ,KAAK,MAAM,KAAK,MAAM,QAAQ,KAAK,OACrD,KAAK,OAAO,YAAY,WAAW,MAAM,IAAI;AAAA,MAC5C,MAAM,OAAO,YAAY,WAAW,MAAM;AAAA,IAC5C;AAAA,EACJ;AACF;AAEA,SAAS,YAAY,QAAmD;AACtE,QAAM,QAAQ,oBAAI,IAA2B;AAC7C,aAAW,SAAS,QAAQ;AAC1B,UAAM,KAAK,IAAI,MAAM,MAAM,MAAM,MAAM;AACvC,UAAM,IAAI,IAAI,CAAC,GAAI,MAAM,IAAI,EAAE,KAAK,CAAC,GAAI,KAAK,CAAC;AAAA,EACjD;AACA,SAAO;AACT;AAEA,SAAS,IAAI,MAAc,QAAwB;AACjD,SAAO,GAAG,UAAU,IAAI,CAAC,IAAI,MAAM;AACrC;AAGA,SAAS,UAAU,MAAsB;AACvC,SAAO,KAAK,QAAQ,SAAS,EAAE;AACjC;","names":[]}
1
+ {"version":3,"sources":["../src/match-diff.ts"],"sourcesContent":["import { adjudicate, type KbAdjudicated } from \"./adjudicate.js\";\nimport type { KbAnchor, KbRecord } from \"./kb-record.schema.js\";\n\n/**\n * Which records apply to which part of a change.\n *\n * Takes a structural description of a diff rather than a patch, so this package\n * carries no diff parser: callers already have one, and a knowledge base has no\n * business preferring a particular flavour of unified diff.\n *\n * Deterministic on purpose. Every step here is mechanical — the one judgment,\n * whether a matched record is worth showing a reviewer, is deliberately absent.\n * A model placed here would sit between the reviewer and their diff on every\n * review, to answer a question nobody has yet shown needs asking.\n *\n * Distinct from `load()`, which hands a reader the whole base. That answers\n * \"does anything address this question\"; this answers \"what is attached to this\n * code\", and an anchor is the author's own statement rather than an inference\n * from one. A reader guessing which record relates to a hunk would be guessing\n * at something already written down — and a diff has dozens of hunks, which is\n * dozens of reader calls against microseconds of matching. Where they compose:\n * this narrows a hunk to a few records, and a reader asked to explain them gets\n * those, not the base.\n */\nexport type DiffHunk = {\n /** 1-based, inclusive, in the file's post-change line numbering. */\n startLine: number;\n endLine: number;\n};\n\nexport type DiffFile = {\n /** Repo-relative, matching how anchors are written. */\n filePath: string;\n hunks: DiffHunk[];\n};\n\n/**\n * A symbol resolved to lines. Supplied by whatever the caller uses to index\n * symbols; absence is tolerated — see `place()`.\n */\nexport type SymbolRange = {\n file: string;\n symbol: string;\n startLine: number;\n endLine: number;\n};\n\nexport type DiffMatch = {\n filePath: string;\n hunk: DiffHunk;\n /** Current records first — what still holds should be read before what does not. */\n records: KbAdjudicated[];\n /**\n * `symbol` when every record here was placed by a resolved symbol range,\n * `file` when at least one fell back to the whole file. Reported rather than\n * hidden: a caller showing a file-level match as though it were pinned to\n * these lines is claiming a precision it does not have.\n */\n precision: \"symbol\" | \"file\";\n};\n\nexport type MatchOptions = {\n /** Without these, symbol anchors degrade to file level rather than vanishing. */\n symbolRanges?: SymbolRange[];\n now?: Date;\n};\n\nexport function matchToDiff(\n files: DiffFile[],\n records: KbRecord[],\n options: MatchOptions = {},\n): DiffMatch[] {\n const ranges = indexRanges(options.symbolRanges ?? []);\n const anchored = records.filter(\n (record) => (record.frontmatter.strauss_anchors ?? []).length > 0,\n );\n const matches: DiffMatch[] = [];\n\n for (const file of files) {\n const candidates = anchored\n .map((record) => ({\n record,\n anchors: (record.frontmatter.strauss_anchors ?? []).filter(\n (anchor) => normalize(anchor.file) === normalize(file.filePath),\n ),\n }))\n .filter(({ anchors }) => anchors.length > 0);\n if (!candidates.length) continue;\n\n for (const hunk of file.hunks) {\n const hits: KbRecord[] = [];\n let precision: DiffMatch[\"precision\"] = \"symbol\";\n\n for (const { record, anchors } of candidates) {\n const placement = place(anchors, file.filePath, hunk, ranges);\n if (placement === \"miss\") continue;\n if (placement === \"file\") precision = \"file\";\n hits.push(record);\n }\n\n if (!hits.length) continue;\n matches.push({\n filePath: file.filePath,\n hunk,\n records: order(adjudicate(hits, records, options.now)),\n precision,\n });\n }\n }\n\n return matches;\n}\n\n/**\n * Whether any of a record's anchors puts it on this hunk.\n *\n * An anchor naming only a file is about the whole file, so it lands on every\n * hunk in it. One naming a symbol lands only where that symbol's lines overlap\n * — unless nothing resolved the symbol, in which case it falls back to the file\n * rather than disappearing. A record silently absent because a resolver was\n * unavailable is worse than one shown imprecisely and labelled as such.\n */\nfunction place(\n anchors: KbAnchor[],\n filePath: string,\n hunk: DiffHunk,\n ranges: Map<string, SymbolRange[]>,\n): \"symbol\" | \"file\" | \"miss\" {\n let fallback: \"file\" | \"miss\" = \"miss\";\n\n for (const anchor of anchors) {\n if (!anchor.symbol) return \"file\";\n\n const resolved = ranges.get(key(filePath, anchor.symbol));\n if (!resolved?.length) {\n fallback = \"file\";\n continue;\n }\n if (resolved.some((range) => overlaps(range, hunk))) return \"symbol\";\n }\n\n return fallback;\n}\n\nfunction overlaps(range: SymbolRange, hunk: DiffHunk): boolean {\n return range.startLine <= hunk.endLine && hunk.startLine <= range.endLine;\n}\n\n/** Current before superseded, then oldest first, so an arc reads in order. */\nfunction order(records: KbAdjudicated[]): KbAdjudicated[] {\n const rank: Record<string, number> = {\n current: 0,\n unsettled: 1,\n open: 2,\n superseded: 3,\n rejected: 4,\n };\n return [...records].sort(\n (left, right) =>\n (rank[left.standing] ?? 9) - (rank[right.standing] ?? 9) ||\n (left.record.frontmatter.generated?.at ?? \"\").localeCompare(\n right.record.frontmatter.generated?.at ?? \"\",\n ),\n );\n}\n\nfunction indexRanges(ranges: SymbolRange[]): Map<string, SymbolRange[]> {\n const byKey = new Map<string, SymbolRange[]>();\n for (const range of ranges) {\n const id = key(range.file, range.symbol);\n byKey.set(id, [...(byKey.get(id) ?? []), range]);\n }\n return byKey;\n}\n\nfunction key(file: string, symbol: string): string {\n return `${normalize(file)}#${symbol}`;\n}\n\n/** Anchors are written by hand often enough that `./` shows up. */\nfunction normalize(path: string): string {\n return path.replace(/^\\.\\//, \"\");\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmEO,SAAS,YACd,OACA,SACA,UAAwB,CAAC,GACZ;AACb,QAAM,SAAS,YAAY,QAAQ,gBAAgB,CAAC,CAAC;AACrD,QAAM,WAAW,QAAQ;AAAA,IACvB,CAAC,YAAY,OAAO,YAAY,mBAAmB,CAAC,GAAG,SAAS;AAAA,EAClE;AACA,QAAM,UAAuB,CAAC;AAE9B,aAAW,QAAQ,OAAO;AACxB,UAAM,aAAa,SAChB,IAAI,CAAC,YAAY;AAAA,MAChB;AAAA,MACA,UAAU,OAAO,YAAY,mBAAmB,CAAC,GAAG;AAAA,QAClD,CAAC,WAAW,UAAU,OAAO,IAAI,MAAM,UAAU,KAAK,QAAQ;AAAA,MAChE;AAAA,IACF,EAAE,EACD,OAAO,CAAC,EAAE,QAAQ,MAAM,QAAQ,SAAS,CAAC;AAC7C,QAAI,CAAC,WAAW,OAAQ;AAExB,eAAW,QAAQ,KAAK,OAAO;AAC7B,YAAM,OAAmB,CAAC;AAC1B,UAAI,YAAoC;AAExC,iBAAW,EAAE,QAAQ,QAAQ,KAAK,YAAY;AAC5C,cAAM,YAAY,MAAM,SAAS,KAAK,UAAU,MAAM,MAAM;AAC5D,YAAI,cAAc,OAAQ;AAC1B,YAAI,cAAc,OAAQ,aAAY;AACtC,aAAK,KAAK,MAAM;AAAA,MAClB;AAEA,UAAI,CAAC,KAAK,OAAQ;AAClB,cAAQ,KAAK;AAAA,QACX,UAAU,KAAK;AAAA,QACf;AAAA,QACA,SAAS,MAAM,WAAW,MAAM,SAAS,QAAQ,GAAG,CAAC;AAAA,QACrD;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AAEA,SAAO;AACT;AAWA,SAAS,MACP,SACA,UACA,MACA,QAC4B;AAC5B,MAAI,WAA4B;AAEhC,aAAW,UAAU,SAAS;AAC5B,QAAI,CAAC,OAAO,OAAQ,QAAO;AAE3B,UAAM,WAAW,OAAO,IAAI,IAAI,UAAU,OAAO,MAAM,CAAC;AACxD,QAAI,CAAC,UAAU,QAAQ;AACrB,iBAAW;AACX;AAAA,IACF;AACA,QAAI,SAAS,KAAK,CAAC,UAAU,SAAS,OAAO,IAAI,CAAC,EAAG,QAAO;AAAA,EAC9D;AAEA,SAAO;AACT;AAEA,SAAS,SAAS,OAAoB,MAAyB;AAC7D,SAAO,MAAM,aAAa,KAAK,WAAW,KAAK,aAAa,MAAM;AACpE;AAGA,SAAS,MAAM,SAA2C;AACxD,QAAM,OAA+B;AAAA,IACnC,SAAS;AAAA,IACT,WAAW;AAAA,IACX,MAAM;AAAA,IACN,YAAY;AAAA,IACZ,UAAU;AAAA,EACZ;AACA,SAAO,CAAC,GAAG,OAAO,EAAE;AAAA,IAClB,CAAC,MAAM,WACJ,KAAK,KAAK,QAAQ,KAAK,MAAM,KAAK,MAAM,QAAQ,KAAK,OACrD,KAAK,OAAO,YAAY,WAAW,MAAM,IAAI;AAAA,MAC5C,MAAM,OAAO,YAAY,WAAW,MAAM;AAAA,IAC5C;AAAA,EACJ;AACF;AAEA,SAAS,YAAY,QAAmD;AACtE,QAAM,QAAQ,oBAAI,IAA2B;AAC7C,aAAW,SAAS,QAAQ;AAC1B,UAAM,KAAK,IAAI,MAAM,MAAM,MAAM,MAAM;AACvC,UAAM,IAAI,IAAI,CAAC,GAAI,MAAM,IAAI,EAAE,KAAK,CAAC,GAAI,KAAK,CAAC;AAAA,EACjD;AACA,SAAO;AACT;AAEA,SAAS,IAAI,MAAc,QAAwB;AACjD,SAAO,GAAG,UAAU,IAAI,CAAC,IAAI,MAAM;AACrC;AAGA,SAAS,UAAU,MAAsB;AACvC,SAAO,KAAK,QAAQ,SAAS,EAAE;AACjC;","names":[]}
package/dist/mcp-main.cjs CHANGED
@@ -46,6 +46,11 @@ var kbActorStampSchema = import_zod.z.object({
46
46
  by: import_zod.z.string().min(1),
47
47
  at: import_zod.z.string().min(1)
48
48
  }).passthrough();
49
+ var kbVerifiedEventSchema = kbActorStampSchema.extend({
50
+ note: import_zod.z.string().refine((s) => s.trim().length > 0, {
51
+ message: "note must say what the check found"
52
+ })
53
+ });
49
54
  var kbAnchorSchema = import_zod.z.object({
50
55
  file: import_zod.z.string().min(1),
51
56
  symbol: import_zod.z.string().min(1).optional()
@@ -1577,8 +1582,37 @@ var validateCommand = define({
1577
1582
  failsWhen: (result) => Array.isArray(result) && result.length > 0
1578
1583
  });
1579
1584
 
1580
- // src/commands/write.ts
1585
+ // src/commands/verify.ts
1581
1586
  var import_zod26 = require("zod");
1587
+ var verifyCommand = define({
1588
+ name: "verify",
1589
+ tool: "kb_verify",
1590
+ usage: "verify <concept-id> --note <text>",
1591
+ description: "Append one verified[] event \u2014 who checked the record, when, and what the check found. Appends only; prior events are never rewritten. A record's own generator is refused unless the actor is human: re-reading your own output is not an independent check.",
1592
+ input: import_zod26.z.object({
1593
+ bundlePath,
1594
+ conceptId,
1595
+ note: import_zod26.z.string().refine((s) => s.trim().length > 0, {
1596
+ message: "note must say what the check found"
1597
+ })
1598
+ }),
1599
+ fromArgv: (argv, path) => ({
1600
+ bundlePath: path,
1601
+ conceptId: argv[1],
1602
+ note: argvFlag(argv, "--note")
1603
+ }),
1604
+ run: async ({ store, actor, now }, { bundlePath: path, conceptId: id, note }) => {
1605
+ await assertBaseNotFrozen(process.cwd(), path);
1606
+ const record = await store.verify(path, id, note, actor, now());
1607
+ return {
1608
+ conceptId: record.conceptId,
1609
+ verified: record.frontmatter.verified?.length ?? 0
1610
+ };
1611
+ }
1612
+ });
1613
+
1614
+ // src/commands/write.ts
1615
+ var import_zod27 = require("zod");
1582
1616
  var writeCommand = define({
1583
1617
  name: "write",
1584
1618
  tool: "kb_write",
@@ -1592,9 +1626,9 @@ var writeCommand = define({
1592
1626
  "- Prefer a new record over overloading an existing one, and keep each short. A record nobody finishes reading is not durable memory.",
1593
1627
  "- Records are never deleted; supersede instead, so the earlier reasoning stays inspectable."
1594
1628
  ].join("\n"),
1595
- input: import_zod26.z.object({
1629
+ input: import_zod27.z.object({
1596
1630
  bundlePath,
1597
- type: import_zod26.z.enum(KB_RECORD_TYPES),
1631
+ type: import_zod27.z.enum(KB_RECORD_TYPES),
1598
1632
  input: composeInputSchema
1599
1633
  }),
1600
1634
  fromArgv: async (argv, path, stdin) => ({
@@ -1618,7 +1652,7 @@ var writeCommand = define({
1618
1652
  });
1619
1653
 
1620
1654
  // src/commands/write-decision.ts
1621
- var import_zod27 = require("zod");
1655
+ var import_zod28 = require("zod");
1622
1656
  var writeDecisionCommand = define({
1623
1657
  name: "write-decision",
1624
1658
  tool: "kb_write_decision",
@@ -1631,7 +1665,7 @@ var writeDecisionCommand = define({
1631
1665
  "- `alternative` is what you turned down and why, not a list of everything considered.",
1632
1666
  "- A reference to material you read goes in `sources`; a reference to code goes in `anchors`; a reference to another record goes in `relatedConceptIds`."
1633
1667
  ].join("\n"),
1634
- input: import_zod27.z.object({ bundlePath, input: decisionInputSchema }),
1668
+ input: import_zod28.z.object({ bundlePath, input: decisionInputSchema }),
1635
1669
  fromArgv: async (_argv, path, stdin) => ({
1636
1670
  bundlePath: path,
1637
1671
  input: JSON.parse(await stdin())
@@ -1659,6 +1693,7 @@ var KB_COMMANDS = [
1659
1693
  statusCommand,
1660
1694
  supersedeCommand,
1661
1695
  answerCommand,
1696
+ verifyCommand,
1662
1697
  loadCommand,
1663
1698
  queryCommand,
1664
1699
  traceCommand,
@@ -1774,6 +1809,25 @@ var KbWriteConflictError = class extends BaseError {
1774
1809
  }
1775
1810
  conceptId;
1776
1811
  };
1812
+ var KbSelfVerificationError = class extends BaseError {
1813
+ constructor(conceptId2, actor, generatedBy) {
1814
+ super({
1815
+ message: `kb: ${conceptId2} was generated by ${generatedBy}, and a record's generator cannot verify it \u2014 only a human or a different actor can`,
1816
+ errorType: "KbSelfVerification" /* KbSelfVerification */,
1817
+ code: 400,
1818
+ fault: "User" /* User */,
1819
+ retriable: false,
1820
+ reportToUser: true,
1821
+ details: { conceptId: conceptId2, actor, generatedBy, action: "refused" }
1822
+ });
1823
+ this.conceptId = conceptId2;
1824
+ this.actor = actor;
1825
+ this.generatedBy = generatedBy;
1826
+ }
1827
+ conceptId;
1828
+ actor;
1829
+ generatedBy;
1830
+ };
1777
1831
  var KbInvalidConceptIdError = class extends BaseError {
1778
1832
  constructor(message, details) {
1779
1833
  super({
@@ -1992,6 +2046,40 @@ var KbStore = class {
1992
2046
  { operation: `status:${status}`, by: actor }
1993
2047
  );
1994
2048
  }
2049
+ /**
2050
+ * Appends one `verified[]` event: who checked the record, when, and what the
2051
+ * check found. Append-only — prior events are history, and are spread into
2052
+ * the new array untouched rather than reshaped through the write schema.
2053
+ *
2054
+ * A record's generator cannot verify its own record unless the actor is
2055
+ * human: the generator re-reading its own output is not an independent
2056
+ * check. The rule runs before the mutation so a refusal never publishes,
2057
+ * and the refusal is logged under its own operation name — `mutate` only
2058
+ * logs what it publishes.
2059
+ */
2060
+ async verify(bundlePath2, conceptId2, note, actor = "unknown", at = (/* @__PURE__ */ new Date()).toISOString()) {
2061
+ const event = kbVerifiedEventSchema.parse({ by: actor, at, note });
2062
+ const existing = await this.read(bundlePath2, conceptId2);
2063
+ if (!existing) throw new KbRecordNotFoundError(conceptId2);
2064
+ const generatedBy = existing.frontmatter.generated?.by;
2065
+ if (generatedBy !== void 0 && actor.toLowerCase() === generatedBy.toLowerCase() && !normalizeActor(actor).startsWith("human:")) {
2066
+ await this.record(this.root(bundlePath2), {
2067
+ operation: "verify:refused",
2068
+ conceptId: conceptId2,
2069
+ by: actor
2070
+ });
2071
+ throw new KbSelfVerificationError(conceptId2, actor, generatedBy);
2072
+ }
2073
+ return this.mutate(
2074
+ bundlePath2,
2075
+ conceptId2,
2076
+ (frontmatter) => ({
2077
+ ...frontmatter,
2078
+ verified: [...frontmatter.verified ?? [], event]
2079
+ }),
2080
+ { operation: "verify", by: actor }
2081
+ );
2082
+ }
1995
2083
  /**
1996
2084
  * Marks `conceptId` superseded by `replacementId`, and links both directions.
1997
2085
  *
@@ -2326,6 +2414,11 @@ function matches(record, needle) {
2326
2414
  (field) => field?.toLowerCase().includes(needle)
2327
2415
  );
2328
2416
  }
2417
+ function normalizeActor(id) {
2418
+ const colon = id.indexOf(":");
2419
+ if (colon === -1) return id.toLowerCase();
2420
+ return id.slice(0, colon + 1).toLowerCase() + id.slice(colon + 1);
2421
+ }
2329
2422
  function digest(contents) {
2330
2423
  return (0, import_node_crypto.createHash)("sha256").update(contents).digest("hex");
2331
2424
  }