@dzhechkov/keysarium 1.5.34 → 1.5.35

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/.dz-manifest.json CHANGED
@@ -13,7 +13,7 @@
13
13
  },
14
14
  {
15
15
  "path": "README.md",
16
- "sha256": "0b1de0c5bd3bde4586c64bab4a3ab90a4313c8659f327bfa774d9fe9d7a268b7"
16
+ "sha256": "5f1e61279a3f6b18e7db46adab9860d3cc95ed31926b122bc8508bf445f11117"
17
17
  },
18
18
  {
19
19
  "path": "README.ru.md",
@@ -29,7 +29,7 @@
29
29
  },
30
30
  {
31
31
  "path": "package.json",
32
- "sha256": "36f43e39204a8d1eb2bddebb20223ff87ef2686ce5f090f00c76752006764ecf"
32
+ "sha256": "206ba50881c8dc2a62ab44af643f314928b52c0b711757f9e1a3d1b0312724bb"
33
33
  },
34
34
  {
35
35
  "path": "scripts/sync-templates.js",
@@ -509,7 +509,7 @@
509
509
  },
510
510
  {
511
511
  "path": "templates/.claude/skills/goap-research-ed25519/SKILL.md",
512
- "sha256": "eb9a58ed3fbfb9b1c6627c563b5ce1b5ac9f1f5fb9d148a79441d94422933687"
512
+ "sha256": "95459a1566b2f88b6fb0c1d2f096f7631d698b95fa156afa26444a7c06e76451"
513
513
  },
514
514
  {
515
515
  "path": "templates/.claude/skills/goap-research-ed25519/references/ed25519-verification.md",
@@ -545,7 +545,7 @@
545
545
  },
546
546
  {
547
547
  "path": "templates/.claude/skills/goap-research-ed25519/scripts/learning_bridge.py",
548
- "sha256": "c9aea90ddfba5990b27405ca66a3f90308afa357e227ddad3877f9eee7df6e36"
548
+ "sha256": "6980dd654d77e296f1ab20a9b7a7c43741fd4125ee44727183c63587421d5503"
549
549
  },
550
550
  {
551
551
  "path": "templates/.claude/skills/goap-research-ed25519/scripts/source_tiers.py",
@@ -557,7 +557,7 @@
557
557
  },
558
558
  {
559
559
  "path": "templates/.claude/skills/goap-research-ed25519/scripts/test_evidence_provenance.py",
560
- "sha256": "1be4f63bbe7e77d599c5f0960d92b20a8f7ea6a6d07b30cb1d728f920b7259f2"
560
+ "sha256": "6825da92d859a299b86bd9bf9910088a95665e0716346e55f0f117d991d25c9b"
561
561
  },
562
562
  {
563
563
  "path": "templates/.claude/skills/knowledge-extractor/SKILL.md",
@@ -965,5 +965,5 @@
965
965
  }
966
966
  ]
967
967
  },
968
- "signature": "PtZE6f5DojsJAIbHGO3jNQqfffcLK078tXUBWNMP4Lt/1O6A6SRmzqMqwy1mmIOtvCgDL9o49urvAsCNHjNLCw=="
968
+ "signature": "e14W9eecgau7bIlWej75568LWR2BC5IMnjU+T14Tz5Cq/2WOySneuN53xfxl3oFJyCUr+vDAojeq4B1gs7wqDg=="
969
969
  }
package/README.md CHANGED
@@ -6,6 +6,15 @@ Full 7-phase pipeline for AI case studies, hackathons, and casariums. Provides s
6
6
 
7
7
  ---
8
8
 
9
+
10
+ > **`goap-research-ed25519` — self-learning (optional, since this release).** When
11
+ > [`@dzhechkov/harness-cli`](https://www.npmjs.com/package/@dzhechkov/harness-cli) is on PATH, the
12
+ > bundled research skill recalls prior METHOD lessons at the start of an investigation and records new
13
+ > ones at four named moments. Without it the skill behaves exactly as before and says so once — it is
14
+ > detected, never required. A guard refuses to record anything shaped like personal data: a method
15
+ > lesson is written in lower case, carries no digits, and holds no identifier. See
16
+ > `skills/goap-research-ed25519/SKILL.md`.
17
+
9
18
  ## Quick Start
10
19
 
11
20
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dzhechkov/keysarium",
3
- "version": "1.5.34",
3
+ "version": "1.5.35",
4
4
  "description": "AI Case Research Toolkit for Claude Code — full 7-phase pipeline with feature-adr, dream cycles, background workers, multi-platform support, reward learning, and Cloud.ru AI Factory mapping (--ai-factory flag)",
5
5
  "main": "src/cli.js",
6
6
  "bin": {
package/sbom.json CHANGED
@@ -35,7 +35,7 @@
35
35
  "hashes": [
36
36
  {
37
37
  "alg": "SHA-256",
38
- "content": "0b1de0c5bd3bde4586c64bab4a3ab90a4313c8659f327bfa774d9fe9d7a268b7"
38
+ "content": "5f1e61279a3f6b18e7db46adab9860d3cc95ed31926b122bc8508bf445f11117"
39
39
  }
40
40
  ]
41
41
  },
@@ -75,7 +75,7 @@
75
75
  "hashes": [
76
76
  {
77
77
  "alg": "SHA-256",
78
- "content": "36f43e39204a8d1eb2bddebb20223ff87ef2686ce5f090f00c76752006764ecf"
78
+ "content": "206ba50881c8dc2a62ab44af643f314928b52c0b711757f9e1a3d1b0312724bb"
79
79
  }
80
80
  ]
81
81
  },
