← Back to blog2026-07-23

Borrow the layout, not the vocabulary

A temporary table codename seeped into directories, components, tests, docs, machine data, and the company website. Cleaning it up was not a global replace; it was a brand-language migration with a target schema, an ordered cutover, semantics-preserving normalization, and a CI constraint.

The hidden payload in a borrowed layout

A layout can be a reference. Its vocabulary should not become yours. A visual reference can teach you where the pot, seats, and actions belong; its brand names, internal codenames, and information architecture do not come with the grid.

We learned that distinction after a temporary codename — borrowed from the very product whose layout we were studying — for a new poker table escaped its prototype. It reached component directories, class names, tests, screenshots, design notes, generated data, and eventually the company website. Nothing broke at runtime. That was exactly why it survived: the software could compile while the product quietly spoke in somebody else’s vocabulary.

The tempting fix is a repository-wide replace. That is not a migration plan. One spelling can play at least four different roles: a source identifier, customer-facing copy, a filename, or an accidental byte sequence inside compressed data. Replacing all four the same way either leaves gaps or corrupts files.

The reusable lesson is this: brand language is a schema. Give it canonical values, migrate every consumer by type, preserve behavior during the cutover, and install a constraint that rejects old values when they return.

Define the target schema before touching files

A good migration starts with the names you want, not the names you dislike. We wrote a small vocabulary contract and assigned each term an audience:

SurfaceCanonical nameReason
Customer copy“new table”Describes the experience, not the implementation.
Code, routes, and testsTable2 / table2Stable, owned, and easy to grep.
Article collection“Blog”Names what exists today: a list of articles.
Future editorial product“Insights”Reserved until it has a distinct promise and structure.

Table2 is intentionally boring. It is an implementation locator, not a product slogan. Customers should not have to understand our component genealogy to understand the table. The public phrase can change with the product; the internal locator only needs to be unambiguous and ours.

The Blog/Insights distinction follows the same rule. Navigation is a label on the object a visitor will open, not a pitch for what the object might become. Calling an ordinary article list “Insights” does not make it more valuable; it only makes the information architecture less truthful. A real Insights product can arrive later with its own format, editorial promise, and route.

Migrate a graph, not a string

codename directory component (import) route e2e test screenshot / evidence generated data (base64 / integrity) public changelog / website pre-push + CI guard 1 structure 2 prose by audience 3 machine data

One string, many consumers: the migration cuts the dependency graph in order, then a guard holds the line.

A codename spreads through relationships: a directory is imported by a component, that component is mounted by a route, the route is opened by an end-to-end test, and the test deposits a screenshot named after the route. The unit of work is that graph, not the original string.

Our migration order was:

  1. Freeze the vocabulary contract. Decide the public and internal names, capitalization, and legitimate exceptions first.
  2. Inventory filenames and contents. Enumerate tracked files plus unignored, untracked files with git ls-files --cached --others --exclude-standard -z. A clean commit is not enough if a generated file waiting to be added is already wrong.
  3. Rename structure before prose. Move directories and files, then repair imports, symbols, route helpers, selectors, tests, fixtures, and evidence names until the compiler and focused tests agree on the new graph.
  4. Rewrite copy by audience. Customer text gets plain product language; code gets the canonical identifier. A blind global replacement cannot make that distinction.
  5. Handle machine data separately. Normalize only representations whose decoded meaning can be proven unchanged.
  6. Run a second full inventory, then turn it into a gate. The final search becomes executable policy rather than a one-time cleanup receipt.

This order matters. If you edit prose first, later directory moves generate fresh import and snapshot churn. If you install the guard first, it can block the very intermediate states required to complete the migration. A schema migration has a cutover point; so does a vocabulary migration.

The dangerous edge: machine data and binary coincidences

A forbidden spelling inside prose is language. The same bytes inside an integrity string, compressed payload, or image stream may be pure coincidence. The policy can still require the raw repository bytes to be clean, but the repair must preserve the value that consumers decode.

