corpus-grounding
The reusable grounding layer from ADR-061: one pure-stdlib engine
(BM25 retrieval + structured pre-filters + decision-rule evaluation)
that any domain plugs into via a manifest — frontend design is its
first consumer (design-intelligence).
A corpus grounds (pre-action, constrains the option space); it never
replaces mid-action reference (RAG), post-action validation (rules), or
the method itself (a framework skill).
Engine provenance: ported from nextlevelbuilder/ui-ux-pro-max-skill
@ b7e3af80f6e331f6fb456667b82b12cade7c9d35 (MIT, last checked
2026-06-07) — BM25 de-duplicated, slide-only paths stripped, every
frontend-hardcoded axis moved into the manifest. Full license obligations:
design-intelligence/ATTRIBUTION.md.
When to use
- A skill needs pre-action selection grounded in curated knowledge
("which layout pattern / threat class / index strategy applies here").
- You are authoring a new domain corpus — write a manifest + CSVs,
validate with the grounding CLI (
ground validate via ./scripts-run),
never fork the engine.
- You need stack-scoped Do/Don't guidance (
--stack react …).
Do NOT
- Do NOT use the corpus for mid-task fact lookup — that is reference
(
references/ docs / RAG).
- Do NOT use it for output validation — write a rule/linter.
- Do NOT build a corpus that fits in 5 lines of an always-on rule.
- Do NOT fork the engine per domain — plug in via a manifest.
- Do NOT merge a manifest without owner + refresh cadence (validator
refuses it anyway).
Procedure: Consult a corpus
- Locate the domain manifest (
<domain-skill>/data/manifest.json).
- Run
search (one domain / stack axis) or ground (reasoning plan).
- Read
confidence + evidence_gap before trusting any row.
- Propose grounded options; the human confirms (Tier-1 default).
Invocation (consumer runtime)
Scripts resolve all paths skill-relative (per
docs/contracts/skill-bundled-assets.md)
— they work from any cwd:
./scripts-run <skills-root>/corpus-grounding/scripts/ground search \
--manifest <skills-root>/<domain-skill>/data/manifest.json \
"fintech dashboard" [--domain style] [--stack react] \
[--filter "Severity=HIGH"] [--max-results 3] [--json]
./scripts-run <skills-root>/corpus-grounding/scripts/ground ground \
--manifest <skills-root>/<domain-skill>/data/manifest.json \
"luxury e-commerce" [--context '{"data_heavy": true}'] [--persist DIR] \
[--variance 1-10] [--motion 1-10] [--density 1-10]
./scripts-run <skills-root>/corpus-grounding/scripts/ground validate --manifest …
<skills-root> is wherever skills are deployed (~/.claude/skills/ for
Claude Code installs; src/skills/ inside this repo).
The three ground dials are optional 1-10 preferences, and unset means
unset — a dial you do not pass changes nothing, so the no-flag result is
byte-identical to what it was before they existed. --variance biases which
style is selected from the rows the corpus returned (never which rows come
back), and when it changes the pick away from the grounding rule's own choice
the run says so in its evidence gaps. --motion needs a gsap domain in the
manifest and reports when a tier matches nothing. --density overrides the
spacing scale. All three render in the text output and in a persisted
MASTER.md, not only under --json.
Output format
Interface contract v1 — stability promise. Per ADR-061 §2 ("Opus
condition"), this interface is versioned; domains may depend on it. Breaking any item below requires a major bump
(manifest_version: 2), a migration note here, and updates to every
consuming skill in the same PR. Additive fields are allowed anytime.
Stable v1 surface:
- Modules + public names —
bm25_search.{BM25, load_csv, apply_filters, search_rows, RETRIEVERS}, decision_engine.{detect_domain, search_domain, search_stack, evaluate_rules, ground, persist_grounding},
schema_validator.{load_manifest, validate_manifest, resolve_data_path, ManifestError, MANIFEST_VERSION, TIERS}.
- CLI — the grounding CLI (
ground {search|ground|validate} via
./scripts-run) with the flags shown above.
- Result shape — search:
{domain|stack, query, file, count, results, scores, filtered_from, confidence{label,score}, evidence_gap[]};
ground: {domain, query, category, rule, rules_evaluation{matched, unmatched}, selections{<domain>: {best, alternatives, confidence}}, confidence{label,score}, evidence_gap[]}.
- Contract invariants — every output carries
confidence and
evidence_gap; retrievers are name-selected (bm25 default,
structured, hybrid); no network, no subprocess, read-only except
the opt-in --persist DIR.
Manifest contract (schema-agnostic plug-in)
Each domain ships data/manifest.json beside its CSVs — declaring its
own axes; the engine never assumes the frontend schema:
{
"manifest_version": 1,
"domain": "frontend-design",
"tier": "conditional-grounding",
"data_dir": ".",
"retriever": "bm25",
"default_domain": "style",
"domains": {
"style": {
"file": "styles.csv",
"search_cols": ["Style Category", "Keywords"],
"output_cols": ["Style Category", "Best For"],
"max_results": 3,
"filters": {}
}
},
"detect": { "style": ["minimalism", "glassmorphism"] },
"stacks": { "react": "stacks/react.csv" },
"stack_cols": { "search_cols": ["…"], "output_cols": ["…"] },
"reasoning": {
"file": "ui-reasoning.csv",
"category_domain": "product",
"category_column": "Product Type",
"match_column": "UI_Category",
"rules_column": "Decision_Rules",
"priority_column": "Style_Priority",
"priority_domain": "style",
"name_columns": { "style": "Style Category" },
"plan": { "style": 3, "color": 2 },
"rules_module": "rules.ts"
},
"owner": "package-maintainer",
"refresh_cadence": "quarterly",
"upstream": { "repo": "…", "sha": "…", "last_checked": "YYYY-MM-DD" }
}
tier ∈ lookup-only | conditional-grounding | constraint-emission;
reasoning is only legal above lookup-only.
owner, refresh_cadence, upstream{repo,sha,last_checked} are
required — provenance discipline per ADR-061 §6; an unowned corpus
is not merged.
rules_module (optional TypeScript escape hatch where JSON rules cap
out — a module exposing evaluate(rules, query, context); a legacy
.py name in a manifest is remapped to its .ts/.js twin) must live
beside the manifest — absolute paths and .. are refused.
- Decision rules are surfaced as
matched and unmatched — the full
rule space stays auditable, never a hidden gate.
Procedure: Author a new domain corpus
- Pass the qualification rubric (ADR-061 §5): decision-rule utility beats
row count; grounding must happen before action; "fits in 5 lines →
it's a rule, not a corpus".
- Write
data/manifest.json + CSVs in your domain skill; run
the grounding CLI (ground validate via ./scripts-run).
- Name an owner + refresh cadence in the manifest, pin the upstream SHA.
- Cite this skill from your domain skill; consult via the CLI (Tier-1
consultation default — propose grounded options, human confirms).
Gotchas
- An empty result is a legitimate outcome — surface the evidence gap and
proceed on priors; never silently widen filters to force a hit.
- The structured
filters pre-filter matches case-insensitive
substrings; an over-specific value silently filters everything out —
check filtered_from vs count when results look thin.
detect keyword routing falls back to default_domain; product-shaped
queries route best ("fintech dashboard"), generic words land on the
default.
- BM25 tokenizer drops tokens ≤2 chars — "UI", "a11y" style queries need
longer companions.
Security constraints (Step 1.6 runtime-safety record)
Four scripts ship under scripts/: ground.ts (CLI), bm25_search.ts,
decision_engine.ts, schema_validator.ts.
- What they may touch — corpus CSVs and the manifest under the
manifest's own directory.
resolve_data_path refuses absolute paths and
.. escapes, so the manifest directory is the read boundary.
- What they must never do — read outside that directory, reach the
network, or spawn a subprocess. Embeddings and remote retrievers are
intentionally not implemented (ADR-061 §2), and that is a containment
decision, not a missing feature.
- Default invocation — read-only. The single write surface is opt-in
--persist DIR, which writes MASTER.md (and pages/<page>.md when
--page is given) under DIR/design-system/<project-slug>/. Nothing
else writes.
- Outbound — nothing. No network access at all.
One qualified exception: rules_module executes a manifest-adjacent
TypeScript module via dynamic import(). That is the same trust domain as
the skill shipping the manifest, and its containment is resolve_data_path.
See also
1---2name: corpus-grounding3description: Shared corpus-grounding engine — BM25 + structured filters + decision rules over CSV corpora via a domain manifest. Use when a skill needs grounded pre-action option-space constraints.4---56# corpus-grounding78> The reusable grounding layer from ADR-061: one pure-stdlib engine9> (BM25 retrieval + structured pre-filters + decision-rule evaluation)10> that any domain plugs into via a **manifest** — frontend design is its11> first consumer ([`design-intelligence`](../design-intelligence/SKILL.md)).12> A corpus **grounds** (pre-action, constrains the option space); it never13> replaces mid-action reference (RAG), post-action validation (rules), or14> the method itself (a framework skill).1516Engine provenance: ported from `nextlevelbuilder/ui-ux-pro-max-skill`17@ `b7e3af80f6e331f6fb456667b82b12cade7c9d35` (MIT, last checked182026-06-07) — BM25 de-duplicated, slide-only paths stripped, every19frontend-hardcoded axis moved into the manifest. Full license obligations:20[`design-intelligence/ATTRIBUTION.md`](../design-intelligence/ATTRIBUTION.md).2122## When to use2324- A skill needs **pre-action selection** grounded in curated knowledge25 ("which layout pattern / threat class / index strategy applies here").26- You are authoring a **new domain corpus** — write a manifest + CSVs,27 validate with the grounding CLI (`ground validate` via ./scripts-run),28 never fork the engine.29- You need stack-scoped Do/Don't guidance (`--stack react …`).3031## Do NOT3233- Do NOT use the corpus for mid-task fact lookup — that is reference34 (`references/` docs / RAG).35- Do NOT use it for output validation — write a rule/linter.36- Do NOT build a corpus that fits in 5 lines of an always-on rule.37- Do NOT fork the engine per domain — plug in via a manifest.38- Do NOT merge a manifest without owner + refresh cadence (validator39 refuses it anyway).4041## Procedure: Consult a corpus42431. Locate the domain manifest (`<domain-skill>/data/manifest.json`).442. Run `search` (one domain / stack axis) or `ground` (reasoning plan).453. Read `confidence` + `evidence_gap` before trusting any row.464. Propose grounded options; the human confirms (Tier-1 default).4748### Invocation (consumer runtime)4950Scripts resolve all paths **skill-relative** (per51[`docs/contracts/skill-bundled-assets.md`](../../../docs/contracts/skill-bundled-assets.md))52— they work from any cwd:5354```bash55./scripts-run <skills-root>/corpus-grounding/scripts/ground search \56 --manifest <skills-root>/<domain-skill>/data/manifest.json \57 "fintech dashboard" [--domain style] [--stack react] \58 [--filter "Severity=HIGH"] [--max-results 3] [--json]5960./scripts-run <skills-root>/corpus-grounding/scripts/ground ground \61 --manifest <skills-root>/<domain-skill>/data/manifest.json \62 "luxury e-commerce" [--context '{"data_heavy": true}'] [--persist DIR] \63 [--variance 1-10] [--motion 1-10] [--density 1-10]6465./scripts-run <skills-root>/corpus-grounding/scripts/ground validate --manifest …66```6768`<skills-root>` is wherever skills are deployed (`~/.claude/skills/` for69Claude Code installs; `src/skills/` inside this repo).7071**The three `ground` dials are optional 1-10 preferences, and unset means72unset** — a dial you do not pass changes nothing, so the no-flag result is73byte-identical to what it was before they existed. `--variance` biases which74style is **selected** from the rows the corpus returned (never which rows come75back), and when it changes the pick away from the grounding rule's own choice76the run says so in its evidence gaps. `--motion` needs a `gsap` domain in the77manifest and reports when a tier matches nothing. `--density` overrides the78spacing scale. All three render in the text output and in a persisted79`MASTER.md`, not only under `--json`.8081## Output format8283**Interface contract v1 — stability promise.** Per ADR-061 §2 ("Opus84condition"), this interface is **versioned**; domains may depend on it. Breaking any item below requires a major bump85(`manifest_version: 2`), a migration note here, and updates to every86consuming skill in the same PR. Additive fields are allowed anytime.8788Stable v1 surface:89901. **Modules + public names** — `bm25_search.{BM25, load_csv, apply_filters,91 search_rows, RETRIEVERS}`, `decision_engine.{detect_domain, search_domain,92 search_stack, evaluate_rules, ground, persist_grounding}`,93 `schema_validator.{load_manifest, validate_manifest, resolve_data_path,94 ManifestError, MANIFEST_VERSION, TIERS}`.952. **CLI** — the grounding CLI (`ground {search|ground|validate}` via96 ./scripts-run) with the flags shown above.973. **Result shape** — search: `{domain|stack, query, file, count, results,98 scores, filtered_from, confidence{label,score}, evidence_gap[]}`;99 ground: `{domain, query, category, rule, rules_evaluation{matched,100 unmatched}, selections{<domain>: {best, alternatives, confidence}},101 confidence{label,score}, evidence_gap[]}`.1024. **Contract invariants** — every output carries `confidence` **and**103 `evidence_gap`; retrievers are name-selected (`bm25` default,104 `structured`, `hybrid`); no network, no subprocess, read-only except105 the opt-in `--persist DIR`.106107## Manifest contract (schema-agnostic plug-in)108109Each domain ships `data/manifest.json` beside its CSVs — declaring its110**own** axes; the engine never assumes the frontend schema:111112```json113{114 "manifest_version": 1,115 "domain": "frontend-design",116 "tier": "conditional-grounding",117 "data_dir": ".",118 "retriever": "bm25",119 "default_domain": "style",120 "domains": {121 "style": {122 "file": "styles.csv",123 "search_cols": ["Style Category", "Keywords"],124 "output_cols": ["Style Category", "Best For"],125 "max_results": 3,126 "filters": {}127 }128 },129 "detect": { "style": ["minimalism", "glassmorphism"] },130 "stacks": { "react": "stacks/react.csv" },131 "stack_cols": { "search_cols": ["…"], "output_cols": ["…"] },132 "reasoning": {133 "file": "ui-reasoning.csv",134 "category_domain": "product",135 "category_column": "Product Type",136 "match_column": "UI_Category",137 "rules_column": "Decision_Rules",138 "priority_column": "Style_Priority",139 "priority_domain": "style",140 "name_columns": { "style": "Style Category" },141 "plan": { "style": 3, "color": 2 },142 "rules_module": "rules.ts"143 },144 "owner": "package-maintainer",145 "refresh_cadence": "quarterly",146 "upstream": { "repo": "…", "sha": "…", "last_checked": "YYYY-MM-DD" }147}148```149150- `tier` ∈ `lookup-only | conditional-grounding | constraint-emission`;151 `reasoning` is only legal above lookup-only.152- `owner`, `refresh_cadence`, `upstream{repo,sha,last_checked}` are153 **required** — provenance discipline per ADR-061 §6; an unowned corpus154 is not merged.155- `rules_module` (optional TypeScript escape hatch where JSON rules cap156 out — a module exposing `evaluate(rules, query, context)`; a legacy157 `.py` name in a manifest is remapped to its `.ts`/`.js` twin) must live158 beside the manifest — absolute paths and `..` are refused.159- Decision rules are surfaced as `matched` **and** `unmatched` — the full160 rule space stays auditable, never a hidden gate.161162## Procedure: Author a new domain corpus1631641. Pass the qualification rubric (ADR-061 §5): decision-rule utility beats165 row count; grounding must happen **before** action; "fits in 5 lines →166 it's a rule, not a corpus".1672. Write `data/manifest.json` + CSVs in your domain skill; run168 the grounding CLI (`ground validate` via ./scripts-run).1693. Name an owner + refresh cadence in the manifest, pin the upstream SHA.1704. Cite this skill from your domain skill; consult via the CLI (Tier-1171 consultation default — propose grounded options, human confirms).172173## Gotchas174175- An empty result is a legitimate outcome — surface the evidence gap and176 proceed on priors; never silently widen filters to force a hit.177- The structured `filters` pre-filter matches case-insensitive178 substrings; an over-specific value silently filters everything out —179 check `filtered_from` vs `count` when results look thin.180- `detect` keyword routing falls back to `default_domain`; product-shaped181 queries route best ("fintech dashboard"), generic words land on the182 default.183- BM25 tokenizer drops tokens ≤2 chars — "UI", "a11y" style queries need184 longer companions.185186## Security constraints (Step 1.6 runtime-safety record)187188Four scripts ship under `scripts/`: `ground.ts` (CLI), `bm25_search.ts`,189`decision_engine.ts`, `schema_validator.ts`.190191- **What they may touch** — corpus CSVs and the manifest under the192 manifest's own directory. `resolve_data_path` refuses absolute paths and193 `..` escapes, so the manifest directory is the read boundary.194- **What they must never do** — read outside that directory, reach the195 network, or spawn a subprocess. Embeddings and remote retrievers are196 intentionally not implemented (ADR-061 §2), and that is a containment197 decision, not a missing feature.198- **Default invocation** — read-only. The single write surface is opt-in199 `--persist DIR`, which writes `MASTER.md` (and `pages/<page>.md` when200 `--page` is given) under `DIR/design-system/<project-slug>/`. Nothing201 else writes.202- **Outbound** — nothing. No network access at all.203204One qualified exception: `rules_module` executes a manifest-adjacent205TypeScript module via dynamic `import()`. That is the same trust domain as206the skill shipping the manifest, and its containment is `resolve_data_path`.207208## See also209210- [`design-intelligence`](../design-intelligence/SKILL.md) — first consumer211 (frontend corpus + manifest).212- [ADR-061](../../../docs/decisions/ADR-061-corpus-grounding-layer.md) —213 architecture, qualification rubric, fork resolutions.214- [`docs/contracts/skill-bundled-assets.md`](../../../docs/contracts/skill-bundled-assets.md)215 — how bundled `scripts/` + `data/` reach consumer runtime.