@@ -1275,7 +1275,7 @@
1275
1275
  "hashes": [
1276
1276
  {
1277
1277
  "alg": "SHA-256",
1278
- "content": "eb9a58ed3fbfb9b1c6627c563b5ce1b5ac9f1f5fb9d148a79441d94422933687"
1278
+ "content": "95459a1566b2f88b6fb0c1d2f096f7631d698b95fa156afa26444a7c06e76451"
1279
1279
  }
1280
1280
  ]
1281
1281
  },
@@ -1365,7 +1365,7 @@
1365
1365
  "hashes": [
1366
1366
  {
1367
1367
  "alg": "SHA-256",
1368
- "content": "c9aea90ddfba5990b27405ca66a3f90308afa357e227ddad3877f9eee7df6e36"
1368
+ "content": "6980dd654d77e296f1ab20a9b7a7c43741fd4125ee44727183c63587421d5503"
1369
1369
  }
1370
1370
  ]
1371
1371
  },
@@ -1395,7 +1395,7 @@
1395
1395
  "hashes": [
1396
1396
  {
1397
1397
  "alg": "SHA-256",
1398
- "content": "1be4f63bbe7e77d599c5f0960d92b20a8f7ea6a6d07b30cb1d728f920b7259f2"
1398
+ "content": "6825da92d859a299b86bd9bf9910088a95665e0716346e55f0f117d991d25c9b"
1399
1399
  }
1400
1400
  ]
1401
1401
  },
@@ -97,8 +97,11 @@ The accepted form has three properties, all checkable: **no digits** (spelled-ou
97
97
  values count — `eight point zero four` is a value), **no identifiers**, and **lower
98
98
  case** after the first word. The last one replaced an attempt to detect names: a name
99
99
  is any word, no list of them exists, and the attempt let `John Smith has HIV` through.
100
- Lower case costs a keystroke and closes that hole. ALL-CAPS and mixed-case acronyms
101
- (`SHBG`, `TSH`, `apoB`) are unaffected — they are how method lessons speak.
100
+ Lower case costs a keystroke and closes that hole. Acronyms are unaffected — a token
101
+ passes if it is fully upper (`SHBG`, `TSH`) or opens lower (`apoB`, `mmHg`); only Title
102
+ Case is refused, because Title Case is the shape of a name. A token mixing letters with
103
+ a long digit run (`ab1234567`, `NCT04368728`) is an identifier and is refused however it
104
+ is punctuated, while a short lab acronym carrying a digit (`hba1c`) is not.
102
105
 
103
106
  Pass `--allow-numbers` when the number IS the knowledge (a guideline threshold) and
104
107
  you take responsibility for it. It never waves through a person beside a number.
