fix(gates): block a dangling /name route with no preceding verb
The header promised explicit route notation always blocks. It did not: /name
reached extraction only behind a ROUTE_VERB, so a target with no verb before it
was never extracted at all -- exit 0, no output. Taking the SUGGESTION's own
advice ('write it as /name and it will be checked properly') was the one edit
that blinded the gate.
Adds two notation sweeps gated on BOUNDARY_MARKER and routed through _add, plus
a path guard so file paths and URLs are not read as routes. Also excises the
matched pointer span before the REFERENCE_PAST sweep, so a reference file can no
longer exempt itself by its own filename, and guards the agent branch with the
isfile test the skills branch already had.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EJJrm5YmacbwMdzZpXcoti
This commit is contained in:
@@ -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
|
||||
# `<plugin>:` 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"(?<![\w./*-])/(%s)\b(?!/|\.\S)" % NAME_ANY, re.I)
|
||||
NOTATION_ARROW = 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
|
||||
|
||||
@@ -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
|
||||
# `<plugin>:` 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"(?<![\w./*-])/(%s)\b(?!/|\.\S)" % NAME_ANY, re.I)
|
||||
NOTATION_ARROW = 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
|
||||
|
||||
@@ -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
|
||||
# `<plugin>:` 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"(?<![\w./*-])/(%s)\b(?!/|\.\S)" % NAME_ANY, re.I)
|
||||
NOTATION_ARROW = 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
|
||||
|
||||
@@ -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
|
||||
# `<plugin>:` 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"(?<![\w./*-])/(%s)\b(?!/|\.\S)" % NAME_ANY, re.I)
|
||||
NOTATION_ARROW = 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
|
||||
|
||||
@@ -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
|
||||
# `<plugin>:` 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"(?<![\w./*-])/(%s)\b(?!/|\.\S)" % NAME_ANY, re.I)
|
||||
NOTATION_ARROW = 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
|
||||
|
||||
Reference in New Issue
Block a user