Skip to content

Commit a163f1b

Browse files
authored
fix: stale-counts policy correction and example-dialogue skip (#38)
The cursor-plugin and mcp-server type-level skip_checks entries in drift-checker.config.json have been suppressing stale-counts entirely on 8 of 9 tool repos since v1.7.5. This was a wider suppression than intended - the policy was meant to exempt narrative-aggregate prose in AGENTS.md and CLAUDE.md, not silence the check on every file in those repo types. Empirical evidence of the over-suppression: the DTD#13 investigation discovered an off-by-one drift in steam-api-reference/SKILL.md (TMHSDigital/Steam-Cursor-Plugin#13) that the production drift-check had been silently missing. Three changes: 1. Removes the type-level stale-counts entries from standards/drift-checker.config.json. The cursor-plugin type now skips nothing; mcp-server still skips required-refs. 2. Adds a hardcoded AGENTS.md / CLAUDE.md skip in stale_counts.py. The narrative-aggregate convention belongs in the check's logic, not in repo-level config that over-suppressed. 3. Implements DTD#37: skips counts inside ## Example sections and on lines beginning with **User:** / **Assistant:** dialogue markers. Handles the category C false positive identified in Home-Lab's secrets-management/SKILL.md. Regression tests added including the Steam off-by-one as a fixture so that future re-suppression cannot go unnoticed. Existing AGENTS.md-based pragma and emergency-skip tests were ported to SKILL.md (the only file type stale-counts now scrutinizes by default in the absence of hardcoded skips). Closes #12, closes #37. Refs #13. Made-with: Cursor Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com>
1 parent 6bdac4f commit a163f1b

5 files changed

Lines changed: 272 additions & 42 deletions

File tree

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.8.5
1+
1.9.0

scripts/drift_check/checks/stale_counts.py

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,27 @@
1616
scan inside fenced code blocks, by design: even in an example block, a
1717
stale count is stale information that will confuse readers once the real
1818
numbers drift.
19+
20+
Two hardcoded scope rules (DTD#12 v1.9.0):
21+
22+
1. ``AGENTS.md`` and ``CLAUDE.md`` are skipped wholesale. By ecosystem
23+
convention these files carry narrative-aggregate prose ("177 skills,
24+
71 rules") that is descriptive, not truth-bearing. Aggregate-truth
25+
enforcement for those repos belongs in a CFX/Unity-style
26+
``validate-counts`` job against ``README.md``. Encoding this in the
27+
check (rather than per-repo config) keeps the policy where it is
28+
structurally true: those files' role in the standard.
29+
30+
2. Lines inside ``## Example`` sections, and lines beginning with
31+
``**User:**`` / ``**Assistant:**`` markers, are skipped (DTD#37). The
32+
numbers in roleplay dialogue are illustrative, not claims about the
33+
skill's actual surface.
1934
"""
2035
from __future__ import annotations
2136

2237
import re
2338
from pathlib import Path
24-
from typing import Iterable, List, Tuple
39+
from typing import Iterable, List, Set, Tuple
2540

2641
from ..types import Finding, RepoSnapshot
2742

@@ -41,6 +56,27 @@
4156
_STRONG_UNITS = ("skill", "rule", "mcp tool", "command")
4257
_YAML_FENCE_RE = re.compile(rb"^---\s*$", re.MULTILINE)
4358

59+
# DTD#12 (v1.9.0): files always skipped by stale-counts regardless of
60+
# config. Match by basename, case-insensitive, to tolerate ``Agents.md``
61+
# or ``claude.md`` variants. The narrative-aggregate convention applies
62+
# to the file's role in the standard, not to a particular casing.
63+
_HARDCODED_SKIP_NAMES: frozenset[str] = frozenset({"agents.md", "claude.md"})
64+
65+
# DTD#37: section headings that introduce roleplay/example content. Any
66+
# ``## Example...`` heading is treated as the start of a skipped section
67+
# until the next ``##``-or-shallower heading. We deliberately match the
68+
# generic ``## Example`` prefix so ``## Example Interaction``, ``## Example
69+
# Interactions``, ``## Example Usage``, etc. all qualify.
70+
_EXAMPLE_HEADING_RE = re.compile(rb"^##\s+Example\b", re.IGNORECASE)
71+
_HEADING_RE = re.compile(rb"^(#{1,6})\s+\S")
72+
# DTD#37: lines starting with these dialogue markers are skipped wherever
73+
# they appear, including outside an example section. Markdown bold form
74+
# wraps the colon: ``**User:**`` / ``**Assistant:**``. Tolerant of leading
75+
# whitespace and trailing content on the same line.
76+
_DIALOGUE_LINE_RE = re.compile(
77+
rb"^\s*\*\*(User|Assistant)\s*:\*\*", re.IGNORECASE
78+
)
79+
4480

4581
def _strip_frontmatter(content: bytes) -> bytes:
4682
"""If the file starts with a YAML frontmatter block, strip it and
@@ -85,6 +121,34 @@ def _frontmatter_line_offset(content: bytes, body: bytes) -> int:
85121
return content.count(b"\n", 0, consumed)
86122

87123

124+
def _example_dialogue_lines(body: bytes) -> Set[int]:
125+
"""Return the set of body-relative (1-indexed) line numbers that fall
126+
inside an ``## Example`` section or that themselves are a roleplay
127+
dialogue line (``**User:**``/``**Assistant:**``). Counts on these
128+
lines are illustrative, not aggregate truth claims (DTD#37).
129+
130+
Section scoping: an ``## Example`` heading opens a region. The region
131+
closes at the next ``##``-or-shallower heading (``# `` or ``## ``),
132+
or end-of-file. Deeper headings (``### `` etc.) stay inside.
133+
"""
134+
skipped: Set[int] = set()
135+
in_example = False
136+
for idx, raw in enumerate(body.split(b"\n"), start=1):
137+
line = raw.rstrip(b"\r")
138+
if _EXAMPLE_HEADING_RE.match(line):
139+
in_example = True
140+
skipped.add(idx)
141+
continue
142+
heading_match = _HEADING_RE.match(line)
143+
if heading_match and len(heading_match.group(1)) <= 2:
144+
in_example = False
145+
if in_example:
146+
skipped.add(idx)
147+
elif _DIALOGUE_LINE_RE.match(line):
148+
skipped.add(idx)
149+
return skipped
150+
151+
88152
class StaleCountsCheck:
89153
name: str = NAME
90154

@@ -94,6 +158,12 @@ def run(self, snapshot: RepoSnapshot) -> Iterable[Finding]:
94158

95159
out: List[Finding] = []
96160
for rel_path, file in snapshot.files.items():
161+
# DTD#12 (v1.9.0): hardcoded narrative-aggregate skip.
162+
# AGENTS.md and CLAUDE.md describe the plugin in prose;
163+
# aggregate-truth lives in README.md per ecosystem convention.
164+
if Path(rel_path).name.lower() in _HARDCODED_SKIP_NAMES:
165+
continue
166+
97167
pragma = next(
98168
(p for p in file.pragmas if p.check_name == NAME), None
99169
)
@@ -114,7 +184,10 @@ def run(self, snapshot: RepoSnapshot) -> Iterable[Finding]:
114184

115185
body = _strip_frontmatter(file.content)
116186
offset = _frontmatter_line_offset(file.content, body)
187+
example_lines = _example_dialogue_lines(body)
117188
for count, unit, line in _iter_counts(body):
189+
if line in example_lines:
190+
continue
118191
actual_line = line + offset
119192
strong = _unit_is_strong(unit)
120193
message = (

standards/drift-checker.config.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66
},
77
"types": {
88
"cursor-plugin": {
9-
"skip_checks": ["stale-counts"]
9+
"skip_checks": []
1010
},
1111
"mcp-server": {
12-
"skip_checks": ["required-refs", "stale-counts"]
12+
"skip_checks": ["required-refs"]
1313
}
1414
},
1515
"repos": {
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Widget API reference
2+
3+
This skill documents the widget tools.
4+
5+
**Requires `WIDGET_API_KEY` (8 tools):**
6+
7+
| Tool | Description |
8+
|------|-------------|
9+
| `widget_one` | Stub |
10+
| `widget_two` | Stub |
11+
| `widget_three` | Stub |
12+
| `widget_four` | Stub |
13+
| `widget_five` | Stub |
14+
| `widget_six` | Stub |
15+
| `widget_seven` | Stub |
16+
| `widget_eight` | Stub |
17+
18+
The widget catalog provides 17 skills and 10 rules.

0 commit comments

Comments
 (0)