Fix catalog parser (fenced-block extraction) and drop overly strict local markdownlint config
This commit is contained in:
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"default": true,
|
||||
"MD013": {
|
||||
"tables": false
|
||||
},
|
||||
"MD024": {
|
||||
"siblings_only": true
|
||||
}
|
||||
}
|
||||
+34
-23
@@ -29,35 +29,46 @@ CACHE_GLOB = os.path.expanduser("~/.claude/plugins/cache/*/{name}/*/")
|
||||
# signals-catalog.md loader
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
KNOWN_SECTIONS = {"script-verbs", "judgment-verbs", "mechanical-tools", "thresholds"}
|
||||
|
||||
|
||||
def load_catalog(path):
|
||||
sections = {}
|
||||
current = None
|
||||
"""Reads only the fenced code block immediately following each
|
||||
recognized `## <section>` heading; everything else is prose for humans
|
||||
and is never parsed (see references/signals-catalog.md's own header for
|
||||
why: markdown and a hand-rolled comment syntax collide on bare `#`)."""
|
||||
with open(path, encoding="utf-8") as f:
|
||||
for raw in f:
|
||||
stripped = raw.strip()
|
||||
if not stripped:
|
||||
continue
|
||||
# Section headers look like comments (start with #) but must be
|
||||
# checked before generic comment-stripping discards them.
|
||||
m = re.match(r"^#{0,2}\s*\[([a-z-]+)\]$", stripped)
|
||||
if m:
|
||||
current = m.group(1)
|
||||
sections[current] = {} if current == "thresholds" else set()
|
||||
continue
|
||||
if stripped.startswith("#"):
|
||||
continue
|
||||
line = raw.split("#", 1)[0].strip()
|
||||
if not line or current is None:
|
||||
continue
|
||||
if current == "thresholds":
|
||||
text = f.read()
|
||||
|
||||
sections = {}
|
||||
heading_re = re.compile(r"^##\s+([a-z-]+)\s*$", re.MULTILINE)
|
||||
for m in heading_re.finditer(text):
|
||||
name = m.group(1)
|
||||
if name not in KNOWN_SECTIONS:
|
||||
continue
|
||||
block = re.search(r"```\n(.*?)```", text[m.end():], re.DOTALL)
|
||||
if not block:
|
||||
continue
|
||||
body = block.group(1)
|
||||
if name == "thresholds":
|
||||
values = {}
|
||||
for raw in body.split("\n"):
|
||||
line = raw.split("#", 1)[0].strip()
|
||||
if ":" in line:
|
||||
k, v = line.split(":", 1)
|
||||
sections[current][k.strip()] = float(v.strip())
|
||||
else:
|
||||
for item in line.split(","):
|
||||
values[k.strip()] = float(v.strip())
|
||||
sections[name] = values
|
||||
else:
|
||||
items = set()
|
||||
for raw in body.split("\n"):
|
||||
for item in raw.split(","):
|
||||
item = item.strip()
|
||||
if re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", item):
|
||||
sections[current].add(item)
|
||||
items.add(item)
|
||||
sections[name] = items
|
||||
missing = KNOWN_SECTIONS - sections.keys()
|
||||
if missing:
|
||||
sys.exit(f"error: {path} is missing section(s): {', '.join(sorted(missing))}")
|
||||
return sections
|
||||
|
||||
|
||||
|
||||
@@ -2,106 +2,93 @@
|
||||
|
||||
Loaded at runtime by `bin/plugin-inventory`. Kept out of the script body so a
|
||||
lexicon or threshold change is a reviewable diff, not a code change — per
|
||||
`design/plan.md` §3.
|
||||
`design/plan.md` §3. `plugin-inventory` parses this file itself — a genuine
|
||||
`pure-script` read (T1–T5 all pass: closed format, one right parse,
|
||||
deterministic, no missing context, bounded grammar), so it is not a
|
||||
candidate for anything more than what it already is.
|
||||
|
||||
Format: one `key: value` per line, grouped under `[section]` headers. Lines
|
||||
starting with `#` are comments. Whitespace-insensitive; commas separate list
|
||||
values. `plugin-inventory` parses this file itself — a genuine `pure-script`
|
||||
read (T1–T5 all pass: closed format, one right parse, deterministic, no
|
||||
missing context, bounded grammar), so it is not a candidate for anything more
|
||||
than what it already is.
|
||||
Format: each `## <section>` heading below is followed by exactly one fenced
|
||||
code block, which is the only part the parser reads — comma-separated lists
|
||||
for the verb/tool sections, `key: value` lines (with `#` trailing comments)
|
||||
for `## thresholds`. Everything outside a fenced block is prose for humans
|
||||
and is ignored by the parser, so it's free to use normal markdown, including
|
||||
literal `#` at the start of a line.
|
||||
|
||||
## [script-verbs]
|
||||
## script-verbs
|
||||
|
||||
Occurrences count toward the script-verb signal. Matched case-insensitively,
|
||||
whole-word, against skill/agent body prose (frontmatter excluded).
|
||||
|
||||
```
|
||||
count, sum, rank, sort, diff, parse, validate, enumerate, dedupe, dedup,
|
||||
deduplicate, format, compare, aggregate, glob, grep, filter, normalize,
|
||||
normalise, tabulate, checksum, hash, lint, tally, group, join, merge, split,
|
||||
truncate, paginate, timestamp, serialize, serialise, deserialize, encode,
|
||||
decode
|
||||
```
|
||||
|
||||
## [judgment-verbs]
|
||||
## judgment-verbs
|
||||
|
||||
Occurrences count toward the judgment-verb signal. Same matching rule.
|
||||
|
||||
```
|
||||
decide, judge, explain, prioritize, prioritise, name, write, weigh,
|
||||
interpret, recommend, assess, evaluate, justify, persuade, negotiate,
|
||||
empathize, empathise, phrase, rephrase, summarize, summarise, synthesize,
|
||||
synthesise, disambiguate, reconcile, adjudicate, arbitrate, narrate
|
||||
```
|
||||
|
||||
# Note: summarize/synthesize sit in judgment, not script — compressing text
|
||||
Note: summarize/synthesize sit in judgment, not script — compressing text
|
||||
into a good summary is not a total function of its inputs (T4), even though
|
||||
"summarize" sounds mechanical. If a step turns out to be a fixed-format
|
||||
extraction (e.g. "summarize" really means "pull these five fields"), that is
|
||||
a digest problem — flag it as low/medium, not high, until a human confirms
|
||||
the extraction is exhaustive.
|
||||
|
||||
# into a good summary is not a total function of its inputs (T4), even though
|
||||
|
||||
# "summarize" sounds mechanical. If a step turns out to be a fixed-format
|
||||
|
||||
# extraction (e.g. "summarize" really means "pull these five fields"), that is
|
||||
|
||||
# a digest problem — flag it as low/medium, not high, until a human confirms
|
||||
|
||||
# the extraction is exhaustive
|
||||
|
||||
## [mechanical-tools]
|
||||
## mechanical-tools
|
||||
|
||||
Tools whose presence in an agent's `tools:` list leans that agent toward
|
||||
"built to fetch." Used for `mechanical_tool_share`.
|
||||
|
||||
```
|
||||
Bash, Read, Grep, Glob, WebFetch, WebSearch, LS, NotebookRead,
|
||||
ListMcpResourcesTool, ReadMcpResourceTool, ReadMcpResourceDirTool, TaskGet,
|
||||
TaskList, TaskOutput, Monitor
|
||||
```
|
||||
|
||||
# Deliberately excluded (judgment-implying, even when mechanical-sounding)
|
||||
Deliberately excluded (judgment-implying, even when mechanical-sounding):
|
||||
Edit, Write, NotebookEdit (produce durable state — a mistake here is
|
||||
consequential, T5/P4), AskUserQuestion (is itself an escalation), Agent
|
||||
(spawns judgment, doesn't replace it).
|
||||
|
||||
# Edit, Write, NotebookEdit (produce durable state — a mistake here is
|
||||
|
||||
# consequential, T5/P4), AskUserQuestion (is itself an escalation), Agent
|
||||
|
||||
# (spawns judgment, doesn't replace it)
|
||||
|
||||
## [thresholds]
|
||||
## thresholds
|
||||
|
||||
```
|
||||
verb_ratio_high: 0.6
|
||||
|
||||
# verb_ratio (script / (script+judgment)) above this with zero bin/ script
|
||||
|
||||
# coverage is the loudest static smell per spec.md §5.1
|
||||
# coverage is the loudest static smell per spec.md §5.1.
|
||||
|
||||
script_coverage_low: 0.15
|
||||
|
||||
# script LOC / (skill+agent body words) below this, combined with
|
||||
|
||||
# verb_ratio_high, is the target profile for a real candidate
|
||||
# verb_ratio_high, is the target profile for a real candidate.
|
||||
|
||||
procedural_density_high: 0.35
|
||||
|
||||
# fraction of a skill body that is numbered/imperative steps containing a
|
||||
|
||||
# literal command, above which the skill reads as a recipe being re-derived
|
||||
|
||||
# every invocation rather than prose reasoning
|
||||
# every invocation rather than prose reasoning.
|
||||
|
||||
mechanical_tool_share_high: 0.75
|
||||
|
||||
# fraction of an agent's declared tools that are in [mechanical-tools]
|
||||
|
||||
# above which the agent's own tool allowlist argues it was built to fetch
|
||||
# fraction of an agent's declared tools that are in mechanical-tools, above
|
||||
# which the agent's own tool allowlist argues it was built to fetch.
|
||||
|
||||
duplicate_block_min_skills: 2
|
||||
|
||||
# a fenced command block (after normalization — see FR-3.5 masking rules
|
||||
|
||||
# a fenced command block (after normalization — see FR-3.5 masking rules,
|
||||
# applied identically here) appearing in at least this many distinct skills
|
||||
|
||||
# counts as "a shared script that was never written."
|
||||
|
||||
hook_prose_drift_min_shared_tokens: 2
|
||||
|
||||
# a hook script and a skill body sharing at least this many identifier-shaped
|
||||
|
||||
# tokens (snake_case, SCREAMING_CASE, or a literal filename — NOT plain
|
||||
|
||||
# English words, which overlap constantly between topically-related prose)
|
||||
|
||||
# counts as the hook's logic being restated in prose — two sources of truth
|
||||
# counts as the hook's logic being restated in prose — two sources of truth.
|
||||
```
|
||||
|
||||
@@ -5,4 +5,6 @@ model: sonnet
|
||||
tools: Bash, Read, Grep, Glob, Edit
|
||||
---
|
||||
|
||||
# fetcher
|
||||
|
||||
Fetches and greps things.
|
||||
|
||||
+2
-2
@@ -17,5 +17,5 @@ git status --short
|
||||
```
|
||||
|
||||
| Finding | Action |
|
||||
|---|---|
|
||||
| foo | bar |
|
||||
| ------- | ------ |
|
||||
| foo | bar |
|
||||
|
||||
Reference in New Issue
Block a user