# Dsl

> Build and maintain a Domain-Shared Lexicon, a glossary of domain terms that you and the AI agree on. Use when the user says "build a lexicon", "we keep using term X differently", "set up a glossary", "what does Y mean in this project", or whenever you notice the same word being used for two different concepts in conversation. Also activates at session start when a lexicon file is present.

- Skill: `p-vbordei/dsl` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add p-vbordei/dsl`
- Raw SKILL.md: https://api.skillmd.com/api/skills/p-vbordei/dsl/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Docs & Writing
- Author: p-vbordei (https://skillmd.com/u/p-vbordei)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/p-vbordei/dsl

---


# 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:

1. **Don't derail.** Finish the user's current ask first if it's mid-flight.
2. 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.md` to keep us aligned? (or `/dsl-update` later)"
3. 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:

1. **If `./LEXICON.md` exists**, treat it as in scope unconditionally. It is loaded via the project `CLAUDE.md` `@import`.
2. **If `~/.claude/LEXICON.md` exists**, use the `Read` tool to load it (do not rely on `@import`, see "Why no global `@import`" below). Parse each `## Domain:` section's `Applies 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.
3. **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.`
4. **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.**
5. **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.
6. **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:

1. **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.
2. **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.
3. **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 questions
- `ingest`: you point me at a file, URL, or paste (docs, transcripts, existing glossaries, code)
- `scan`: I scan this repo's `README.md`, any `GLOSSARY.md`/`TERMS.md`-style files, and directory names
- `hybrid`: 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.md` already references `./LEXICON.md` via `@./LEXICON.md`. If not, propose appending the snippet from `templates/claude-md-snippet-project.md`. If `./CLAUDE.md` doesn't exist, propose creating one containing only that snippet.
- Global: check if `~/.claude/CLAUDE.md` already contains the global lexicon block. If not, propose appending the snippet from `templates/claude-md-snippet-global.md`. If `~/.claude/CLAUDE.md` doesn'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:

```bash
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`)

1. Read the existing file
2. Ask the user what to add / fix / disambiguate
3. Run a tight interview only on those terms
4. Rewrite the file in full (no diff markers in the file itself, git handles history)
5. Run the audit (Step 5 above) and fix any errors
6. 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.

```bash
# 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`)

1. Accept: file path, URL, or pasted text
2. Read it
3. Extract domain-relevant noun phrases (`X is a ...`, `we call this a ...`, repeated capitalized terms, acronyms with expansions)
4. Skip generic programming concepts (`array`, `function`, `endpoint`) unless they have domain-specific meaning here
5. Present candidates in a short list with proposed definitions drawn from the source
6. Let the user accept / edit / reject each
7. 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 --audit` enforces 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 the `Flagged ambiguities` section 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 ambiguities` with 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.

