@saasontools/strauss-kb 0.1.1 → 0.1.2

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.
@@ -0,0 +1,85 @@
1
+ # Architecture
2
+
3
+ The README says what the format is and how to use it. This says why it is
4
+ shaped that way, and which alternatives were tried and dropped — the decisions
5
+ a later reader would otherwise reopen.
6
+
7
+ ## One record per file
8
+
9
+ The filename is the identity, so two writers never merge; they only choose
10
+ distinct names. Publication uses `link`, which fails when the name is taken, and
11
+ a collision surfaces as a 409 the caller has to answer rather than a silent
12
+ last-write-wins.
13
+
14
+ Read-modify-write (`setStatus`, `answer`) checks a content digest immediately
15
+ before publishing. That narrows the lost-update window to two adjacent syscalls
16
+ rather than closing it. A lock would close it and add a stale-hold failure worse
17
+ than the residue: a crashed holder blocks every later writer, where a lost
18
+ update costs one retry.
19
+
20
+ ## Read for a question, not for a session
21
+
22
+ A base loaded at the start of a long conversation is summarised away by the end
23
+ of it, and nothing keeps it alive. So no consumer loads it that way:
24
+
25
+ | Consumer | How it reads |
26
+ | ------------------------------- | ---------------------------------------------------------------------------- |
27
+ | Diff annotation | `matchToDiff` — deterministic, no context involved |
28
+ | "Has this been decided?" | a fresh short-lived reader, given the base and the question, discarded after |
29
+ | An implementor writing a record | a point query at the moment of writing, not a load an hour earlier |
30
+
31
+ Each has clean context by construction. Where a base genuinely must stay
32
+ resident it will drift, and there is no defence — the mitigation is that
33
+ reloading costs about three thousand tokens, so read it again at the point of
34
+ use rather than trying to keep it.
35
+
36
+ ## What happens when a base outgrows a context
37
+
38
+ Loading stops working somewhere above a few hundred records. What replaces it is
39
+ not a different answer to the same question — it is the same reader, given
40
+ candidates instead of everything:
41
+
42
+ ```
43
+ vector recall → top ~20 candidates, with their scores
44
+
45
+ reader judges → picks what answers, or says nothing does
46
+ ```
47
+
48
+ The reader stays the judge in both regimes, which is what preserves the two
49
+ structural wins the README reports: it can say no record answers the question,
50
+ and it picks the record that answers rather than the one nearest the topic.
51
+ Neither survives if a ranker's top hit is taken as the answer.
52
+
53
+ **A score threshold is not the growth path.** The wrong `audit trail` hit scored
54
+ 0.318 against the correct `race condition` hit at 0.295; any cut that drops the
55
+ first drops the second. A threshold excludes the absurd — an unrelated query
56
+ scored 0.091 — and nothing else.
57
+
58
+ **Tags are not the growth path either.** The field exists, is written, and is
59
+ read by nothing but an index line, deliberately. Free-text tags drift the way
60
+ `auth` / `authentication` / `authn` drift, which is the failure this format was
61
+ rewritten to remove; enforcing a vocabulary would make them a closed enum, which
62
+ `type` already is. The labels that matter here are already verifiable —
63
+ `strauss_anchors` names a file and a symbol, which either match the repository
64
+ or do not, where a tag can be wrong forever. If narrowing ever matters, measure
65
+ tag narrowing against vector recall on a real base rather than adding both.
66
+
67
+ ## Rejected: a format that needs a parser
68
+
69
+ This was broken twice. A hand-rolled frontmatter reader could not express nested
70
+ maps, so it misread every OKF `generated`, `sources[]`, and `verified[]`. Its
71
+ replacement's first log format was `·`-delimited, with a splitter to read it
72
+ back. Both are gone: the log is JSONL and the schema is emitted from Zod, so
73
+ `strauss-kb schema` is the contract rather than a description of one.
74
+
75
+ ## Rejected for now: a base registry
76
+
77
+ Cross-base questions are unaskable by construction — supersession, traces, and
78
+ search stop at the directory boundary. That is the price of a base that can be
79
+ copied, deleted, or handed over whole, and it is what keeps the search index
80
+ disposable.
81
+
82
+ If cross-base ever becomes the common case, the cheap escape is a registry: a
83
+ list of paths a caller may name explicitly, queried one at a time and merged
84
+ only for display. It is deliberately unbuilt. Adding it early would drag back
85
+ the cross-scope machinery this model exists to avoid.
package/README.md CHANGED
@@ -146,6 +146,10 @@ the name is taken — two writers choosing one concept id is a 409 the caller mu
146
146
  answer, by picking a more specific slug or by saying it meant to replace.
147
147
  `rename` is used only when the caller passes `overwrite`.
148
148
 
149
+ Read-modify-write (`setStatus`, `answer`) checks a content digest immediately
150
+ before publishing, which narrows the lost-update window rather than closing it.
151
+ [ARCHITECTURE.md](./ARCHITECTURE.md) says why a lock was rejected.
152
+
149
153
  `supersede` writes both directions, so a backlink cannot drift in normal use and
150
154
  `validate` drops to catching hand-edits.
151
155
 
@@ -267,6 +271,10 @@ say no record answers the question, where vector search returns its nearest
267
271
  neighbour whatever the distance; and a reader picks the record that answers the
268
272
  question rather than the one nearest the topic.
269
273
 
274
+ Read for a question, not for a session: a base loaded at the start of a long
275
+ conversation is summarised away by the end of it, and reloading costs about
276
+ three thousand tokens. Read it again at the point of use.
277
+
270
278
  `load` refuses rather than truncating when a base exceeds its budget (25,000
271
279
  tokens by default). A truncated base is indistinguishable from a complete one,
272
280
  so a caller would answer "that was never decided" from a slice it did not know
@@ -333,7 +341,8 @@ hold is the exception — no invariant, deterministic path.
333
341
  the directory boundary. "Was this settled somewhere else?" is answered by a
334
342
  person choosing which base to open. That is the price of a base that can be
335
343
  copied, deleted, or handed over whole, and it is what keeps the search index
336
- disposable.
344
+ disposable. [ARCHITECTURE.md](./ARCHITECTURE.md) covers the registry that would
345
+ lift it, and why it is unbuilt.
337
346
 
338
347
  ## License
339
348
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saasontools/strauss-kb",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Knowledge base of markdown records with standing, supersession and trace: library, CLI, and MCP server over one command set",
5
5
  "license": "MIT",
6
6
  "author": "Assaf Kamil",
@@ -29,6 +29,7 @@
29
29
  "files": [
30
30
  "dist",
31
31
  "README.md",
32
+ "ARCHITECTURE.md",
32
33
  "LICENSE"
33
34
  ],
34
35
  "keywords": [