@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/README.md CHANGED
@@ -88,6 +88,24 @@ concepts. `type` is the only key OKF requires; `title`, `description`,
88
88
  OKF's. Unknown keys are preserved rather than stripped, as OKF requires of
89
89
  consumers.
90
90
 
91
+ `verified` is the record's append-only trail of checks. Each entry is OKF's
92
+ actor stamp — `{ by, at }` — and the entries this package's `verify` writes add
93
+ a `note`: what the check actually found, not just that one happened. The `note`
94
+ is a strauss extension key on the entries this tool writes, not an OKF
95
+ requirement on the array, so noteless entries a foreign producer wrote remain
96
+ readable, and prior entries are spread forward untouched rather than reshaped.
97
+
98
+ Who may append is the point. A verifier whose actor equals the record's
99
+ `generated.by` — compared case-insensitively over the whole actor, so case
100
+ drift cannot mint a distinct verifier identity — is refused unless the actor
101
+ is `human:`-prefixed: trust that can be self-granted is not trust, and a
102
+ generator re-reading its own output is not an independent check. The refusal
103
+ is recorded in the log as `verify:refused`, so an audit sees the attempt as
104
+ well as the rule. The `human:` prefix itself is an honor-system label — actor
105
+ identity is self-declared through `STRAUSS_KB_ACTOR`, not an authenticated
106
+ identity claim — which is worth knowing when deciding how much weight a
107
+ human-verified event carries.
108
+
91
109
  Anything prefixed `strauss_` is this package's extension, namespaced so a later
92
110
  OKF version defining the same name cannot collide:
93
111
 
@@ -196,6 +214,7 @@ strauss-kb [--bundle PATH] <command> [args]
196
214
  status <concept-id> <status> Move a record's status, compare-and-swap.
197
215
  supersede <concept-id> <replacement-id> Mark a record superseded, linking both directions.
198
216
  answer <concept-id> <answer...> Resolve an open question and append the answer.
217
+ verify <concept-id> --note <text> Append a verified[] event — who checked, when, and what the check found.
199
218
  load [type] [--budget N | --all] Hand over the whole base, each record with its standing.
200
219
  query <text...> Search; every match arrives flagged with its standing.
201
220
  trace <concept-id> [edges...] How a position was arrived at, as a timeline.
@@ -239,8 +258,8 @@ strauss-kb validate || echo "problems above"
239
258
 
240
259
  `strauss-kb-mcp` speaks stdio and takes no API key and no required environment.
241
260
  Every CLI verb is a tool: `kb_write`, `kb_write_decision`, `kb_no_decision`,
242
- `kb_status`, `kb_supersede`, `kb_answer`, `kb_load`, `kb_query`, `kb_trace`,
243
- `kb_list`, `kb_index`, `kb_log`, `kb_validate`, `kb_schema`, `kb_types`,
261
+ `kb_status`, `kb_supersede`, `kb_answer`, `kb_verify`, `kb_load`, `kb_query`,
262
+ `kb_trace`, `kb_list`, `kb_index`, `kb_log`, `kb_validate`, `kb_schema`, `kb_types`,
244
263
  `kb_pin`, `kb_unpin`, `kb_pins`, `kb_context`. Most tools take a `bundlePath`;
245
264
  `kb_schema` and `kb_types` describe the format rather than any one base, and
246
265
  `kb_pins` and `kb_context` read the workspace pin manifests instead. The one
@@ -299,6 +318,15 @@ problem:
299
318
  | Standing | `strauss_status`, the supersession chain | is this still what we hold? |
300
319
  | Freshness | `stale_after`, `verified[]` | has anyone confirmed it lately? |
301
320
 
321
+ Freshness is tiered by who did the confirming. OKF's spec (§5.3) defines the
322
+ trust tiers from the verifying actor's prefix: an empty `verified[]` is
323
+ unverified, an agent-prefixed verifier makes the record machine-confirmed, and
324
+ a `human:`-prefixed verifier makes it human-reviewed. Of that ladder, today's
325
+ adjudication reports only the first rung — the warning it attaches when
326
+ `verified[]` is empty; reporting the full tier is upcoming tooling. When it
327
+ lands, the tier will be derived from the events at read time, never stored, so
328
+ it cannot drift from the trail that justifies it.
329
+
302
330
  **Load before you search.** These bases run to a few thousand tokens — twenty
303
331
  records measured at about 3,000 — so the first thing to try is taking all of it.
304
332
  On nine questions whose wording appears in no record, a reader holding the whole
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  KB_COMMANDS,
3
3
  KbStore
4
- } from "./chunk-FZIMFPGR.js";
4
+ } from "./chunk-PNSRTKYN.js";
5
5
 
6
6
  // src/mcp.ts
7
7
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -45,4 +45,4 @@ export {
45
45
  createKbMcpServer,
46
46
  runKbMcpServer
47
47
  };
