Skip to content

9 min read

A lint cannot detect that two sentences contradict each other

On this page

RFC-118 — the platform team's numbered design proposal, circulated for comment before anything is built — is two pages and opens with a count: the platform monorepo contains 34 instruction files, 33 of them nested, totalling 6,140 tokens, and no single person has read more than six. The proposal is that all agent configuration lives in exactly one file at the repository root. No nested files. No user-layer files for work in this repository. Organisation policy included by reference, vendored into the repository — copied in and committed as an ordinary file, the way a team vendors a third-party dependency it wants pinned and readable — so that CI can detect drift. The three incidents in the RFC's appendix are the ones from the previous chapters — the changesets, the commit prefixes, the session timeout — and in each of them the causal chain runs through two texts that nobody read together.

The objections arrive within an hour and they are not obstruction. The mobile package has no backend and the root's integration-testing rule is noise there. The sdk-python package is the only Python in a TypeScript monorepo. The vendor/ tree is generated and should not be edited at all. These are real differences, and the RFC's answer — put them in the root file — is where the argument becomes interesting.

The case for one file is stronger than it sounds

Precedence ambiguity disappears by construction. Everything from chapter 1 onward has been about the interaction of texts, and one text has no interactions; the five-minute discovery probe, the precedence test, the assembled-configuration inventory and the decision log all become unnecessary at once. That is not a small saving, because each of those is a practice a team has to sustain rather than a thing they build once.

The file becomes reviewable. A single 2,000-token document in the repository root gets read during onboarding, shows up in git blame when someone asks why a rule exists, and produces a diff on every change that a code owner can require review on. Thirty-three nested files produce thirty-three diffs that land in thirty-three teams' review queues, and the changeset rule in chapter 1 was added by one engineer to one file that no reviewer outside that package ever saw.

Vendoring the org layer fixes the invisibility problem from chapter 2 directly. Priya could not read the 1,180 tokens of organisation policy that were outvoting her; if that text is a file in the repository with a CI job that fails when it drifts from the canonical version, then every layer in the system is a file that everyone can read, and the diagnosis moves from a support ticket to a grep.

There is also a token argument that cuts both ways and is worth stating in numbers. Cumulative discovery in this monorepo puts between 1,100 and 2,300 tokens of instruction into a session depending on where it starts, and the single-file proposal that tries to carry every package's conventions would put 6,140 into every session regardless of relevance. The RFC's real proposal is not "one file with everything in it" but "one file with only the things that apply everywhere", and the per-package material has to go somewhere or stop existing.

Two situations where it is clearly worse

The first is the package that genuinely differs, and the mechanism of the harm is dilution rather than omission. Suppose sdk-python's conventions — 150 tokens about type stubs, ruff configuration and the release process to PyPI — go into the root file under a heading naming the package. Every session in the monorepo now carries them, including the 90% that never touch Python, and they compete for attention with everything else. This is measurable: on the retry-helper prompt from chapter 3, the organisation's uncontested copyright header appeared in 3 runs of 5 inside a 2,792-token assembly, and re-running the same test with the assembly padded to 6,100 tokens of mostly-irrelevant instruction dropped it to 1 of 5. Adding instructions reduces adherence to the instructions already there, so a root file that absorbs 33 packages' worth of local detail makes the root's own cross-cutting rules less likely to be followed.

The second is removing the user layer, where the mechanism is displacement. The preferences in those files are real — an engineer who wants verbose explanations because they are learning the codebase, one who wants the agent to stop asking for confirmation, one who needs output formatted for a screen reader — and forbidding the file does not remove the need. What happens instead is a shell alias that prepends a paragraph to every prompt, or a wrapper script in someone's ~/bin, or a snippet pasted at the start of each session. Every one of those is a user layer with worse properties: it is not in a file the host can inventory, so it does not appear in the assembled-configuration view at all, and the incident from chapter 2 becomes strictly harder to diagnose rather than impossible to cause. Banning a layer that has a real reason to exist relocates it somewhere you cannot see.

The compromise that sounds like it settles the argument

The proposal that emerges in the RFC's comments is the one to be careful about: allow nested files, but require that they only ever add and never contradict the root, enforced by a lint in CI. It sounds like it keeps local flexibility and removes the ambiguity, and it is worth being precise about why it does not, since the failure is not an implementation quality issue.

Here is the kind of lint that gets written.

python
# tools/lint_instructions.py — flags nested rules that overlap the root
NEGATIONS = {"skip", "never", "no", "don't", "avoid", "not", "without"}

def keywords(line: str) -> set[str]:
    return {w.strip(".,`").lower() for w in line.split() if len(w) > 4}

def contradicts(root_line: str, nested_line: str) -> bool:
    shared = keywords(root_line) & keywords(nested_line)
    if len(shared) < 2:
        return False
    root_neg = bool(keywords(root_line) & NEGATIONS)
    nested_neg = bool(keywords(nested_line) & NEGATIONS)
    return root_neg != nested_neg

