@dzhechkov/skills-idea2prd 0.1.6 → 0.1.7

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
@@ -21,7 +21,7 @@
21
21
  },
22
22
  {
23
23
  "path": "package.json",
24
- "sha256": "ee0b0a6cd08b2d962cd3b734369870b434f33eeafe874a7f0f0dd1c10b68f85c"
24
+ "sha256": "4bfe6564c3745ab76783640b91973036f6687155111052f3e3e34a3eb4dcd874"
25
25
  },
26
26
  {
27
27
  "path": "sources.json",
@@ -109,7 +109,7 @@
109
109
  },
110
110
  {
111
111
  "path": "templates/.claude/skills/goap-research-ed25519/scripts/learning_bridge.py",
112
- "sha256": "6980dd654d77e296f1ab20a9b7a7c43741fd4125ee44727183c63587421d5503"
112
+ "sha256": "f6ec73a2ef239b8195a321b5f20b6ab390d9a7004b42226b2a861181cf9e2d2f"
113
113
  },
114
114
  {
115
115
  "path": "templates/.claude/skills/goap-research-ed25519/scripts/source_tiers.py",
@@ -121,7 +121,7 @@
121
121
  },
122
122
  {
123
123
  "path": "templates/.claude/skills/goap-research-ed25519/scripts/test_evidence_provenance.py",
124
- "sha256": "6825da92d859a299b86bd9bf9910088a95665e0716346e55f0f117d991d25c9b"
124
+ "sha256": "86216fb6a40fcf270eada41c4a534600d2bcb2799052cbacfd64b459229b5bfe"
125
125
  },
126
126
  {
127
127
  "path": "templates/.claude/skills/idea2prd-manual/SKILL.md",
@@ -173,5 +173,5 @@
173
173
  }
174
174
  ]
175
175
  },
176
- "signature": "UhTuxP7abRlbM11LsOmZlKlUfHTEjXUHLQ3d6OmAeJDWvn+AQiJ4k1pj5p2LIKshE3rsGQrckDPa/OQXH92sCw=="
176
+ "signature": "Rc5O0UZz+GsMlKiMvr/E8f4SUUtkaHJnL30dpGPXNIxY7Wo5SEYaTnB+P0GRSi1AYJjihTlsAo7d34RrSfQpAQ=="
177
177
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dzhechkov/skills-idea2prd",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Idea2PRD Manual — composite skill for Claude Code: from problem/idea to Vibe-Coding-ready docs (PRD + ADR + DDD + C4 + Pseudocode + Test Scenarios + Completion) with 9 checkpoints",
5
5
  "main": "src/cli.js",
6
6
  "bin": {
package/sbom.json CHANGED
@@ -55,7 +55,7 @@
55
55
  "hashes": [
56
56
  {
57
57
  "alg": "SHA-256",
58
- "content": "ee0b0a6cd08b2d962cd3b734369870b434f33eeafe874a7f0f0dd1c10b68f85c"
58
+ "content": "4bfe6564c3745ab76783640b91973036f6687155111052f3e3e34a3eb4dcd874"
59
59
  }
60
60
  ]
61
61
  },
@@ -275,7 +275,7 @@
275
275
  "hashes": [
276
276
  {
277
277
  "alg": "SHA-256",
278
- "content": "6980dd654d77e296f1ab20a9b7a7c43741fd4125ee44727183c63587421d5503"
278
+ "content": "f6ec73a2ef239b8195a321b5f20b6ab390d9a7004b42226b2a861181cf9e2d2f"
279
279
  }
280
280
  ]
281
281
  },
@@ -305,7 +305,7 @@
305
305
  "hashes": [
306
306
  {
307
307
  "alg": "SHA-256",
308
- "content": "6825da92d859a299b86bd9bf9910088a95665e0716346e55f0f117d991d25c9b"
308
+ "content": "86216fb6a40fcf270eada41c4a534600d2bcb2799052cbacfd64b459229b5bfe"
309
309
  }
310
310
  ]
311
311
  },