48
- //# sourceMappingURL=chunk-VOJ6D6OX.js.map
48
+ //# sourceMappingURL=chunk-BVF7X5VO.js.map
@@ -11,6 +11,11 @@ var kbActorStampSchema = z.object({
11
11
  by: z.string().min(1),
12
12
  at: z.string().min(1)
13
13
  }).passthrough();
14
+ var kbVerifiedEventSchema = kbActorStampSchema.extend({
15
+ note: z.string().refine((s) => s.trim().length > 0, {
16
+ message: "note must say what the check found"
17
+ })
18
+ });
14
19
  var kbAnchorSchema = z.object({
15
20
  file: z.string().min(1),
16
21
  symbol: z.string().min(1).optional()
@@ -1536,8 +1541,37 @@ var validateCommand = define({
1536
1541
  failsWhen: (result) => Array.isArray(result) && result.length > 0
1537
1542
  });
1538
1543
 
1539
- // src/commands/write.ts
1544
+ // src/commands/verify.ts
1540
1545
  import { z as z26 } from "zod";
1546
+ var verifyCommand = define({
1547
+ name: "verify",
1548
+ tool: "kb_verify",
1549
+ usage: "verify <concept-id> --note <text>",
1550
+ 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.",
1551
+ input: z26.object({
1552
+ bundlePath,
1553
+ conceptId,
1554
+ note: z26.string().refine((s) => s.trim().length > 0, {
1555
+ message: "note must say what the check found"
1556
+ })
1557
+ }),
1558
+ fromArgv: (argv, path) => ({
1559
+ bundlePath: path,
1560
+ conceptId: argv[1],
1561
+ note: argvFlag(argv, "--note")
1562
+ }),
1563
+ run: async ({ store, actor, now }, { bundlePath: path, conceptId: id, note }) => {
1564
+ await assertBaseNotFrozen(process.cwd(), path);
1565
+ const record = await store.verify(path, id, note, actor, now());
1566
+ return {
1567
+ conceptId: record.conceptId,
1568
+ verified: record.frontmatter.verified?.length ?? 0
1569
+ };
1570
+ }
1571
+ });
1572
+
1573
+ // src/commands/write.ts
1574
+ import { z as z27 } from "zod";
1541
1575
  var writeCommand = define({
1542
1576
  name: "write",
1543
1577
  tool: "kb_write",
@@ -1551,9 +1585,9 @@ var writeCommand = define({
1551
1585
  "- Prefer a new record over overloading an existing one, and keep each short. A record nobody finishes reading is not durable memory.",
1552
1586
  "- Records are never deleted; supersede instead, so the earlier reasoning stays inspectable."
1553
1587
  ].join("\n"),
1554
- input: z26.object({
1588
+ input: z27.object({
1555
1589
  bundlePath,
1556
- type: z26.enum(KB_RECORD_TYPES),
1590
+ type: z27.enum(KB_RECORD_TYPES),
1557
1591
  input: composeInputSchema
1558
1592
  }),
1559
1593
  fromArgv: async (argv, path, stdin) => ({
@@ -1577,7 +1611,7 @@ var writeCommand = define({
1577
1611
  });
1578
1612
 
1579
1613
  // src/commands/write-decision.ts
1580
- import { z as z27 } from "zod";
1614
+ import { z as z28 } from "zod";
1581
1615
  var writeDecisionCommand = define({
1582
1616
  name: "write-decision",
1583
1617
  tool: "kb_write_decision",
@@ -1590,7 +1624,7 @@ var writeDecisionCommand = define({
1590
1624
  "- `alternative` is what you turned down and why, not a list of everything considered.",
1591
1625
  "- A reference to material you read goes in `sources`; a reference to code goes in `anchors`; a reference to another record goes in `relatedConceptIds`."
1592
1626
  ].join("\n"),
1593
- input: z27.object({ bundlePath, input: decisionInputSchema }),
1627
+ input: z28.object({ bundlePath, input: decisionInputSchema }),
1594
1628
  fromArgv: async (_argv, path, stdin) => ({
1595
1629
  bundlePath: path,
1596
1630
  input: JSON.parse(await stdin())
@@ -1618,6 +1652,7 @@ var KB_COMMANDS = [
1618
1652
  statusCommand,
1619
1653
  supersedeCommand,
1620
1654
  answerCommand,
1655
+ verifyCommand,
1621
1656
  loadCommand,
1622
1657
  queryCommand,
1623
1658
  traceCommand,
@@ -1673,6 +1708,7 @@ var ErrorTypes = /* @__PURE__ */ ((ErrorTypes2) => {
1673
1708
  ErrorTypes2["KbRecordAlreadyExists"] = "KbRecordAlreadyExists";
1674
1709
  ErrorTypes2["KbInvalidConceptId"] = "KbInvalidConceptId";
1675
1710
  ErrorTypes2["KbRecordNotFound"] = "KbRecordNotFound";
1711
+ ErrorTypes2["KbSelfVerification"] = "KbSelfVerification";
1676
1712
  ErrorTypes2["KbWriteConflict"] = "KbWriteConflict";
1677
1713
  return ErrorTypes2;
1678
1714
  })(ErrorTypes || {});
@@ -1741,6 +1777,25 @@ var KbWriteConflictError = class extends BaseError {
1741
1777
  }
1742
1778
  conceptId;
1743
1779
  };
1780
+ var KbSelfVerificationError = class extends BaseError {
1781
+ constructor(conceptId2, actor, generatedBy) {
1782
+ super({
1783
+ message: `kb: ${conceptId2} was generated by ${generatedBy}, and a record's generator cannot verify it \u2014 only a human or a different actor can`,
1784
+ errorType: "KbSelfVerification" /* KbSelfVerification */,
1785
+ code: 400,
1786
+ fault: "User" /* User */,
1787
+ retriable: false,
1788
+ reportToUser: true,
1789
+ details: { conceptId: conceptId2, actor, generatedBy, action: "refused" }
1790
+ });
1791
+ this.conceptId = conceptId2;
1792
+ this.actor = actor;
1793
+ this.generatedBy = generatedBy;
1794
+ }
1795
+ conceptId;
1796
+ actor;
1797
+ generatedBy;
1798
+ };
1744
1799
  var KbInvalidConceptIdError = class extends BaseError {
1745
1800
  constructor(message, details) {
1746
1801
  super({
@@ -1971,6 +2026,40 @@ var KbStore = class {
1971
2026
  { operation: `status:${status}`, by: actor }
1972
2027
  );
1973
2028
  }
2029
+ /**
2030
+ * Appends one `verified[]` event: who checked the record, when, and what the
2031
+ * check found. Append-only — prior events are history, and are spread into
2032
+ * the new array untouched rather than reshaped through the write schema.
2033
+ *
2034
+ * A record's generator cannot verify its own record unless the actor is
2035
+ * human: the generator re-reading its own output is not an independent
2036
+ * check. The rule runs before the mutation so a refusal never publishes,
2037
+ * and the refusal is logged under its own operation name — `mutate` only
2038
+ * logs what it publishes.
2039
+ */
2040
+ async verify(bundlePath2, conceptId2, note, actor = "unknown", at = (/* @__PURE__ */ new Date()).toISOString()) {
2041
+ const event = kbVerifiedEventSchema.parse({ by: actor, at, note });
2042
+ const existing = await this.read(bundlePath2, conceptId2);
2043
+ if (!existing) throw new KbRecordNotFoundError(conceptId2);
2044
+ const generatedBy = existing.frontmatter.generated?.by;
2045
+ if (generatedBy !== void 0 && actor.toLowerCase() === generatedBy.toLowerCase() && !normalizeActor(actor).startsWith("human:")) {
2046
+ await this.record(this.root(bundlePath2), {
2047
+ operation: "verify:refused",
2048
+ conceptId: conceptId2,
2049
+ by: actor
2050
+ });
2051
+ throw new KbSelfVerificationError(conceptId2, actor, generatedBy);
2052
+ }
2053
+ return this.mutate(
2054
+ bundlePath2,
2055
+ conceptId2,
2056
+ (frontmatter) => ({
2057
+ ...frontmatter,
2058
+ verified: [...frontmatter.verified ?? [], event]
2059
+ }),
2060
+ { operation: "verify", by: actor }
2061
+ );
2062
+ }
1974
2063
  /**
1975
2064
  * Marks `conceptId` superseded by `replacementId`, and links both directions.
1976
2065
  *
@@ -2305,6 +2394,11 @@ function matches(record, needle) {
2305
2394
  (field) => field?.toLowerCase().includes(needle)
2306
2395
  );
2307
2396
  }
2397
+ function normalizeActor(id) {
2398
+ const colon = id.indexOf(":");
2399
+ if (colon === -1) return id.toLowerCase();
2400
+ return id.slice(0, colon + 1).toLowerCase() + id.slice(colon + 1);
2401
+ }
2308
2402
  function digest(contents) {
2309
2403
  return createHash("sha256").update(contents).digest("hex");
2310
2404
  }
@@ -2312,6 +2406,7 @@ function digest(contents) {
2312
2406
  export {
2313
2407
  kbSourceSchema,
2314
2408
  kbActorStampSchema,
2409
+ kbVerifiedEventSchema,
2315
2410
  kbAnchorSchema,
2316
2411
  KB_RECORD_TYPES,
2317
2412
  KB_SLUG_PATTERN,
@@ -2377,6 +2472,7 @@ export {
2377
2472
  KbRecordAlreadyExistsError,
2378
2473
  KbRecordNotFoundError,
2379
2474
  KbWriteConflictError,
2475
+ KbSelfVerificationError,
2380
2476
  KbInvalidConceptIdError,
2381
2477
  SEARCH_INDEX_FILE,
2382
2478
  searchBase,
@@ -2385,4 +2481,4 @@ export {
2385
2481
  KB_DIR,
2386
2482
  KbStore
2387
2483
  };
2388
- //# sourceMappingURL=chunk-FZIMFPGR.js.map
2484
+ //# sourceMappingURL=chunk-PNSRTKYN.js.map