@@ -76,7 +76,13 @@ BOOST_NOTE_TAILS = (
76
76
  def _boost_note_present(out: str, domain: str = LEARNING_DOMAIN) -> bool:
77
77
  """True when THIS dz printed a real domain-boost note (not a lesson quoting one)."""
78
78
  head = f'domain "{domain}":'
79
- for line in out.splitlines():
79
+ # split("\n"), NOT splitlines(). Python's splitlines() also breaks on U+2028,
80
+ # U+0085 and friends, which the CLI does not treat as line breaks — so a lesson
81
+ # containing one produced a "line" the renderer never emitted, and that forged line
82
+ # could satisfy this probe. Splitting exactly the way the producer joins keeps a
83
+ # forged tail stuck on a line that starts with the hit prefix, where the anchor
84
+ # below rejects it.
85
+ for line in out.split("\n"):
80
86
  stripped = line.strip()
81
87
  if stripped.startswith(head) and any(tail in stripped for tail in BOOST_NOTE_TAILS):
82
88
  return True
@@ -151,6 +157,13 @@ _IDENTIFIER_PATTERNS: Sequence[Tuple[str, str, bool]] = (
151
157
  (r"\+?\d[\d\s().-]{7,}\d", "something shaped like a phone number", True),
152
158
  (r"\b(mrn|снилс|полис|истори\w* болезни|medical record (no|number)|record no)\b[:\s#]*\w*",
153
159
  "a medical-record identifier", True),
160
+ # Round 4: the TOKEN check refused `ab1234567`, so the same identifier written
161
+ # `ab—1234567` or `ab/1234567` was split by the tokenizer into a lower-case word
162
+ # and a permitted number — laundered by punctuation. This reads the RAW text, where
163
+ # the joiner is still visible. A SPACE is deliberately not a joiner here, or every
164
+ # "the 2019 guideline" would be refused.
165
+ (r"\b[A-Za-zА-Яа-я]{2,}[—–/._-]?\d{4,}\b",
166
+ "letters sitting against a long run of digits — that is an identifier, however it is punctuated", False),
154
167
  )
155
168
 
156
169
  # THE CAPITALISATION RULE — round 2 forced this rewrite.
@@ -190,8 +203,25 @@ _TOKEN_EDGE_PUNCT = " \t\n\r.,;:!?()[]{}\"'«»„“”‘’—–-/\\|*_`"
190
203
 
191
204
 
192
205
  def _is_acronym(token: str) -> bool:
193
- """Two or more capitals in one token: SHBG, HIV, apoB, HbA1c. A name has one."""
194
- return sum(1 for ch in token if ch.isupper()) >= 2
206
+ """An acronym, as opposed to a name.
207
+
208
+ Round 4 killed the previous rule ("two or more capitals"), which was wrong in BOTH
209
+ directions at once: `Anne-Marie` and `McDonald` carry two capitals and sailed
210
+ through as acronyms, while `apoB` carries one and was refused — contradicting the
211
+ documentation in the same repository that offered it as the example.
212
+
213
+ What actually separates the two is WHERE the first capital sits. A name is Title
214
+ Case: capital first, lower case after. An acronym is either fully upper (SHBG, HIV,
215
+ TSH) or starts lower and capitalises later (apoB, mmHg). So: accept a token whose
216
+ first letter is lower case, or one whose letters are ALL upper case. Refuse
217
+ anything that opens with a capital and then drops to lower case, whatever it is.
218
+ """
219
+ letters = [ch for ch in token if ch.isalpha()]
220
+ if not letters:
221
+ return False
222
+ if not letters[0].isupper():
223
+ return True # apoB, mmHg — lower-case opening is never a name
224
+ return all(ch.isupper() for ch in letters) # SHBG, HIV, D
195
225
 
196
226
 
197
227
  # Splitting on whitespace ALONE was the last hole round 3 found: `method—John has HIV`
@@ -212,6 +242,13 @@ def _tokens(raw: str) -> List[str]:
212
242
  # not a measurement in any unit system; it is a label for one thing.
213
243
  _NUMBER_TOKEN_RE = r"^[<>≥≤~±]?\d+([.,]\d+)?[a-zа-яё%°]{0,4}$"
214
244
 
245
+ # …but a few real lab acronyms carry a digit (`HbA1c`, `CYP2D6`). What separates them
246
+ # from an accession is SIZE: an identifier has to be long enough to be unique. So a
247
+ # mixed token is an identifier when it is long or digit-heavy, and a lab acronym
248
+ # otherwise. The bound is a judgement call and is stated rather than hidden.
249
+ _IDENTIFIER_MIN_LEN = 7
250
+ _IDENTIFIER_MIN_DIGITS = 4
251
+
215
252
  # A reference to ONE person. Alone it is fine — a method sentence may discuss patients
216
253
  # in the abstract. Beside a value it is load-bearing: a person plus a reading is a
217
254
  # record, and no --allow-numbers waves that through.
@@ -285,7 +322,9 @@ def check_lesson(text: str, allow_numbers: bool = False) -> LessonVerdict:
285
322
  for index, token in enumerate(tokens):
286
323
  has_digit = any(ch.isdigit() for ch in token)
287
324
  has_alpha = any(ch.isalpha() for ch in token)
288
- if has_digit and has_alpha and not re.match(_NUMBER_TOKEN_RE, token):
325
+ digit_count = sum(1 for ch in token if ch.isdigit())
326
+ looks_like_id = len(token) >= _IDENTIFIER_MIN_LEN or digit_count >= _IDENTIFIER_MIN_DIGITS
327
+ if has_digit and has_alpha and looks_like_id and not re.match(_NUMBER_TOKEN_RE, token):
289
328
  reasons.append(
290
329
  f"{token!r} mixes letters and digits — that is the shape of an identifier "
291
330
  "(a record number, an accession, a passport), not of a measurement, and no "
@@ -301,7 +340,8 @@ def check_lesson(text: str, allow_numbers: bool = False) -> LessonVerdict:
301
340
  reasons.append(
302
341
  f"a capitalised word ({token!r}) after the first one — a method lesson is written "
303
342
  "in lower case, because one capital is the shape of a name and names cannot be "
304
- "listed. Lower-case it (acronyms like SHBG, TSH or apoB carry two capitals and pass)"
343
+ "listed. Lower-case it — an acronym passes either fully upper (SHBG, TSH) or opening "
344
+ "lower (apoB); only Title Case is refused"
305
345
  )
306
346
  break
307
347
 
@@ -676,7 +676,11 @@ class LearningBridgeTests(unittest.TestCase):
676
676
  for ok_text in (
677
677
  "total testosterone is uninterpretable without SHBG",
678
678
  "a single ferritin cannot separate overload from inflammation, pair it with CRP",
679
- "apoB tracks risk better than LDL alone",
679
+ # NOT first in the sentence — round 4 found this test placed `apoB` at the
680
+ # start, where the sentence-start exemption made it pass no matter what the
681
+ # acronym rule did. A test whose subject sits in the one exempt position
682
+ # proves nothing about the rule it claims to cover.
683
+ "measure apoB before interpreting LDL alone",
680
684
  ):
681
685
  with self.subTest(text=ok_text):
682
686
  self.assertTrue(self.lb.check_lesson(ok_text).ok, ok_text)
@@ -766,6 +770,48 @@ class LearningBridgeTests(unittest.TestCase):
766
770
  allow_numbers=True).ok)