@@ -127,7 +127,12 @@ DZ_MISSING_NOTE = (
127
127
 
128
128
  # Any digit. Yes, any: a threshold, a dose, a year and a lab value are the same
129
129
  # character class, and no amount of context-sniffing reliably tells them apart.
130
- _DIGIT_RE = r"\d"
130
+ # NOT the regex `\d`. Round 5: the text scan used `\d` while the token scan used
131
+ # str.isdigit(), and `²` satisfies the second but not the first — so it was skipped as
132
+ # "a number" by one half and never refused by the other. Two different definitions of
133
+ # "digit" in one guard is a gap by construction. One predicate, used everywhere.
134
+ def _has_digit(text: str) -> bool:
135
+ return any(ch.isdigit() or ch.isnumeric() for ch in text)
131
136
 
132
137
  # A value SPELLED OUT is still a value. Round 2 walked `John's TSH was eight point
133
138
  # zero four` through the digit rule untouched. Requiring two number-words in a row
@@ -162,7 +167,11 @@ _IDENTIFIER_PATTERNS: Sequence[Tuple[str, str, bool]] = (
162
167
  # and a permitted number — laundered by punctuation. This reads the RAW text, where
163
168
  # the joiner is still visible. A SPACE is deliberately not a joiner here, or every
164
169
  # "the 2019 guideline" would be refused.
165
- (r"\b[A-Za-zА-Яа-я]{2,}[—–/._-]?\d{4,}\b",
170
+ # Any letter in ANY script, any single non-space separator, a long digit run. Round 5
171
+ # walked `ab:1234567` (colon was not in the joiner list) and `αβ—1234567` (Greek was
172
+ # not in the letter class) straight through. Enumerating joiners and alphabets is the
173
+ # same losing game as enumerating names; "not a space" and "any letter" are not.
174
+ (r"[^\W\d_]{2,}[^\w\s]?\d{4,}",
166
175
  "letters sitting against a long run of digits — that is an identifier, however it is punctuated", False),
167
176
  )
168
177
 
@@ -264,11 +273,11 @@ _IDENTIFIER_MIN_DIGITS = 4
264
273
  # runs when --allow-numbers was passed (digits are refused outright otherwise). It is
265
274
  # the narrowest of the three clauses and the only one that is still an enumeration.
266
275
  _PERSON_RE = (
267
- r"\b(patient|subject|participant|client|donor|volunteer|"
276
+ r"\b(patient|subject|participant|client|donor|volunteer|person|"
268
277
  r"woman|man|individual|child|infant|adult|"
269
278
  r"he|she|him|his|her|hers|"
270
279
  r"пациент(?!ы|ов|ам|ами|ах)\w*|больн(ой|ого|ому|ым|ом)|испытуем(ый|ого|ому|ым|ом)|"
271
- r"мужчина|мужчины\b|женщина|женщины\b|ребёнок|ребенок|доброволец|"
280
+ r"мужчина|мужчины\b|женщина|женщины\b|ребёнок|ребенок|доброволец|человек|"
272
281
  r"у него|у неё)\b"
273
282
  )
274
283
 
@@ -276,6 +285,7 @@ _PERSON_RE = (
276
285
  class LessonVerdict:
277
286
  ok: bool
278
287
  reasons: List[str]
288
+ numbers_allowed: bool = False
279
289
 
280
290
  @property
281
291
  def note(self) -> str:
@@ -283,6 +293,17 @@ class LessonVerdict:
283
293
  # NOT "safe": a shape detector cannot certify safety, and saying so would
284
294
  # transfer responsibility it does not have (Codex QE #2). It reports the
285
295
  # absence of a detected shape — the author still owns the content.
296
+ # The note must describe what was actually checked. Round 5: it said "no
297
+ # digits, no figures" even on the --allow-numbers path, where digits were
298
+ # present and consciously allowed — a message that contradicts the flag the
299
+ # caller just passed teaches the reader to stop reading messages.
300
+ if self.numbers_allowed:
301
+ return (
302
+ "no personal-data SHAPE detected (identifiers, capitalisation, person-plus-value). "
303
+ "Digits were ALLOWED by --allow-numbers and are present — you asserted the number "
304
+ "is knowledge, not a reading, and you own that call. This is not a safety "
305
+ "certificate: the guard checks shapes, not meaning."
306
+ )
286
307
  return (
287
308
  "no personal-data SHAPE detected (no digits, no identifiers, lower case). This is "
288
309
  "not a safety certificate: the guard checks shapes, not meaning — a lower-case "
@@ -331,11 +352,14 @@ def check_lesson(text: str, allow_numbers: bool = False) -> LessonVerdict:
331
352
  "flag accepts it"
332
353
  )
333
354
  break
334
- if has_digit or not has_alpha:
335
- continue # numbers are judged below; punctuation-only tokens carry nothing
355
+ if not has_alpha:
356
+ continue # a pure number is judged by the digit rule; punctuation carries nothing
336
357
  if index == 0:
337
358
  continue # an ordinary sentence start
338
- if token.islower() or _is_acronym(token):
359
+ # Capitalisation is checked for EVERY token carrying letters, digits or not.
360
+ # Round 5: digit-bearing tokens returned before this check, so `method Test2
361
+ # applies` passed — a Title Case name only had to carry a digit to become exempt.
362
+ if all(ch.islower() for ch in token if ch.isalpha()) or _is_acronym(token):
339
363
  continue
340
364
  reasons.append(
341
365
  f"a capitalised word ({token!r}) after the first one — a method lesson is written "
@@ -346,7 +370,7 @@ def check_lesson(text: str, allow_numbers: bool = False) -> LessonVerdict:
346
370
  break
347
371
 
348
372
  spelled = re.search(_SPELLED_NUMBER_RE, raw, flags=re.IGNORECASE | re.UNICODE)
349
- has_digits = re.search(_DIGIT_RE, raw) is not None or spelled is not None
373
+ has_digits = _has_digit(raw) or spelled is not None
350
374
  if has_digits and not allow_numbers:
351
375
  reasons.append(
352
376
  "the lesson contains digits — a method lesson rarely needs them, and a number is where "
@@ -367,7 +391,7 @@ def check_lesson(text: str, allow_numbers: bool = False) -> LessonVerdict:
367
391
  "a person is referred to alongside a number — that is a record about someone, "
368
392
  "and no flag makes it a method"
369
393
  )
370
- return LessonVerdict(not reasons, reasons)
394
+ return LessonVerdict(not reasons, reasons, numbers_allowed=allow_numbers)
371
395
 
372
396
 
373
397
  # ---------------------------------------------------------------- dz detection
@@ -812,6 +812,45 @@ class LearningBridgeTests(unittest.TestCase):
812
812
  self.assertTrue(self.lb._boost_note_present(" [0.90] (general) x\n " + tail))
813
813
 
814
814
 
815
+ # ---------------------------------------------------------------- round 5
816
+
817
+ def test_round5_ONE_definition_of_digit_everywhere(self):
818
+ """The text scan used the regex `\\d` and the token scan used str.isdigit().
819
+ `²` satisfies the second but not the first, so one half skipped it as a number
820
+ and the other never refused it. Two definitions of the same word inside one
821
+ guard is a gap by construction."""
822
+ self.assertFalse(self.lb.check_lesson("threshold is ²").ok)
823
+
824
+ def test_round5_capitalisation_is_checked_even_when_a_token_carries_a_digit(self):
825
+ """Digit-bearing tokens returned before the capitalisation check, so a Title
826
+ Case name only had to carry a digit to become exempt."""
827
+ self.assertFalse(self.lb.check_lesson("method Test2 applies", allow_numbers=True).ok)
828
+
829
+ def test_round5_joiners_and_alphabets_are_not_enumerated(self):
830
+ """`ab:1234567` passed because a colon was not in the joiner list, and
831
+ `αβ—1234567` because Greek was not in the letter class. Enumerating joiners and
832
+ alphabets is the same losing game as enumerating names."""
833
+ for leak in ("passport ab:1234567 identifies the holder",
834
+ "passport αβ—1234567 identifies the holder"):
835
+ with self.subTest(leak=leak):
836
+ self.assertFalse(self.lb.check_lesson(leak, allow_numbers=True).ok, leak)
837
+
838
+ def test_round5_the_word_person_is_a_person(self):
839
+ """The singular list enumerated patient/subject/participant and omitted the
840
+ plainest word of all."""
841
+ self.assertFalse(self.lb.check_lesson("this person has tsh 8.04", allow_numbers=True).ok)
842
+
843
+ def test_round5_the_acceptance_note_does_not_contradict_the_flag(self):
844
+ """It said "no digits, no figures" on the --allow-numbers path, where digits
845
+ were present and consciously allowed. A message that contradicts the flag the
846
+ caller just passed teaches the reader to stop reading messages."""
847
+ verdict = self.lb.check_lesson("a 56h fast lowers total testosterone by a third",
848
+ allow_numbers=True)
849
+ self.assertTrue(verdict.ok)
850
+ self.assertNotIn("no digits", verdict.note)
851
+ self.assertIn("ALLOWED by --allow-numbers", verdict.note)
852
+
853
+
815
854
  def test_a_real_failure_is_not_disguised_as_an_old_cli(self):
816
855
  """A crash, timeout or corrupt store must read as a failure, not as 'upgrade'."""
817
856
  original = self.lb._run_dz