diff --git a/plugins/kyberforge/.apm/skills/agent-audit/scripts/validate.sh b/plugins/kyberforge/.apm/skills/agent-audit/scripts/validate.sh index c24f739..46319f4 100755 --- a/plugins/kyberforge/.apm/skills/agent-audit/scripts/validate.sh +++ b/plugins/kyberforge/.apm/skills/agent-audit/scripts/validate.sh @@ -264,6 +264,16 @@ def _collect_package(pkg_dir, names): names.add(os.path.basename(path.rstrip('/')).lower()) for sub in ('.apm/agents/*.md', 'agents/*.md'): for path in glob.glob(os.path.join(safe_dir, sub)): + # The same rule one directory over, which until now had no + # counterpart here at all: the skills branch above tests for a + # SKILL.md, the agents branch took every glob hit on trust. A + # DIRECTORY named `ghost-agent.md` matches `*.md` and glob does not + # tell the two apart, so a leftover of that shape resolved a routing + # target on the machine holding it and dangled everywhere else — + # identical install-dependence, arriving through the one door + # nobody guarded. + if not os.path.isfile(path): + continue base = os.path.basename(path) if base.endswith('.agent.md'): base = base[:-len('.agent.md')] @@ -542,6 +552,34 @@ def known_targets(start_dir): # ambiguity to resolve, and an author who wants a route checked unconditionally # has two ways to say so. # +# BOTH FORMS ARE SWEPT FOR ON THEIR OWN inside a boundary sentence, and that is +# a repair of the promise above rather than a widening of it. Until the sweeps +# existed, notation was only ever seen as the OBJECT OF A ROUTE VERB (`use +# /name`) or as the tail of a `not ... ->` clause with no `;` or sentence end in +# between. Every one of these therefore exited 0 in total silence — no ERROR, no +# SUGGESTION, not even the target's name: +# Do not use for Y — /no-such-skill instead. +# Do not use for Y; /no-such-skill handles that. +# Do not use for Y (/no-such-skill covers it). +# Do not use for Y — that is /no-such-skill's job. +# Do not use for Y — defer to /no-such-skill. +# Do not use for Y — /no-such-skill. +# Do not use for Y; -> no-such-skill covers it. +# The target was never EXTRACTED, so the notation-first rule in _add() had +# nothing to apply itself to and the "always blocks" promise was false for the +# ordinary way an author writes the thing. The SUGGESTION tier made it worse +# than a gap: its printed remedy tells the author to "write it as `/name` or +# `-> name` and it will be checked properly", and taking that advice turned a +# visible SUGGESTION into silence — the gate teaching the one edit that blinds +# it. +# +# The sweeps are gated on the sentence carrying a BOUNDARY_MARKER, the same gate +# the backtick sweep uses, and NOTATION_SLASH refuses a token that is part of a +# PATH: a following `/`, or a `.` followed by a non-space, means +# `references/foo.md`, `docs/a/b.md` or `https://x/y`, not a route. A sentence's +# closing `.` is not followed by a non-space, so `— /no-such-skill.` still +# counts. +# # NAMESPACE: `plugin:skill` is live in this repo (native user-scope installs # still resolve `gitea:gitea-prs`), so the patterns admit an optional # `:` prefix and normalize_target() strips it before resolution. @@ -565,6 +603,23 @@ ROUTE_ANY = re.compile(r"\b%s\s+(?:the\s+|an?\s+)?%s" % (ROUTE_VERB, ANY_TARGET) CONT_MARKED = re.compile(r"\s*(?:or|and|/|,)\s*%s" % MARKED_TARGET, re.I) CONT_ANY = re.compile(r"\s*(?:or|and|/|,)\s*%s" % ANY_TARGET, re.I) ARROW_MARKED = re.compile(r"(?:->|→)\s*%s" % MARKED_TARGET, re.I) +# The two EXPLICIT ROUTE NOTATION sweeps, scoped to a boundary sentence by their +# caller. NOTATION_SLASH is deliberately not a reuse of MARKED_TARGET's `/name` +# alternative: that one only ever runs behind a route verb or an arrow, and the +# trailing lookahead here is the part that makes a FREE-STANDING sweep safe. +# NOTATION_ARROW is ARROW_BOUNDARY minus its leading `\bnot\b%s*?`, which is +# what made `Do not use for Y; -> no-such-skill covers it.` invisible: +# CLAUSE_BODY cannot cross the `;`, so the clause's own punctuation disarmed the +# check. Dropping that prefix costs the one false positive the bare-arrow bullet +# above names — a process chain ending in a hyphenated word, `Instead, reproduce +# -> minimise -> regression-test.` — and costs it only in a sentence that already +# carries a BOUNDARY_MARKER. That exposure is neither new nor larger: the same +# chain written `Do not use for X — reproduce -> regression-test.` was already a +# hard ERROR under ARROW_BOUNDARY, so this changes which boundary words reach the +# arrow, not whether prose can. An author who means the chain and not a route +# writes it in its own sentence, where neither pattern looks. +NOTATION_SLASH = re.compile(r"(?|→)\s*(%s)\b" % NAME_HYPH, re.I) # CLAUSE_BODY is what may sit between `Not` and the arrow, and it is NOT # `[^.;]`. That class cannot cross a `.`, so every boundary clause naming a # DOTTED FILENAME between the two — `.pre-commit-config.yaml`, `AGENTS.md`, @@ -740,6 +795,16 @@ def _extract_sentence(sentence): _add(out, sentence, match.group(1), match.start(1), match.end(1), strict=True, arrow=True) if boundary: + # Route notation wherever it sits in the clause, not only where a route + # verb or an arrow happens to precede it. See the EXPLICIT ROUTE + # NOTATION note in the header for the seven phrasings this recovers and + # for why silence was the failure mode. Neither sweep takes the follower + # test: _add() reads the notation first and both forms reach it marked. + for match in NOTATION_SLASH.finditer(sentence): + _add(out, sentence, match.group(1), match.start(1), match.end(1)) + for match in NOTATION_ARROW.finditer(sentence): + _add(out, sentence, match.group(1), match.start(1), match.end(1), + strict=True, arrow=True) for match in BACKTICK.finditer(sentence): _add(out, sentence, match.group(1), match.start(1), match.end(1)) return out @@ -1110,7 +1175,15 @@ def missing_reference_pointers(body, skill_dir): end = masked.find('\n', match.end()) if end < 0: end = len(masked) - if REFERENCE_PAST.search(masked[start:end]): + # The pointer's OWN SPAN is excised before the sweep. Run over the + # whole line, the past-tense test matched the very path it was judging, + # so a file exempted itself by its NAME: `references/deprecated-api.md`, + # `references/removed-flags.md` and `references/gone.md` produced no + # ERROR at all, while `references/missing.md` — an identical break — + # errored. The exemption is about what the SENTENCE says about the + # pointer, never about what the pointer is called. + line = masked[start:match.start()] + masked[match.end():end] + if REFERENCE_PAST.search(line): continue if REFERENCE_QUALIFIER.search(masked[start:match.start()]): continue diff --git a/plugins/kyberforge/.apm/skills/skill-audit/scripts/validate.sh b/plugins/kyberforge/.apm/skills/skill-audit/scripts/validate.sh index a871fe2..8934687 100755 --- a/plugins/kyberforge/.apm/skills/skill-audit/scripts/validate.sh +++ b/plugins/kyberforge/.apm/skills/skill-audit/scripts/validate.sh @@ -190,6 +190,16 @@ def _collect_package(pkg_dir, names): names.add(os.path.basename(path.rstrip('/')).lower()) for sub in ('.apm/agents/*.md', 'agents/*.md'): for path in glob.glob(os.path.join(safe_dir, sub)): + # The same rule one directory over, which until now had no + # counterpart here at all: the skills branch above tests for a + # SKILL.md, the agents branch took every glob hit on trust. A + # DIRECTORY named `ghost-agent.md` matches `*.md` and glob does not + # tell the two apart, so a leftover of that shape resolved a routing + # target on the machine holding it and dangled everywhere else — + # identical install-dependence, arriving through the one door + # nobody guarded. + if not os.path.isfile(path): + continue base = os.path.basename(path) if base.endswith('.agent.md'): base = base[:-len('.agent.md')] @@ -468,6 +478,34 @@ def known_targets(start_dir): # ambiguity to resolve, and an author who wants a route checked unconditionally # has two ways to say so. # +# BOTH FORMS ARE SWEPT FOR ON THEIR OWN inside a boundary sentence, and that is +# a repair of the promise above rather than a widening of it. Until the sweeps +# existed, notation was only ever seen as the OBJECT OF A ROUTE VERB (`use +# /name`) or as the tail of a `not ... ->` clause with no `;` or sentence end in +# between. Every one of these therefore exited 0 in total silence — no ERROR, no +# SUGGESTION, not even the target's name: +# Do not use for Y — /no-such-skill instead. +# Do not use for Y; /no-such-skill handles that. +# Do not use for Y (/no-such-skill covers it). +# Do not use for Y — that is /no-such-skill's job. +# Do not use for Y — defer to /no-such-skill. +# Do not use for Y — /no-such-skill. +# Do not use for Y; -> no-such-skill covers it. +# The target was never EXTRACTED, so the notation-first rule in _add() had +# nothing to apply itself to and the "always blocks" promise was false for the +# ordinary way an author writes the thing. The SUGGESTION tier made it worse +# than a gap: its printed remedy tells the author to "write it as `/name` or +# `-> name` and it will be checked properly", and taking that advice turned a +# visible SUGGESTION into silence — the gate teaching the one edit that blinds +# it. +# +# The sweeps are gated on the sentence carrying a BOUNDARY_MARKER, the same gate +# the backtick sweep uses, and NOTATION_SLASH refuses a token that is part of a +# PATH: a following `/`, or a `.` followed by a non-space, means +# `references/foo.md`, `docs/a/b.md` or `https://x/y`, not a route. A sentence's +# closing `.` is not followed by a non-space, so `— /no-such-skill.` still +# counts. +# # NAMESPACE: `plugin:skill` is live in this repo (native user-scope installs # still resolve `gitea:gitea-prs`), so the patterns admit an optional # `:` prefix and normalize_target() strips it before resolution. @@ -491,6 +529,23 @@ ROUTE_ANY = re.compile(r"\b%s\s+(?:the\s+|an?\s+)?%s" % (ROUTE_VERB, ANY_TARGET) CONT_MARKED = re.compile(r"\s*(?:or|and|/|,)\s*%s" % MARKED_TARGET, re.I) CONT_ANY = re.compile(r"\s*(?:or|and|/|,)\s*%s" % ANY_TARGET, re.I) ARROW_MARKED = re.compile(r"(?:->|→)\s*%s" % MARKED_TARGET, re.I) +# The two EXPLICIT ROUTE NOTATION sweeps, scoped to a boundary sentence by their +# caller. NOTATION_SLASH is deliberately not a reuse of MARKED_TARGET's `/name` +# alternative: that one only ever runs behind a route verb or an arrow, and the +# trailing lookahead here is the part that makes a FREE-STANDING sweep safe. +# NOTATION_ARROW is ARROW_BOUNDARY minus its leading `\bnot\b%s*?`, which is +# what made `Do not use for Y; -> no-such-skill covers it.` invisible: +# CLAUSE_BODY cannot cross the `;`, so the clause's own punctuation disarmed the +# check. Dropping that prefix costs the one false positive the bare-arrow bullet +# above names — a process chain ending in a hyphenated word, `Instead, reproduce +# -> minimise -> regression-test.` — and costs it only in a sentence that already +# carries a BOUNDARY_MARKER. That exposure is neither new nor larger: the same +# chain written `Do not use for X — reproduce -> regression-test.` was already a +# hard ERROR under ARROW_BOUNDARY, so this changes which boundary words reach the +# arrow, not whether prose can. An author who means the chain and not a route +# writes it in its own sentence, where neither pattern looks. +NOTATION_SLASH = re.compile(r"(?|→)\s*(%s)\b" % NAME_HYPH, re.I) # CLAUSE_BODY is what may sit between `Not` and the arrow, and it is NOT # `[^.;]`. That class cannot cross a `.`, so every boundary clause naming a # DOTTED FILENAME between the two — `.pre-commit-config.yaml`, `AGENTS.md`, @@ -666,6 +721,16 @@ def _extract_sentence(sentence): _add(out, sentence, match.group(1), match.start(1), match.end(1), strict=True, arrow=True) if boundary: + # Route notation wherever it sits in the clause, not only where a route + # verb or an arrow happens to precede it. See the EXPLICIT ROUTE + # NOTATION note in the header for the seven phrasings this recovers and + # for why silence was the failure mode. Neither sweep takes the follower + # test: _add() reads the notation first and both forms reach it marked. + for match in NOTATION_SLASH.finditer(sentence): + _add(out, sentence, match.group(1), match.start(1), match.end(1)) + for match in NOTATION_ARROW.finditer(sentence): + _add(out, sentence, match.group(1), match.start(1), match.end(1), + strict=True, arrow=True) for match in BACKTICK.finditer(sentence): _add(out, sentence, match.group(1), match.start(1), match.end(1)) return out @@ -1036,7 +1101,15 @@ def missing_reference_pointers(body, skill_dir): end = masked.find('\n', match.end()) if end < 0: end = len(masked) - if REFERENCE_PAST.search(masked[start:end]): + # The pointer's OWN SPAN is excised before the sweep. Run over the + # whole line, the past-tense test matched the very path it was judging, + # so a file exempted itself by its NAME: `references/deprecated-api.md`, + # `references/removed-flags.md` and `references/gone.md` produced no + # ERROR at all, while `references/missing.md` — an identical break — + # errored. The exemption is about what the SENTENCE says about the + # pointer, never about what the pointer is called. + line = masked[start:match.start()] + masked[match.end():end] + if REFERENCE_PAST.search(line): continue if REFERENCE_QUALIFIER.search(masked[start:match.start()]): continue diff --git a/plugins/kyberforge/skills/agent-audit/scripts/validate.sh b/plugins/kyberforge/skills/agent-audit/scripts/validate.sh index c24f739..46319f4 100755 --- a/plugins/kyberforge/skills/agent-audit/scripts/validate.sh +++ b/plugins/kyberforge/skills/agent-audit/scripts/validate.sh @@ -264,6 +264,16 @@ def _collect_package(pkg_dir, names): names.add(os.path.basename(path.rstrip('/')).lower()) for sub in ('.apm/agents/*.md', 'agents/*.md'): for path in glob.glob(os.path.join(safe_dir, sub)): + # The same rule one directory over, which until now had no + # counterpart here at all: the skills branch above tests for a + # SKILL.md, the agents branch took every glob hit on trust. A + # DIRECTORY named `ghost-agent.md` matches `*.md` and glob does not + # tell the two apart, so a leftover of that shape resolved a routing + # target on the machine holding it and dangled everywhere else — + # identical install-dependence, arriving through the one door + # nobody guarded. + if not os.path.isfile(path): + continue base = os.path.basename(path) if base.endswith('.agent.md'): base = base[:-len('.agent.md')] @@ -542,6 +552,34 @@ def known_targets(start_dir): # ambiguity to resolve, and an author who wants a route checked unconditionally # has two ways to say so. # +# BOTH FORMS ARE SWEPT FOR ON THEIR OWN inside a boundary sentence, and that is +# a repair of the promise above rather than a widening of it. Until the sweeps +# existed, notation was only ever seen as the OBJECT OF A ROUTE VERB (`use +# /name`) or as the tail of a `not ... ->` clause with no `;` or sentence end in +# between. Every one of these therefore exited 0 in total silence — no ERROR, no +# SUGGESTION, not even the target's name: +# Do not use for Y — /no-such-skill instead. +# Do not use for Y; /no-such-skill handles that. +# Do not use for Y (/no-such-skill covers it). +# Do not use for Y — that is /no-such-skill's job. +# Do not use for Y — defer to /no-such-skill. +# Do not use for Y — /no-such-skill. +# Do not use for Y; -> no-such-skill covers it. +# The target was never EXTRACTED, so the notation-first rule in _add() had +# nothing to apply itself to and the "always blocks" promise was false for the +# ordinary way an author writes the thing. The SUGGESTION tier made it worse +# than a gap: its printed remedy tells the author to "write it as `/name` or +# `-> name` and it will be checked properly", and taking that advice turned a +# visible SUGGESTION into silence — the gate teaching the one edit that blinds +# it. +# +# The sweeps are gated on the sentence carrying a BOUNDARY_MARKER, the same gate +# the backtick sweep uses, and NOTATION_SLASH refuses a token that is part of a +# PATH: a following `/`, or a `.` followed by a non-space, means +# `references/foo.md`, `docs/a/b.md` or `https://x/y`, not a route. A sentence's +# closing `.` is not followed by a non-space, so `— /no-such-skill.` still +# counts. +# # NAMESPACE: `plugin:skill` is live in this repo (native user-scope installs # still resolve `gitea:gitea-prs`), so the patterns admit an optional # `:` prefix and normalize_target() strips it before resolution. @@ -565,6 +603,23 @@ ROUTE_ANY = re.compile(r"\b%s\s+(?:the\s+|an?\s+)?%s" % (ROUTE_VERB, ANY_TARGET) CONT_MARKED = re.compile(r"\s*(?:or|and|/|,)\s*%s" % MARKED_TARGET, re.I) CONT_ANY = re.compile(r"\s*(?:or|and|/|,)\s*%s" % ANY_TARGET, re.I) ARROW_MARKED = re.compile(r"(?:->|→)\s*%s" % MARKED_TARGET, re.I) +# The two EXPLICIT ROUTE NOTATION sweeps, scoped to a boundary sentence by their +# caller. NOTATION_SLASH is deliberately not a reuse of MARKED_TARGET's `/name` +# alternative: that one only ever runs behind a route verb or an arrow, and the +# trailing lookahead here is the part that makes a FREE-STANDING sweep safe. +# NOTATION_ARROW is ARROW_BOUNDARY minus its leading `\bnot\b%s*?`, which is +# what made `Do not use for Y; -> no-such-skill covers it.` invisible: +# CLAUSE_BODY cannot cross the `;`, so the clause's own punctuation disarmed the +# check. Dropping that prefix costs the one false positive the bare-arrow bullet +# above names — a process chain ending in a hyphenated word, `Instead, reproduce +# -> minimise -> regression-test.` — and costs it only in a sentence that already +# carries a BOUNDARY_MARKER. That exposure is neither new nor larger: the same +# chain written `Do not use for X — reproduce -> regression-test.` was already a +# hard ERROR under ARROW_BOUNDARY, so this changes which boundary words reach the +# arrow, not whether prose can. An author who means the chain and not a route +# writes it in its own sentence, where neither pattern looks. +NOTATION_SLASH = re.compile(r"(?|→)\s*(%s)\b" % NAME_HYPH, re.I) # CLAUSE_BODY is what may sit between `Not` and the arrow, and it is NOT # `[^.;]`. That class cannot cross a `.`, so every boundary clause naming a # DOTTED FILENAME between the two — `.pre-commit-config.yaml`, `AGENTS.md`, @@ -740,6 +795,16 @@ def _extract_sentence(sentence): _add(out, sentence, match.group(1), match.start(1), match.end(1), strict=True, arrow=True) if boundary: + # Route notation wherever it sits in the clause, not only where a route + # verb or an arrow happens to precede it. See the EXPLICIT ROUTE + # NOTATION note in the header for the seven phrasings this recovers and + # for why silence was the failure mode. Neither sweep takes the follower + # test: _add() reads the notation first and both forms reach it marked. + for match in NOTATION_SLASH.finditer(sentence): + _add(out, sentence, match.group(1), match.start(1), match.end(1)) + for match in NOTATION_ARROW.finditer(sentence): + _add(out, sentence, match.group(1), match.start(1), match.end(1), + strict=True, arrow=True) for match in BACKTICK.finditer(sentence): _add(out, sentence, match.group(1), match.start(1), match.end(1)) return out @@ -1110,7 +1175,15 @@ def missing_reference_pointers(body, skill_dir): end = masked.find('\n', match.end()) if end < 0: end = len(masked) - if REFERENCE_PAST.search(masked[start:end]): + # The pointer's OWN SPAN is excised before the sweep. Run over the + # whole line, the past-tense test matched the very path it was judging, + # so a file exempted itself by its NAME: `references/deprecated-api.md`, + # `references/removed-flags.md` and `references/gone.md` produced no + # ERROR at all, while `references/missing.md` — an identical break — + # errored. The exemption is about what the SENTENCE says about the + # pointer, never about what the pointer is called. + line = masked[start:match.start()] + masked[match.end():end] + if REFERENCE_PAST.search(line): continue if REFERENCE_QUALIFIER.search(masked[start:match.start()]): continue diff --git a/plugins/kyberforge/skills/skill-audit/scripts/validate.sh b/plugins/kyberforge/skills/skill-audit/scripts/validate.sh index a871fe2..8934687 100755 --- a/plugins/kyberforge/skills/skill-audit/scripts/validate.sh +++ b/plugins/kyberforge/skills/skill-audit/scripts/validate.sh @@ -190,6 +190,16 @@ def _collect_package(pkg_dir, names): names.add(os.path.basename(path.rstrip('/')).lower()) for sub in ('.apm/agents/*.md', 'agents/*.md'): for path in glob.glob(os.path.join(safe_dir, sub)): + # The same rule one directory over, which until now had no + # counterpart here at all: the skills branch above tests for a + # SKILL.md, the agents branch took every glob hit on trust. A + # DIRECTORY named `ghost-agent.md` matches `*.md` and glob does not + # tell the two apart, so a leftover of that shape resolved a routing + # target on the machine holding it and dangled everywhere else — + # identical install-dependence, arriving through the one door + # nobody guarded. + if not os.path.isfile(path): + continue base = os.path.basename(path) if base.endswith('.agent.md'): base = base[:-len('.agent.md')] @@ -468,6 +478,34 @@ def known_targets(start_dir): # ambiguity to resolve, and an author who wants a route checked unconditionally # has two ways to say so. # +# BOTH FORMS ARE SWEPT FOR ON THEIR OWN inside a boundary sentence, and that is +# a repair of the promise above rather than a widening of it. Until the sweeps +# existed, notation was only ever seen as the OBJECT OF A ROUTE VERB (`use +# /name`) or as the tail of a `not ... ->` clause with no `;` or sentence end in +# between. Every one of these therefore exited 0 in total silence — no ERROR, no +# SUGGESTION, not even the target's name: +# Do not use for Y — /no-such-skill instead. +# Do not use for Y; /no-such-skill handles that. +# Do not use for Y (/no-such-skill covers it). +# Do not use for Y — that is /no-such-skill's job. +# Do not use for Y — defer to /no-such-skill. +# Do not use for Y — /no-such-skill. +# Do not use for Y; -> no-such-skill covers it. +# The target was never EXTRACTED, so the notation-first rule in _add() had +# nothing to apply itself to and the "always blocks" promise was false for the +# ordinary way an author writes the thing. The SUGGESTION tier made it worse +# than a gap: its printed remedy tells the author to "write it as `/name` or +# `-> name` and it will be checked properly", and taking that advice turned a +# visible SUGGESTION into silence — the gate teaching the one edit that blinds +# it. +# +# The sweeps are gated on the sentence carrying a BOUNDARY_MARKER, the same gate +# the backtick sweep uses, and NOTATION_SLASH refuses a token that is part of a +# PATH: a following `/`, or a `.` followed by a non-space, means +# `references/foo.md`, `docs/a/b.md` or `https://x/y`, not a route. A sentence's +# closing `.` is not followed by a non-space, so `— /no-such-skill.` still +# counts. +# # NAMESPACE: `plugin:skill` is live in this repo (native user-scope installs # still resolve `gitea:gitea-prs`), so the patterns admit an optional # `:` prefix and normalize_target() strips it before resolution. @@ -491,6 +529,23 @@ ROUTE_ANY = re.compile(r"\b%s\s+(?:the\s+|an?\s+)?%s" % (ROUTE_VERB, ANY_TARGET) CONT_MARKED = re.compile(r"\s*(?:or|and|/|,)\s*%s" % MARKED_TARGET, re.I) CONT_ANY = re.compile(r"\s*(?:or|and|/|,)\s*%s" % ANY_TARGET, re.I) ARROW_MARKED = re.compile(r"(?:->|→)\s*%s" % MARKED_TARGET, re.I) +# The two EXPLICIT ROUTE NOTATION sweeps, scoped to a boundary sentence by their +# caller. NOTATION_SLASH is deliberately not a reuse of MARKED_TARGET's `/name` +# alternative: that one only ever runs behind a route verb or an arrow, and the +# trailing lookahead here is the part that makes a FREE-STANDING sweep safe. +# NOTATION_ARROW is ARROW_BOUNDARY minus its leading `\bnot\b%s*?`, which is +# what made `Do not use for Y; -> no-such-skill covers it.` invisible: +# CLAUSE_BODY cannot cross the `;`, so the clause's own punctuation disarmed the +# check. Dropping that prefix costs the one false positive the bare-arrow bullet +# above names — a process chain ending in a hyphenated word, `Instead, reproduce +# -> minimise -> regression-test.` — and costs it only in a sentence that already +# carries a BOUNDARY_MARKER. That exposure is neither new nor larger: the same +# chain written `Do not use for X — reproduce -> regression-test.` was already a +# hard ERROR under ARROW_BOUNDARY, so this changes which boundary words reach the +# arrow, not whether prose can. An author who means the chain and not a route +# writes it in its own sentence, where neither pattern looks. +NOTATION_SLASH = re.compile(r"(?|→)\s*(%s)\b" % NAME_HYPH, re.I) # CLAUSE_BODY is what may sit between `Not` and the arrow, and it is NOT # `[^.;]`. That class cannot cross a `.`, so every boundary clause naming a # DOTTED FILENAME between the two — `.pre-commit-config.yaml`, `AGENTS.md`, @@ -666,6 +721,16 @@ def _extract_sentence(sentence): _add(out, sentence, match.group(1), match.start(1), match.end(1), strict=True, arrow=True) if boundary: + # Route notation wherever it sits in the clause, not only where a route + # verb or an arrow happens to precede it. See the EXPLICIT ROUTE + # NOTATION note in the header for the seven phrasings this recovers and + # for why silence was the failure mode. Neither sweep takes the follower + # test: _add() reads the notation first and both forms reach it marked. + for match in NOTATION_SLASH.finditer(sentence): + _add(out, sentence, match.group(1), match.start(1), match.end(1)) + for match in NOTATION_ARROW.finditer(sentence): + _add(out, sentence, match.group(1), match.start(1), match.end(1), + strict=True, arrow=True) for match in BACKTICK.finditer(sentence): _add(out, sentence, match.group(1), match.start(1), match.end(1)) return out @@ -1036,7 +1101,15 @@ def missing_reference_pointers(body, skill_dir): end = masked.find('\n', match.end()) if end < 0: end = len(masked) - if REFERENCE_PAST.search(masked[start:end]): + # The pointer's OWN SPAN is excised before the sweep. Run over the + # whole line, the past-tense test matched the very path it was judging, + # so a file exempted itself by its NAME: `references/deprecated-api.md`, + # `references/removed-flags.md` and `references/gone.md` produced no + # ERROR at all, while `references/missing.md` — an identical break — + # errored. The exemption is about what the SENTENCE says about the + # pointer, never about what the pointer is called. + line = masked[start:match.start()] + masked[match.end():end] + if REFERENCE_PAST.search(line): continue if REFERENCE_QUALIFIER.search(masked[start:match.start()]): continue diff --git a/scripts/skill-size-check.sh b/scripts/skill-size-check.sh index cf15c71..619b9ed 100755 --- a/scripts/skill-size-check.sh +++ b/scripts/skill-size-check.sh @@ -292,6 +292,16 @@ def _collect_package(pkg_dir, names): names.add(os.path.basename(path.rstrip('/')).lower()) for sub in ('.apm/agents/*.md', 'agents/*.md'): for path in glob.glob(os.path.join(safe_dir, sub)): + # The same rule one directory over, which until now had no + # counterpart here at all: the skills branch above tests for a + # SKILL.md, the agents branch took every glob hit on trust. A + # DIRECTORY named `ghost-agent.md` matches `*.md` and glob does not + # tell the two apart, so a leftover of that shape resolved a routing + # target on the machine holding it and dangled everywhere else — + # identical install-dependence, arriving through the one door + # nobody guarded. + if not os.path.isfile(path): + continue base = os.path.basename(path) if base.endswith('.agent.md'): base = base[:-len('.agent.md')] @@ -570,6 +580,34 @@ def known_targets(start_dir): # ambiguity to resolve, and an author who wants a route checked unconditionally # has two ways to say so. # +# BOTH FORMS ARE SWEPT FOR ON THEIR OWN inside a boundary sentence, and that is +# a repair of the promise above rather than a widening of it. Until the sweeps +# existed, notation was only ever seen as the OBJECT OF A ROUTE VERB (`use +# /name`) or as the tail of a `not ... ->` clause with no `;` or sentence end in +# between. Every one of these therefore exited 0 in total silence — no ERROR, no +# SUGGESTION, not even the target's name: +# Do not use for Y — /no-such-skill instead. +# Do not use for Y; /no-such-skill handles that. +# Do not use for Y (/no-such-skill covers it). +# Do not use for Y — that is /no-such-skill's job. +# Do not use for Y — defer to /no-such-skill. +# Do not use for Y — /no-such-skill. +# Do not use for Y; -> no-such-skill covers it. +# The target was never EXTRACTED, so the notation-first rule in _add() had +# nothing to apply itself to and the "always blocks" promise was false for the +# ordinary way an author writes the thing. The SUGGESTION tier made it worse +# than a gap: its printed remedy tells the author to "write it as `/name` or +# `-> name` and it will be checked properly", and taking that advice turned a +# visible SUGGESTION into silence — the gate teaching the one edit that blinds +# it. +# +# The sweeps are gated on the sentence carrying a BOUNDARY_MARKER, the same gate +# the backtick sweep uses, and NOTATION_SLASH refuses a token that is part of a +# PATH: a following `/`, or a `.` followed by a non-space, means +# `references/foo.md`, `docs/a/b.md` or `https://x/y`, not a route. A sentence's +# closing `.` is not followed by a non-space, so `— /no-such-skill.` still +# counts. +# # NAMESPACE: `plugin:skill` is live in this repo (native user-scope installs # still resolve `gitea:gitea-prs`), so the patterns admit an optional # `:` prefix and normalize_target() strips it before resolution. @@ -593,6 +631,23 @@ ROUTE_ANY = re.compile(r"\b%s\s+(?:the\s+|an?\s+)?%s" % (ROUTE_VERB, ANY_TARGET) CONT_MARKED = re.compile(r"\s*(?:or|and|/|,)\s*%s" % MARKED_TARGET, re.I) CONT_ANY = re.compile(r"\s*(?:or|and|/|,)\s*%s" % ANY_TARGET, re.I) ARROW_MARKED = re.compile(r"(?:->|→)\s*%s" % MARKED_TARGET, re.I) +# The two EXPLICIT ROUTE NOTATION sweeps, scoped to a boundary sentence by their +# caller. NOTATION_SLASH is deliberately not a reuse of MARKED_TARGET's `/name` +# alternative: that one only ever runs behind a route verb or an arrow, and the +# trailing lookahead here is the part that makes a FREE-STANDING sweep safe. +# NOTATION_ARROW is ARROW_BOUNDARY minus its leading `\bnot\b%s*?`, which is +# what made `Do not use for Y; -> no-such-skill covers it.` invisible: +# CLAUSE_BODY cannot cross the `;`, so the clause's own punctuation disarmed the +# check. Dropping that prefix costs the one false positive the bare-arrow bullet +# above names — a process chain ending in a hyphenated word, `Instead, reproduce +# -> minimise -> regression-test.` — and costs it only in a sentence that already +# carries a BOUNDARY_MARKER. That exposure is neither new nor larger: the same +# chain written `Do not use for X — reproduce -> regression-test.` was already a +# hard ERROR under ARROW_BOUNDARY, so this changes which boundary words reach the +# arrow, not whether prose can. An author who means the chain and not a route +# writes it in its own sentence, where neither pattern looks. +NOTATION_SLASH = re.compile(r"(?|→)\s*(%s)\b" % NAME_HYPH, re.I) # CLAUSE_BODY is what may sit between `Not` and the arrow, and it is NOT # `[^.;]`. That class cannot cross a `.`, so every boundary clause naming a # DOTTED FILENAME between the two — `.pre-commit-config.yaml`, `AGENTS.md`, @@ -768,6 +823,16 @@ def _extract_sentence(sentence): _add(out, sentence, match.group(1), match.start(1), match.end(1), strict=True, arrow=True) if boundary: + # Route notation wherever it sits in the clause, not only where a route + # verb or an arrow happens to precede it. See the EXPLICIT ROUTE + # NOTATION note in the header for the seven phrasings this recovers and + # for why silence was the failure mode. Neither sweep takes the follower + # test: _add() reads the notation first and both forms reach it marked. + for match in NOTATION_SLASH.finditer(sentence): + _add(out, sentence, match.group(1), match.start(1), match.end(1)) + for match in NOTATION_ARROW.finditer(sentence): + _add(out, sentence, match.group(1), match.start(1), match.end(1), + strict=True, arrow=True) for match in BACKTICK.finditer(sentence): _add(out, sentence, match.group(1), match.start(1), match.end(1)) return out @@ -1138,7 +1203,15 @@ def missing_reference_pointers(body, skill_dir): end = masked.find('\n', match.end()) if end < 0: end = len(masked) - if REFERENCE_PAST.search(masked[start:end]): + # The pointer's OWN SPAN is excised before the sweep. Run over the + # whole line, the past-tense test matched the very path it was judging, + # so a file exempted itself by its NAME: `references/deprecated-api.md`, + # `references/removed-flags.md` and `references/gone.md` produced no + # ERROR at all, while `references/missing.md` — an identical break — + # errored. The exemption is about what the SENTENCE says about the + # pointer, never about what the pointer is called. + line = masked[start:match.start()] + masked[match.end():end] + if REFERENCE_PAST.search(line): continue if REFERENCE_QUALIFIER.search(masked[start:match.start()]): continue