DSL: Domain-Shared Lexicon
A focused skill with one job: align the vocabulary between you (the AI) and the user for the domain they work in. Nothing else. Not plan reviews, not ADRs, not design decisions. Only the words and what they mean.
Why this exists
When the user says "the order is rejected" they may mean a specific lifecycle state with downstream effects. When you (the AI) hear "order" you may default to a generic e-commerce meaning. Drift compounds: code uses one name, comments another, the user a third, and you a fourth. The lexicon collapses all of that into a single source of truth that everyone references, including future-you in a fresh session.
The governing principle is borrowed from ASD-STE100, the 1986 controlled-English standard written so a non-native aircraft mechanic could not misread a repair manual: one thing, one name; one word, one meaning. The standard's power comes from being machine-checkable, not from being agreed to. That is why this skill ships lexicon-lint.py. A lexicon nobody can check is a wish list.
Scope (read this twice)
The lexicon stores domain terms and what they mean. That is the whole scope.
- ✅ Term → definition → aliases to avoid
- ✅ Relationships between terms ("an Invoice belongs to one Customer")
- ✅ Flagged ambiguities (same word used for two things)
- ❌ Not architecture decisions (those go in ADRs)
- ❌ Not design plans, PRDs, or feature specs
- ❌ Not coding conventions or style rules
- ❌ Not "how to use this codebase"
If you find yourself writing how a feature works, you've drifted. Stop and put it elsewhere.
Where lexicons live
| Scope | Path | Applies to |
|---|---|---|
| Project | ./LEXICON.md |
This project only |
| Global | ~/.claude/LEXICON.md |
The user's recurring domains across all their projects |
The global file is not one big bag. It is a collection of per-domain sections, each with explicit Applies when: signals. At session start, you match the current project against those signals and load only matching domains. A user whose primary work is metal smelting should not get smelting vocabulary auto-loaded into a recipe app side project.
See templates/lexicon-template.md for the canonical structure.
Three ways this skill runs
1. On demand (the /dsl-init, /dsl-update, /dsl-ingest commands)
The user explicitly asks. Run the matching command's flow.
2. Proactive mid-conversation
You're working on something else and notice a term being used in two senses in the same conversation, or a domain term the user uses confidently that you cannot define. When this happens:
- Don't derail. Finish the user's current ask first if it's mid-flight.
- At a natural pause, raise the ambiguity in one short sentence: "Quick check: 'account' has come up meaning both the paying customer and a login identity. Worth adding to
LEXICON.mdto keep us aligned? (or/dsl-updatelater)" - If the user says yes, run the update flow on just that term. If no or silence, drop it. Do not nag.
Threshold for raising: real ambiguity or genuine unknown, not stylistic preference. Do not interrupt for terms that have one clear meaning in context.
3. At session start (auto-detection)
When loaded via CLAUDE.md at the start of a session:
- If
./LEXICON.mdexists, treat it as in scope unconditionally. It is loaded via the projectCLAUDE.md@import. - If
~/.claude/LEXICON.mdexists, use theReadtool to load it (do not rely on@import, see "Why no global@import" below). Parse each## Domain:section'sApplies when:block and evaluate against the current project (see "Matching the project to a global domain"). Mentally apply only matching sections. Ignore the rest as if they weren't there. - When precedence collides (a term defined in both project and a matching global domain), the project definition wins. Note the divergence to the user once:
'Heat' has both global and project definitions, using the project one. - Acknowledgement: in your first substantive response, emit exactly one line of the form
Lexicon loaded: <N> terms (project: ./LEXICON.md, global: <matched-domain>).Omit any source that contributed zero. Skip the line entirely if nothing loaded. - If neither matches and the project shows clear domain-specific signals (specialized jargon in README, narrow industry deps), make a one-time, one-line offer in your first substantive response:
This project has domain-specific terminology. Want to build a lexicon? Run /dsl-init.Do not offer again in the same session if declined. - If the project is generic / off-domain (todo apps, recipe apps, generic CRUD with no specialized vocabulary, AND no global domain matches), say nothing. Silence is the right behaviour here.
Why no global @import
@~/.claude/LEXICON.md in ~/.claude/CLAUDE.md would load the entire global lexicon into context every session, including off-domain vocabulary. That defeats the matching guarantee. A metallurgist's smelting terms would still enter context when they open a recipe app. Instead, the global CLAUDE.md snippet asks you to Read the lexicon at session start and filter by Applies when: yourself. Off-domain terms never enter context.
Matching the project to a global domain
Each ## Domain: section in ~/.claude/LEXICON.md declares Applies when: signals. Evaluate them with this priority:
- Explicit override: if a line matching the regex
^\s*domain:\s*<name>\s*$appears in the project's./CLAUDE.md, or as the first non-blank, non-heading line of./LEXICON.md, use that. Skip detection. - Strong signal: README mentions 2+ named keywords from the domain's
Applies when:list, OR project deps (package.json,pyproject.toml,Cargo.toml,go.mod, etc.) include declared domain libraries. - Weak signal: file/dir names match declared patterns, OR domain-specific file extensions are present.
Load a global domain only when strong signal OR explicit override matches. Weak signals alone are not enough. They cause false positives across unrelated projects.
When in doubt, prefer not loading over polluting context with off-domain vocabulary. The user can always invoke /dsl-init to force-attach a global domain to the current project.
Building the lexicon (the interview flow)
When invoked via /dsl-init or when the user accepts the proactive offer:
Step 0. Pick the scope
Ask once: Is this lexicon for this project only, or part of your recurring domain across projects? (Options: project / global-new-domain / global-add-to-existing.) Use AskUserQuestion if available.
Step 1. Gather raw material
Ask once: How should we extract terms? Options:
interview: I ask you questionsingest: you point me at a file, URL, or paste (docs, transcripts, existing glossaries, code)scan: I scan this repo'sREADME.md, anyGLOSSARY.md/TERMS.md-style files, and directory nameshybrid: combine the above
For ingest: ask for the source. Read it. Extract candidate terms.
For scan: read the project's README.md and any file with a glossary-shaped name at the project root or one level deep (GLOSSARY.md, TERMS.md, LEXICON.md, DICTIONARY.md). Also use the directory tree (names only, not file contents) as a hint for domain. Extract candidates. Do not read docs/, CONTRIBUTING.md, ADRs, design docs, or source code. Those describe how the system works, which is out of scope. If the user wants terms extracted from a design doc, they should ingest it explicitly so they're aware that's a deliberate widening of scope.
Step 2. Interview the user
Ask one question at a time. Prefer AskUserQuestion with 2-4 concrete options + Other.
Cover only:
- For each candidate term:
What does <Term> mean here, in one sentence? - For each apparent synonym pair:
Are <A> and <B> the same thing, or different? - For each ambiguous term:
When you say <X>, do you mean <interpretation A> or <interpretation B>? - For each known industry term the user uses confidently but you don't know:
Quick: what is a <Term> in your domain?
You are an interviewer, not an executor. Do not start coding, refactoring, or restructuring anything as a side effect. The deliverable is the lexicon, full stop.
Stop when:
- All ambiguities are resolved
- All candidate terms have one-sentence definitions
- The user says "enough" / "wrap up"
10 to 15 terms is a normal first pass. Don't pad.
Step 3. Write the file
Before writing, always show the proposed lexicon contents to the user and confirm. Never write a LEXICON.md silently.
Write ./LEXICON.md (or update ~/.claude/LEXICON.md for global). Follow templates/lexicon-template.md exactly. Group terms into tables by natural cluster (lifecycle / actors / artifacts). Include the Flagged ambiguities and Example dialogue sections.
For a new global domain, populate the Applies when: block carefully. These signals decide future auto-loading. Be specific: deps, README keywords, file patterns. Vague signals cause false positives.
Empty-lexicon guard: if the interview / ingest / scan yielded fewer than 2 terms (or no real ambiguities), do not write a file. Tell the user: Not enough domain-specific terms for a lexicon, skipping. Run /dsl-init again when you have more material.
Step 4. Wire it up
Check the relevant CLAUDE.md. Always confirm with the user before writing or modifying their CLAUDE.md. Never write a CLAUDE.md file without first showing the user the snippet you intend to append.
- Project: check if
./CLAUDE.mdalready references./LEXICON.mdvia@./LEXICON.md. If not, propose appending the snippet fromtemplates/claude-md-snippet-project.md. If./CLAUDE.mddoesn't exist, propose creating one containing only that snippet. - Global: check if
~/.claude/CLAUDE.mdalready contains the global lexicon block. If not, propose appending the snippet fromtemplates/claude-md-snippet-global.md. If~/.claude/CLAUDE.mddoesn't exist, propose creating one containing only that snippet.
Project lexicon loading: the project snippet uses @./LEXICON.md, so the whole file enters context every session, which is the intended behaviour (project lexicons always apply). If the project lexicon is large enough to be a context concern (say, over 500 lines), tell the user; consider splitting by sub-domain into separate files referenced from ./LEXICON.md.
Global lexicon loading: the global snippet does not use @import. It instructs you to Read and filter at session start. This keeps off-domain vocabulary out of context.
Step 5. Verify (never skip)
Writing the file is not the end of the flow. Run the audit against what you just wrote:
python3 ~/.claude/skills/dsl/lexicon-lint.py --lexicon ./LEXICON.md --audit
It reports two classes:
- Errors: structural contradictions: a term defined twice, an alias that is itself a defined term, one alias claimed by two terms, a missing definition. Fix these and rewrite the file. They are bugs in the lexicon, not style opinions.
- Warnings: definition style (over 25 words, semicolon, multiple sentences, circular). Fix them unless the user overrules; a definition that fails these is a definition that will be read two ways.
Report the audit line verbatim to the user. Do not claim the lexicon is written until the audit has actually run. An unaudited lexicon is exactly the banned-word list this skill exists to replace.
Step 6. Confirm
State, in one line: Wrote <path>: <N> terms in <M> domains, audit clean. CLAUDE.md updated. Do not summarize the contents. The file is the artifact.
Updating an existing lexicon (/dsl-update)
- Read the existing file
- Ask the user what to add / fix / disambiguate
- Run a tight interview only on those terms
- Rewrite the file in full (no diff markers in the file itself, git handles history)
- Run the audit (Step 5 above) and fix any errors
- Confirm
When invoked because of a proactive ambiguity catch (mode 2 above), skip Step 2 and go straight to the specific term.
Checking for drift (lexicon-lint.py)
The audit checks the lexicon. The lint checks everything else: whether the repo actually uses the canonical terms.
# where has the codebase used a banned alias?
python3 ~/.claude/skills/dsl/lexicon-lint.py docs/*.md src/**/*.py
# a global lexicon, scoped to one domain
python3 ~/.claude/skills/dsl/lexicon-lint.py --lexicon ~/.claude/LEXICON.md \
--domain smelting docs/*.md
Output is path:line:col: 'batch' -> use "Heat", and the exit code is 1 when anything is found. Code fences and inline code are skipped by default (--code includes them). A line carrying the marker lexicon-ok is skipped. That is the escape hatch for a deliberate use.
Use it when:
- The user asks whether the code and the lexicon still agree.
- You have just written a batch of docs or comments in a repo that has a lexicon. Lint your own output before presenting it.
- A term was renamed in the lexicon and you need every stale occurrence.
Report hits, do not auto-fix them. A hit is a question ("is this the domain term or ordinary English?"), not a defect. Renaming identifiers as a side effect is explicitly out of scope. Surface the list and let the user decide.
Ingesting external material (/dsl-ingest)
- Accept: file path, URL, or pasted text
- Read it
- Extract domain-relevant noun phrases (
X is a ...,we call this a ..., repeated capitalized terms, acronyms with expansions) - Skip generic programming concepts (
array,function,endpoint) unless they have domain-specific meaning here - Present candidates in a short list with proposed definitions drawn from the source
- Let the user accept / edit / reject each
- Continue to Step 3 of the interview flow (write the file)
For URLs, prefer WebFetch. For PDFs and Office files, ask the user to convert or paste the relevant section. Do not assume a converter is installed.
Rules of the format
These rules apply when you write any lexicon file:
- Be opinionated. When multiple words exist for the same concept, pick one and list the others under
Aliases to avoid. - Tight definitions. These are the checkable ones.
lexicon-lint.py --auditenforces them:- No em dashes or en dashes, ever. Use a period, a comma, a colon, or parentheses. This is an audit error, not a style warning. A dash is where two thoughts get welded together instead of one being chosen, which is the opposite of what a definition is for.
- One sentence, 25 words or fewer.
- No semicolons. A semicolon means you are defining two things. Split them.
- Never use the term inside its own definition.
- Define what something is, not what it does.
- Aliases must be plausible substitutes, not just any synonym. The alias column is a linted banned-word list. If you list a common English word (
run,load,state) it will fire on every unrelated use in the codebase. List it only if someone would genuinely reach for it to mean this term. When it's a common word that still matters, say so in theFlagged ambiguitiessection instead. - Mark part of speech when a term is both.
**Tap** *(v.)*and**Tap** *(n.)*are two entries, not one. This is the single most common source of quiet drift. STE's dictionary carries part of speech for exactly this reason. - Bold term references. When a definition mentions another term in the lexicon, bold it:
**Customer**. This makes relationships obvious. - Group when natural, single table when not. Don't force subgroups onto a 5-term lexicon.
- Only domain terms. Skip framework/language jargon unless it has domain-specific meaning here.
- Flag ambiguities explicitly. If the source material used a term in two senses, surface it in
## Flagged ambiguitieswith a clear recommendation. - One example dialogue. A 3 to 5 line exchange that shows the terms used precisely in context.
What you must not do
- Do not generate ADRs, design docs, PRDs, or feature specs as part of this skill.
- Do not refactor code or rename identifiers as a side effect of building a lexicon. Note the divergence, surface it to the user, and stop.
- Do not auto-load a global domain whose
Applies when:signals don't match. - Do not pad the lexicon with terms just to look thorough. If a term has no real ambiguity and no special domain meaning, leave it out.
- Do not interrupt the user's current task to volunteer a lexicon update. Wait for a natural pause.