That rules out a naive replace. Change three characters inside a base64 image and you change the image. Change an integrity value and you can break dependency verification. We use context-specific, equivalent encodings — and a fail-closed manual path for binaries — instead:

  • Inside supported JSON machine fields, Unicode escapes remove the literal source-byte sequence while JSON.parse returns the exact same string.
  • Inside an HTML src or href data URI, numeric character references change the serialized HTML while the decoded attribute value stays identical.
  • Inside PNG, JPEG, or PDF data, the guard only fails closed with a byte offset; the asset is re-exported and its rendered output re-checked by hand — unlike the JSON and HTML paths, no automated equivalence test binds this step.

The normalizer is deliberately narrow. It does not entity-encode a data URI in TypeScript, where an HTML entity would become literal data, and it does not hide a retired term in prose. Tests bind the useful guarantees: parsed JSON equality, decoded attribute equality, idempotence, and a red guard when ordinary prose remains after an unrelated data URI.

This is the subtle principle: byte cleanliness and semantic equality are two separate assertions. A safe migration proves both. “The search has no hits” is not enough if the artifact no longer decodes to the same thing — that only trades a brand problem for data corruption.

A company website needs a content schema too

The same cleanup exposed a second problem. Our marketing site had accumulated implementation notes, release-state language, and repeated explanations. It answered “what did the team change?” more often than “what is this company offering, who is it for, and why should I trust it?”

We separated those jobs:

QuestionHome for the answerWhat stays out
What does the product do?Home and ProductInternal architecture and rollout notes
What does it cost and where are the boundaries?Pricing and LegalVague promises and page-by-page copies of the full legal text
Who operates it and how do I get help?Company, Support, ContactTeam logs and test evidence
What changed in a release?UpdatesEvergreen product explanation
What lesson is worth reusing?BlogChronological “then we edited this file” narration

That last row is why this article exists. A build log records activity. A useful blog post extracts a mechanism a stranger can apply. The company site becomes shorter not because information disappears, but because every kind of information gets one proper home. The one deliberate exception: short trust-critical boundaries — practice chips, no real money — repeat at every point where a visitor decides, and a test keeps them there.

Make the new language executable

A naming guide in a document will drift. The final migration therefore added a guard to both pre-push checks and CI. It examines filenames and raw bytes across the repository inventory, gives line-level findings for text, checks supported machine-data representations for canonical encoding, and fails on binary matches with an offset.

The brand guard is not the website’s only contract. A separate marketing-facts check compares public prices, store availability, solver boundaries, and release-gated features with their canonical code and status sources. Naming and truth are different constraints: a page can use perfectly owned language and still be factually wrong.

The error does more than say no. It states the allowed replacement: Table2/table2 for internal identifiers and “new table” for customer copy. That positive vocabulary matters. A blacklist without a destination only teaches the next contributor to invent a third name.

inventory = tracked files + unignored untracked files

for each file:
    reject retired language in the filename
    reject retired byte sequences in the raw file
    if text:
        require canonical machine-data representation
        report any remaining prose with line context

The gate is intentionally boring and deterministic. No language model decides whether a name feels too close to somebody else’s brand. The policy is reviewed by people; the repository enforces the reviewed result every time.

The honest limits

  • The repository inventory is not the universe. Git history, ignored local files, old backups, external caches, and knowledge stores outside the checkout need their own migration and retention decisions.
  • A byte match is not always a semantic mention. Compressed assets can contain random collisions. That is why the gate reports an offset and the repair must preserve the artifact’s rendered meaning — verified by hand — instead of pretending every hit is prose.
  • A clean vocabulary does not prove original design. Naming hygiene cannot replace a visual-design review, trademark review, or provenance audit.
  • Canonical names can still change. The contract makes a future change explicit and reviewable; it does not make today’s schema permanent.

Recap

  1. Borrow the layout, not the vocabulary. Visual inspiration does not justify inheriting another product’s brand language or internal taxonomy.
  2. Treat brand language as a schema. Define canonical values by audience before editing: plain customer language, stable internal identifiers, and honest content labels.
  3. Migrate the dependency graph. Filenames, imports, routes, tests, evidence, docs, copy, and generated data are connected consumers, not independent search hits.
  4. Prove bytes and meaning separately. Equivalent encodings and careful asset re-exports must remove raw collisions without changing parsed values or rendered output.
  5. Put the contract in pre-push and CI. A cleanup is complete when the retired language cannot quietly return in the next release.