Run it across the 33 nested files against the root and the results are not close to usable. The eleven genuine conflicts a human found by reading everything over two days included the following pairs, and the lint's verdict on each is instructive.

Root instructionNested instructionShared wordsContradictsLint says
Every user-visible change ships with a changesetThis package is not published; skip changesetschangesetYesNo — one shared word
Write integration tests for new endpointsThis package has no backendnoneYesNo
Prefer composition over inheritanceAll handlers extend BaseHandlernoneYesNo
Use pnpm for all installsRun pnpm build before committingpnpm, buildNoYes — flagged
Never commit secrets to sourceTest fixtures may commit dummy tokenscommitPartlyYes — flagged

The two failure directions have different causes. Missing the real conflicts is a semantic problem: "this package has no backend" contradicts "write integration tests for new endpoints" through a chain of world knowledge — integration tests here test the backend, and a package without one cannot have them — and no lexical rule reaches that. Flagging the innocuous overlaps is a specificity problem: text about the same subject is usually not in conflict, and a check that treats overlap as suspicion will fire on every nested file that mentions a tool the root also mentions. On this corpus the lint caught 3 of 11 real conflicts and produced 9 false positives out of 12 flags, which is a precision of 0.25 — the share of its flags that were real — against a recall of 0.27, the share of the real conflicts it found.

Turning the check over to a model improves both numbers and does not change the shape of the problem. Asking a model to compare each nested line against the root, on this corpus, found 9 of the 11 and flagged 4 innocuous pairs, which is a genuinely useful tool and a considerable improvement over grep. It is still probabilistic, it still misses two, and the two it missed were the subtle ones that a lexical check also missed. A team that runs it and sees a green CI job will conclude the ambiguity has been designed away, which is the actual danger — the eleven conflicts were survivable while everyone knew they might exist, and a check that certifies their absence removes the vigilance that was doing the work.

What to ship

The rule that survives contact with both the RFC and the objections is narrower than either side proposed.

ElementRuleWhy this line
Root fileOwns every cross-cutting policy: security, testing, release, commitsOne place to change a rule that applies everywhere
Nested filesAllowed, capped at 40 lines, only on a declared list of subjectsLocal difference is real; unbounded local policy is not
Nested contentMust name its path and the root rule it exceptsChapters 4 and 5: no rivals, only stated exceptions
Org layerVendored into the repo, CI fails on driftEvery layer becomes readable by everyone
User layerPermitted for interaction style; forbidden for anything affecting shipped artifactsPreserves the real need, removes the invisible policy
ReviewNested files carry a CODEOWNERS entry to the platform teamThe conflicts are visible only to someone who reads both
VerificationThe chapter 3 precedence test committed and re-run on host upgradesBehaviour is measured, not assumed

The subject list is the load-bearing part and the reason this is not just "be careful". Nested files may speak about build tooling, language-specific idiom, test layout and publication; they may not speak about security, secrets handling, dependency policy or anything the organisation layer covers. That converts an open-ended precedence problem into a bounded one, since the pairs that can conflict are drawn from a set someone chose.

The first thing that will go wrong

The subject list becomes the battleground. A team that wants an exemption from the integration-testing rule and cannot write it under a permitted heading will write it under one they are allowed to use — a sentence about test layout that says fixtures live in __mocks__ and that endpoint tests are handled by the platform's contract suite, which is a testing exemption wearing a permitted hat. It will pass review, because it is 40 lines in a package the reviewer does not work in and it does not contain the word "integration". The mechanism is the same one that has run through every chapter here: the rule is enforced on the form of the text and the conflict lives in its meaning.

What to watch for is therefore not compliance with the subject list but the two measurements that detect the underlying problem regardless of how it is smuggled in. The leak audit from chapter 4, run monthly, tells you whether package conventions are reaching code that never asked for them. The count of nested files and their total token weight, plotted over a quarter, tells you whether the cap is holding; a repository that goes from 8 nested files to 25 has not become more expressive, it has started using local files to avoid arguments about the root. Both take minutes and both fail loudly, which is the property the lint lacked.

That is the whole toolkit for this material: know which discovery model you have, read what a session assembled rather than what a file says, measure how layers combine instead of assuming, name the boundary of a local convention, resolve conflicts by deciding an owner and deleting the loser's statement, and build a gate for anything that has to hold. None of it is expensive individually. The next thing to do with it is to open the repository you actually work in, run /context from inside its most deeply nested package, and find out how many of the texts in that list you have never read.

Scoping and precedence — read the current version at https://daviddominguez.dev/learn/en/scoping-and-precedence/a-lint-cannot-detect-that-two-sentences-contradict-each-other

Back to top