External syntax variants
For PHP, Ruby, or Swift, load the selected skill with its sibling provider and read the matching on-demand guide before execution:
Each supports one declared direct-call enclosure condition only. The guides own the exact commands, standards inputs, native gates, and semantic non-claims.
/find-standard-gaps
Kotlin/JVM 2.4.10 branch
Trigger this branch only for a manifest-selected .kt target and explicit
host-owned standards JSON. Keep sibling _kotlin, read
../_kotlin/GUIDE.md, and enter through
scripts/scan_coverage_kotlin.py. It supports one direct call-spelling plus
lexical if-enclosure condition. Callee identity, aliases/extensions,
overload selection, reflection, generated/Java/Gradle/runtime behavior,
general policy, detekt/ktlint, plugins, and frameworks remain unresolved.
C# 14 / .NET 10 branch
Use scripts/scan_coverage_csharp.py with the sibling _csharp provider and
an explicit ideas file; run it with --help for the exact CLI. It measures one
configured direct call-spelling plus enclosed_by: "if" Roslyn syntax
condition over the current compiled manifest closure. Callee identity,
aliases, runtime control flow, and general policy compliance remain unresolved.
C++20 branch
Use scripts/scan_coverage_cpp.py with the sibling _cpp provider and an
explicit ideas file; run it with --help for the exact CLI. It measures one
configured direct-call/if syntax condition over a current complete C++20
compiler-owned graph with exact namespace/signature/overload boundaries. It
does not prove callee behavior, ODR/ABI, specializations, dispatch, or external
variants.
C17 branch
Use scripts/scan_coverage_c.py with the sibling _c-syntax provider; run the
script with --help for the exact CLI. This external-library branch supports
direct call spelling plus the configured syntactic enclosed_by: "if"
condition only. Callee identity, aliases, function pointers, runtime control
flow, general policy, and frameworks remain unresolved.
Dart v1
Dart v1 supports one frozen condition: a direct spelled call is enclosed by a
try statement. It does not resolve callee identity, aliases, receiver calls,
exception flow, or framework policy. Copy sibling _dart/scripts and its
locked public-analyzer tool; offline tool absence is visible partial evidence.
SKILL_ROOT=".agents/skills/on-demand/find-standard-gaps"
python3 "${SKILL_ROOT}/scripts/scan_coverage_dart.py" \
--project-root "$PWD" --target . \
--ideas "${DART_STANDARDS:?Set the host-owned Dart standards JSON}" \
--output-dir "$PWD/reports/standard-gaps/dart" \
--native-test "${DART_DIRECT_TEST:?Set a dependency-free direct test path}" \
--smoke "${DART_SMOKE:?Set a direct smoke entrypoint}" \
--smoke-stdout "${DART_EXPECTED_STDOUT:?Set exact stdout including any newline}"
Rust v1
Rust v1 supports one declared syntax condition: whether a direct spelled call
is enclosed by match. It does not resolve aliases, receivers, types, traits,
or the intended API. Copy sibling _rust-syntax; broader detector conditions
remain pending rather than silently degrading to lexical clean.
SKILL_ROOT=".agents/skills/on-demand/find-standard-gaps"
python3 "${SKILL_ROOT}/scripts/scan_coverage_rust.py" \
--project-root "$PWD" --target src \
--ideas "$PWD/standards-rust.json" \
--output-dir "$PWD/reports/standard-gaps/rust"
You are the orchestrator for a SUSPECT skill. Given a standards file — a JSON file of declared baseline standards, each carrying an executable detector — you scan the codebase and report every coverage gap: a site where a standard's triggering situation holds but the standard is not applied.
This is the value-coverage idea made operational: a good standard is worth nothing at the sites that don't use it. The skill generalizes the project's hand-written AST lints — instead of authoring one lint per rule, a rule is declared once as a standard and coverage-checked.
The skill is deterministic — scripts/scan_coverage.py does the
work; there is no scout fan-out. The detector model (how ast and
grep detectors work, why ast is preferred) is in
knowledge/detector-model.md.
How success is judged
coverage.mdandcoverage.jsonagree on every standard's status. Grade the run by these artifacts plus the pastedscan_coverage.pystdout/stderr, not by an executor's claim that the scan "passed".coverage.mdenumerates every in-scope standard's coverage cells — situation-site count, gap count, coverage % — with no standard silently dropped (manual/skillstandards reported as skipped).- Each standard carries an explicit analyzability verdict:
gated_out,language_unsupported,no_files_matched,partial, anderrorare surfaced as non-passing statuses, never passed off as 0 gaps / compliant. - Clean standards are named as positive results only when their status is
scannedand their gap count is 0. - No production edits — the run writes only under
reports/standard-gaps/scan-<TS>/.
Core beliefs
- Absence is the finding. Structural skills audit code that exists; this one finds a standard that should be at a site and isn't. That is a different, complementary question.
astovergrep. Anastdetector is syntactically precise — it never matches a comment or a string literal.grepis a fallback for genuinely lexical patterns only. Seeknowledge/detector-model.md.- A gap is not a verdict. A flagged gap is high-confidence "the standard is not applied here" — not "this is a bug." Some gaps are deliberate exceptions. Triage is a fix-time decision.
- A clean standard is a result. A standard with 0 gaps, 0 skipped files, and 0 unsupported matched files is a passing standard — it confirms the codebase upholds the rule, and the scan becomes a regression guard if re-run.
Scope
- Project root: the repository root.
- Python:
scan_coverage.py,project_state.py, andcensus.pyare stdlib-only; use the host.venv/bin/pythonwhen it exists, otherwisepython3. - TypeScript/TSX v1: Node plus a
typescriptpackage resolvable from the host project'spackage.json. The bundled Compiler API launcher usescreateSourceFile; it does not read a tsconfig, construct a Program, or infer framework behavior. - Go v1: Go 1.22+ on
PATH. The bundled standard-library parser supports one syntax-only contract:enclosed_by: "defer"for directly spelled calls. It resolves no imports, types, aliases, or receivers. Generated, vendored, test, andtestdatafiles are excluded; malformed or build-constrained matched files make the result partial rather than clean. - Java v1: JDK 17+ (
javaandjavac) onPATH. The bundled public JDK Compiler Tree API helper supportsenclosed_by: "try"for directly spelled method calls. It invokes neither Maven nor Gradle and resolves no classpath, imports, types, aliases, overloads, receivers, or framework behavior. Generated, vendored, test, build-output, and external-symlink paths are excluded; malformed/read-error matched files make the result partial. - Output:
reports/standard-gaps/scan-<TS>/only. Never edits code.
Installed command
Copy standards/standards.example.json to a host-owned standards.json,
adapt its paths and standards, then set STANDARDS=standards.json. Run the
following two blocks verbatim from the host root. They support both the stock
.agents projection and this source checkout.
SKILL_ROOT=""
for SKILL_CANDIDATE in \
".agents/skills/find-standard-gaps" \
".claude/skills/find-standard-gaps"
do
if [ -f "${SKILL_CANDIDATE}/SKILL.md" ]; then
SKILL_ROOT="$(cd "${SKILL_CANDIDATE}" && pwd)"
break
fi
done
if [ -z "${SKILL_ROOT}" ]; then
printf '%s\n' "find-standard-gaps is not installed in .agents/skills or .claude/skills" >&2
exit 2
fi
if [ -x ".venv/bin/python" ]; then
HOST_PYTHON="$(pwd)/.venv/bin/python"
else
HOST_PYTHON="python3"
fi
: "${STANDARDS:?Set STANDARDS to the host-owned standards JSON file}"
TS=$(date +%Y%m%d-%H%M%S)
REPORT_DIR="reports/standard-gaps/scan-${TS}"
mkdir -p "${REPORT_DIR}"
"${HOST_PYTHON}" "${SKILL_ROOT}/scripts/scan_coverage.py" \
--ideas "${STANDARDS}" \
--project-root "$(pwd)" \
--output-dir "${REPORT_DIR}"
Argument
The argument is an optional path to a host-owned standards file. In a source
checkout the historical default is
.claude/skills/find-standard-gaps/standards/standards.json; a stock install
must not write into .agents, so use standards.json at the host root (or
another explicit host path) through STANDARDS.
This skill ships standards/standards.example.json — a template
with two universal example standards. On first use, copy it to
standards.json and adapt: narrow each detector's paths to your
source root, and replace the examples with the baseline standards your
codebase should uphold. The file shape and the detector model are in
knowledge/detector-model.md.
Standards may include an activation block (ADR 0020 shape):
{"baseline": true} or {"rungs": [{"min_maturity": "...", "min_stakes": "..."}]}. scan_coverage.py gates each standard against
the project state before running the detector.
Project state is read from .engineering/project-state.json, falling
back to the legacy .project-state.json. If no state file exists, the
script assumes MAX (production / public-adversarial) and prints a
warning so no standard is silently skipped. To test a specific state
surface, pass --project-state <path> explicitly.
Pipeline
Stage 0 — Setup
STANDARDS="<host-owned standards file, commonly standards.json>"
Stage 1 — Scan
"${HOST_PYTHON}" "${SKILL_ROOT}/scripts/scan_coverage.py" \
--ideas "$STANDARDS" \
--project-root "$(pwd)" \
--output-dir "$REPORT_DIR"
scan_coverage.py runs each standard's detector against the tree and
writes coverage.md (human report) and coverage.json (machine
evidence). It recognises ast (enclosed_by / requires_kwarg) and
grep detectors; manual/skill standards are reported as skipped.
Paste the script's stdout/stderr into your closeout or report. The
summary line names the declared project state, scanned count, total gap
count, and non-passing status counts such as gated out,
language-unsupported, and no-files-matched.
Optional explicit-state form, for replaying a project-state fork:
"${HOST_PYTHON}" "${SKILL_ROOT}/scripts/scan_coverage.py" \
--ideas "$STANDARDS" \
--project-root "$(pwd)" \
--project-state ".engineering/project-state.json" \
--output-dir "$REPORT_DIR"
Stage 2 — Summarize
Read coverage.md and, when judging status bands, confirm the matching
record in coverage.json. Report to the user in ≤10 lines:
- per standard: situation-site count, gap count, coverage %;
- the highest-priority gaps (a security/resilience standard with gaps outranks a style one);
- standards that came back clean (
status: scanned, 0 gaps) — name them, that is a positive result; - standards that were
partial— name their skipped-file count and any unsupported-file count/extensions separately; their gaps are triage evidence, but the standard is not clean/compliant; - standards that were
gated_out,language_unsupported,no_files_matched,skipped, orerror— name them separately and do not count them as compliant; - path to
${REPORT_DIR}/coverage.md.
Stage 3 — Hand off
- Genuine gaps on a security/resilience standard →
/fix-workflowwith the gap list, or spin off a triage task. - A standard that is mostly-clean with a couple of gaps → fix inline.
- A standard you keep wanting → add it to the standards file so every future run checks it. The standards file is the durable artifact.
TypeScript/TSX support and limits
TypeScript v1 supports one intentionally narrow structural contract:
kind: "ast", enclosed_by: "try", and a call_matches regular expression
against a direct syntactic identifier/property chain. For example, this scans
both .ts and .tsx source without assuming React or another framework:
{
"kind": "ast",
"call_matches": "^JSON\\.parse$",
"enclosed_by": "try",
"paths": ["src/**/*.ts", "src/**/*.tsx"]
}
The bundled TypeScript Compiler API parser establishes syntax only. It does not resolve aliases, types, receivers, or frameworks beyond that direct spelling.
JSON.parse means that literal call spelling, not a proof that it is the global API. A nested
function/callback body resets try enclosure because the scanner does not
infer when that callback runs. It ignores .d.ts, generated/minified/bundle,
test/spec, fixture, build, dependency, report, and vendor paths even when the
detector directly names them; paths are project-root-relative, and symlinks
escaping the project root are excluded.
requires_kwarg and enclosed_by: "with" remain Python-only contracts.
TypeScript/TSX standards using either return language_unsupported; split a
mixed-language standard into language-specific entries rather than treating an
unsupported condition as a clean scan. Missing Node, missing host-local
typescript, or a TypeScript parser preflight failure also returns
language_unsupported, never 0 gaps. Mixed Python plus TypeScript/TSX paths
are scanned together only for the shared enclosed_by: "try" contract.
A per-file TS syntax/read failure produces status: partial with
skipped_files; that is never clean/compliant even when its gap count is 0.
When an ast path also matches an unsupported extension (for example .js),
the scanner still reports findings from supported .py/.ts/.tsx files but
returns partial with unsupported_files and unsupported_extensions.
If no supported files remain, it returns language_unsupported instead.
Go support and limits
Go v1 supports kind: "ast", a direct syntactic call_matches regular
expression, and enclosed_by: "defer". For example:
{
"kind": "ast",
"call_matches": "^cleanup$",
"enclosed_by": "defer",
"paths": ["**/*.go"]
}
The bundled go/parser/go/ast helper establishes only the direct call
spelling and whether it executes beneath a defer statement. It does not
prove symbol identity or function signatures. Other conditions return
language_unsupported; missing or Go <1.22 tooling does the same. A matched
syntax failure or build-constrained file makes the result partial, never
clean. Generated, vendored, test, testdata, dependency, report, and external
symlink paths are excluded by the fixed source policy.
Java support and limits
Java v1 supports kind: "ast", a direct syntactic call_matches regular
expression, and enclosed_by: "try". For example:
{
"kind": "ast",
"call_matches": "^Json\\.decode$",
"enclosed_by": "try",
"paths": ["src/main/java/**/*.java"]
}
The bundled JDK 17 Compiler Tree API helper validates syntax then records only
direct identifier/member-select invocation spelling and whether it executes in
a try resource/body. Catch/finally, lambda, local-class, and anonymous-class
bodies do not inherit that enclosure. It does not resolve aliases, types,
receivers, imports, or frameworks (or overloads); an unresolved reference is still
syntax evidence, not proof of symbol identity. requires_kwarg,
enclosed_by: "with", and enclosed_by: "defer" are unsupported for Java.
Missing/old JDK returns language_unsupported; a syntax/read failure is
partial, never clean. Kotlin/JVM source is not Java support and remains an
unsupported extension in a mixed target.
When the target language or condition isn't supported
When a standard's paths cannot be analyzed,
scan_coverage.py reports language_unsupported — not as "0 gaps".
Treat language_unsupported as "could not analyze", never as
"compliant". A silent "0 gaps" on an unanalyzable language is the one
genuinely dangerous failure mode of this skill.
When a standard comes back language_unsupported, apply this rule:
- Enumerate the situation sites cheaply. A plain
grepfor the call or pattern works in any language — it tells you how many places the standard could apply. - Branch on size:
- Small — a bounded surface (rule of thumb: ≤ ~20 situation sites, readable in one pass): read those files directly, judge the standard by hand, and report the gaps explicitly marked "manual review — not tool-verified".
- Large — more sites than that, or the situation can't be cheaply
grepped: stop. Do not hand-scan a large surface — it is
unreliable and burns context for a low-confidence result.
Recommend building the detector tooling first — a
tree-sitter-backed
scan_coverage, or a language-specific AST pass — then re-running. An honest "tooling needed" beats a half-finished manual sweep.
The principle: small → read directly; large → build the tooling
first. A grep detector still runs on any language, but it is
comment/string-blind — trust it for enumerating situations, not for
deciding satisfaction.
Non-goals
- Editing or fixing code — detection only.
- Scout-verifying gaps — the
astdetector is the precision; triage is downstream. - Re-deriving structural smells — that is the
find-*family. - Replacing real lints for a one-off rule — a standard pays off when it is reused; a single-use rule is just a lint.
When things go sideways
| Symptom | Action |
|---|---|
--ideas is missing, malformed JSON, or has no ideas array |
Stop and report the exact script error. Do not synthesize a standard list from prose |
Explicit --project-state <path> does not exist, or a present state file is malformed |
Stop and fix the state path/file. Do not fall back to assumed MAX for an explicit typo |
| No project state exists at the default location | Accept the script's assumed-MAX warning, paste it, and tell the user /orient can declare the real (maturity, stakes) |
A standard is gated_out |
Report it as out of scope for the declared project state. It was not scanned and is not a 0-gap pass |
| A standard reports a huge gap count | The detector is too broad, or the situation regex matches non-code — tighten call_matches, or switch a grep standard to ast |
A grep standard flags comments/strings |
Expected — grep is comment/string-blind. Convert it to an ast detector |
| 0 standards fully scanned | Check coverage.json: entries may be partial, gated_out, manual/skill, no_files_matched, or error. Report the actual statuses |
partial |
Report skipped-file count plus unsupported-file count/extensions when present. Do not call 0 gaps compliant; repair the source, narrow the paths, or extend the scanner and re-run |
no_files_matched |
Treat as a misconfigured glob or wrong project root, not as compliance |
language_unsupported |
Check the reported reason (unsupported language/condition, missing TS prerequisite, or missing/old JDK), then apply the unsupported branch above |
| A gap is a deliberate exception | Not a tool failure — note it at fix time; a future --allow list could record approved exceptions |
Replay case
For future repairs to this skill, replay a tiny standard against a
temporary project and paste the real stdout plus the first status row from
coverage.json. The expected shape is: absent project state prints the
assumed-MAX warning; a Python or supported TypeScript ast standard with one
unsatisfied call prints state production/public-adversarial: scanned 1/1 standard(s): 1 coverage gap(s); coverage.json records status: scanned.
Census mode — discover before you declare
scripts/census.py answers the upstream question: "for a given concern,
what variants exist across a surface, what is the majority, and who are the
stragglers?" Use it before declaring a standard when you do not yet know
which shape to canonicalize.
The workflow:
- Run census → see variant distribution.
- Pick the majority variant as the standard (or consciously choose a different one and note why in the ADR).
- Declare the standard in
standards.jsonand usescan_coverage.pyto enforce it going forward.
Census output feeds /decide — paste the variant table into the ADR context
to record the population state at decision time.
When to use census vs scan_coverage
| Question | Tool |
|---|---|
| "Is standard X applied everywhere?" | scan_coverage.py |
| "What variants exist for concern Y before I decide?" | census.py |
Pipeline
TS=$(date +%Y%m%d-%H%M%S)
REPORT_DIR="reports/standard-gaps/census-${TS}"
mkdir -p "$REPORT_DIR"
"${HOST_PYTHON}" "${SKILL_ROOT}/scripts/census.py" \
--concern json_response_envelope \
--project-root "$(pwd)" \
app/api \
--json "${REPORT_DIR}/findings.json"
Output: per-variant counts sorted desc, majority variant + share %, straggler
file:line list for minority variants, opaque (non-literal payload) count.
The --json artifact carries the full structured data for downstream tooling.
Registered concerns
| Concern ID | What it detects |
|---|---|
json_response_envelope |
Django JsonResponse({...}) dict-literal shapes: sorted top-level keys, status kwarg presence, literal status value. Opaque = variable/non-literal payload. |
To add a concern: register a new Concern(...) entry in CONCERN_REGISTRY
in census.py (~30 lines). A concern provides a site_finder(path, root) → list[Site] that returns one Site per occurrence with a normalised
variant key or "opaque".
Repository layout
.claude/skills/find-standard-gaps/
├── SKILL.md # this file — orchestrator
├── scripts/
│ ├── scan_coverage.py # gap scan — deterministic, stdlib-only
│ ├── project_state.py # ADR-0020 activation gate helper
│ ├── engineering_home.py # bundled state-home resolver
│ ├── detect_typescript_calls.mjs # TS/TSX syntax facts
│ ├── detect_go_calls.go # Go direct-call/defer syntax facts
│ ├── detect_java_calls.java # Java direct-call/try facts (JDK 17+)
│ └── census.py # census mode — discover before you declare
├── knowledge/
│ └── detector-model.md # the detector model + how to add a standard
└── standards/
└── standards.example.json # template — copy to standards.json and adapt