767
771
 
768
772
 
773
+ # ---------------------------------------------------------------- round 4
774
+
775
+ def test_round4_acronym_vs_name_is_decided_by_WHERE_the_capital_sits(self):
776
+ """The previous predicate — "two or more capitals" — was wrong in both
777
+ directions at once: `Anne-Marie` and `McDonald` passed as acronyms while
778
+ `apoB` was refused, contradicting this repo's own documentation."""
779
+ for name in ("patient Anne-Marie has HIV", "patient McDonald has HIV"):
780
+ with self.subTest(name=name):
781
+ self.assertFalse(self.lb.check_lesson(name).ok, name)
782
+ for acronym in ("measure apoB before interpreting risk",
783
+ "total testosterone is uninterpretable without SHBG"):
784
+ with self.subTest(acronym=acronym):
785
+ self.assertTrue(self.lb.check_lesson(acronym).ok, acronym)
786
+
787
+ def test_round4_punctuation_cannot_launder_an_identifier(self):
788
+ """`ab1234567` was refused, so the same identifier written `ab—1234567` was
789
+ split by the tokenizer into a lower-case word plus a permitted number. The raw
790
+ text still shows the joiner, so that is where this one is checked."""
791
+ for leak in ("passport ab—1234567 identifies the holder",
792
+ "passport ab/1234567 identifies the holder",
793
+ "trial NCT—04368728 enrolled a cohort"):
794
+ with self.subTest(leak=leak):
795
+ self.assertFalse(self.lb.check_lesson(leak, allow_numbers=True).ok, leak)
796
+ # A YEAR is not an identifier: a space is deliberately not a joiner.
797
+ self.assertTrue(self.lb.check_lesson("the 2019 guideline moved the threshold",
798
+ allow_numbers=True).ok)
799
+ # …and a short lab acronym carrying a digit still passes.
800
+ self.assertTrue(self.lb.check_lesson("hba1c drifts with red-cell turnover",
801
+ allow_numbers=True).ok)
802
+
803
+ def test_round4_the_probe_cannot_be_forged_by_an_exotic_line_separator(self):
804
+ """`splitlines()` breaks on U+2028 and U+0085, which the renderer does not treat
805
+ as line breaks — so a lesson containing one produced a 'line' the CLI never
806
+ emitted, and that forged line satisfied the capability probe. Splitting exactly
807
+ the way the producer joins keeps a forged tail on the hit's own line."""
808
+ tail = 'domain "health-research": order unchanged, nothing was hidden'
809
+ self.assertFalse(self.lb._boost_note_present(" [0.90] (general) x\u2028" + tail))
810
+ self.assertFalse(self.lb._boost_note_present(" [0.90] (general) x " + tail))
811
+ # A REAL note — its own line — is still detected.
812
+ self.assertTrue(self.lb._boost_note_present(" [0.90] (general) x\n " + tail))
813
+
814
+
769
815
  def test_a_real_failure_is_not_disguised_as_an_old_cli(self):
770
816
  """A crash, timeout or corrupt store must read as a failure, not as 'upgrade'."""
771
817
  original = self.lb._run_dz