rulereceipt 0.1.55 → 0.1.56

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.
@@ -293,6 +293,25 @@ const FILE_PATH_PATTERN = /^[^\s]*(\.(json|ya?ml|toml|md|env|lock|ini|cfg|conf|x
293
293
  // file, not merely mentioning one (e.g. "read `config.yaml` before
294
294
  // starting" names a path but isn't a protection rule).
295
295
  const FILE_MUTATION_INTENT = /\b(modif|chang|edit|delet|remov|overwrit|touch|writ|creat|rename|mov)\w*\b/i;
296
+ /**
297
+ * A backtick literal distinctive enough to check INSIDE written code without
298
+ * matching ordinary prose. A single token (no whitespace), not a shell flag,
299
+ * carrying code punctuation (`. - / @ # :`) and at least three alphanumerics —
300
+ * so `lucide-react`, `#0af`, `@deprecated`, `react-dom` qualify, while plain
301
+ * words (`TODO`, `name`), shell commands (`git push --force`) and bare flags
302
+ * do not. Used to route a FORBID rule's token to codeContent: found in
303
+ * Write/Edit content it is an ACTION (a real FAIL), not a mention. Added
304
+ * 2026-09-28 to move import/value-style rules out of the UNCLEAR bucket.
305
+ */
306
+ function isContentToken(literal) {
307
+ if (/\s/.test(literal))
308
+ return false;
309
+ if (literal.startsWith("-"))
310
+ return false;
311
+ if (!/[.\-/@#:]/.test(literal))
312
+ return false;
313
+ return (literal.match(/[A-Za-z0-9]/g) ?? []).length >= 3;
314
+ }
296
315
  // Catches rules like "add tests for every change" or "every new function
297
316
  // needs a test" - no literal backtick token to pattern-match, so without
298
317
  // this they'd fall all the way through to judgment (an LLM call) even
@@ -485,6 +504,14 @@ function polarityWasInferred(rule) {
485
504
  */
486
505
  const EMOJI_SUBJECT = /\bemojis?\b|\bemoticons?\b/i;
487
506
  const EMOJI_FORBID = /\b(no|never|avoid|don't|do not|without|free of|refrain from|must not|shall not|not use|zero)\b/i;
507
+ // A permissive threshold means the rule allows SOME emoji — not a zero-ban the
508
+ // deterministic checker can decide.
509
+ const EMOJI_THRESHOLD = /\b(?:liberal|sparing|minimal|excessive|overus|one or two|a couple|a few|maximum|at most|no more than|too many|limit)\w*/i;
510
+ // A rule scoped only to an artifact the checker can't see (it reads the
511
+ // assistant's chat text, not posts/commits/READMEs) can't be verified from
512
+ // that text — unless it also names the chat/output the checker DOES see.
513
+ const EMOJI_OFFTARGET = /\b(?:posts?|commits?|pull requests?|prs?|readme|docs?|documentation|blog|articles?|captions?|changelog)\b/i;
514
+ const EMOJI_ONTARGET = /\b(?:output|repl(?:y|ies)|response|chat|message|answer|conversation|everywhere|anywhere)\b/i;
488
515
  function isEmojiRule(rule) {
489
516
  const text = `${rule.title} ${rule.text}`;
490
517
  if (!EMOJI_SUBJECT.test(text))
@@ -497,7 +524,17 @@ function isEmojiRule(rule) {
497
524
  if (!m || m.index === undefined)
498
525
  return false;
499
526
  const window = text.slice(Math.max(0, m.index - 60), m.index + 40);
500
- return EMOJI_FORBID.test(window);
527
+ if (!EMOJI_FORBID.test(window))
528
+ return false;
529
+ // Real false positive 2026-09-28: "one or two emojis per post is the maximum"
530
+ // (a THRESHOLD, about POSTS) failed on a ✅ in a chat reply. A permissive
531
+ // threshold, or a rule scoped only to an artifact this checker can't see,
532
+ // is not a confident deterministic FAIL — it goes to judgment.
533
+ if (EMOJI_THRESHOLD.test(text))
534
+ return false;
535
+ if (EMOJI_OFFTARGET.test(text) && !EMOJI_ONTARGET.test(text))
536
+ return false;
537
+ return true;
501
538
  }
502
539
  /**
503
540
  * A rule that forbids an AI-authorship mark in git commits, PRs or comments.
@@ -668,6 +705,16 @@ export function classifyRule(rule) {
668
705
  if (filePath && FILE_MUTATION_INTENT.test(text)) {
669
706
  return { kind: "fileLifecycle", rule, filePath, polarity, polarityInferred };
670
707
  }
708
+ // A forbid rule naming a distinctive token (an import, a value) is a real
709
+ // violation when the agent WRITES it — codeContent checks Write/Edit content
710
+ // only, so this is an action not a mention. Plain words and shell commands
711
+ // are excluded by isContentToken; protected files already routed above.
712
+ if (polarity === "forbid") {
713
+ const contentTokens = [...patterns].filter(isContentToken);
714
+ if (contentTokens.length > 0) {
715
+ return { kind: "codeContent", rule, patterns: contentTokens, polarity, polarityInferred };
716
+ }
717
+ }
671
718
  return { kind: "deterministic", rule, patterns: [...patterns], polarity, polarityInferred };
672
719
  }
673
720
  export function classifyRules(rules) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rulereceipt",
3
- "version": "0.1.55",
3
+ "version": "0.1.56",
4
4
  "description": "Checks whether your AI coding agent followed your rules, with evidence. Works with Claude Code (Codex in testing); reads CLAUDE.md, AGENTS.md, Cursor, Copilot and Windsurf rules.",
5
5
  "repository": {
6
6
  "type": "git",