llm-security-commons/CONVENTIONS.md
Kjell Tore Guttormsen f0a511369d fix(spec): section 7 stated its own premise and then applied itself beyond it
Section 7 justified the fixture-is-ground-truth ordering with "Two
implementations that return different verdicts" and then stated the rule
with no scope. For signatures/active-content.json there is no second
implementation, and the seed runtime has stated the classification behind
it is calibration it does not freeze. As written, section 7 turned a change
they reserved into a bug on their side.

New section 7.1 keys the scope on a structural property, never on a table
name: a scope only one runtime implements, whose payload that runtime
authored. It creates no fourth verdict - the declaration schema closes
result with additionalProperties:false over four counts, so a fifth would
break every consumer's parser. The case still fails and is still named in
failed_cases; what changes is what the failure licenses concluding.

Two limits are stated rather than left to inference: it does not reach a
third-party implementer of the same table, and it is not a licence for a
runtime to self-declare its own divergence as calibration.

manifest.json 0.6.1 -> 0.6.2 retires the open-question sentence, quoted
rather than dropped. The retirement is partial: "section 7 is NOT amended
by this block" stays true, because the spec was amended by its own release.

Neighbours measured over the whole repository, widened past "ground truth"
to the second paragraph's own wording. CONVENTIONS.md and CLAUDE.md carried
the premise and are changed; SECURITY.md gets a cross-reference only, since
its claim is about a fixture expecting too little and 7.1 does not narrow
that direction; README.md and docs/extraction-plan.md are named as
deliberately untouched.

Breaking in category, minor in number - 0.x, per the reading [0.3.0]
recorded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012pZ2FLQ6xkWvj2VcwgwnQv
2026-08-13 23:31:28 +02:00

205 lines
11 KiB
Markdown

# Conventions
The rules a change to this repository is held to, in one place.
Nothing here is new policy. Every rule below was already being applied — some of it stated in
[README.md](README.md), some in [SECURITY.md](SECURITY.md), some only visible in the shape of
the files themselves. It is collected here because a convention that only exists in the
maintainer's head is not a convention an outside reader can meet, and because two of the
decisions this repository makes — that nothing here runs, and that pull requests are switched
off — look arbitrary until the reason is written down next to them.
This file binds **contributions to this repository**. It does not bind the runtimes that read
the data; that is what `spec/` is for, and those files say `Status: normative` and mean it.
## The charter: nothing here runs
**This repository contains no executable code, and it will not acquire any.**
Not permitted, without exception:
- `.mjs`, `.js`, `.ts`, `.py`, `.sh` — or any other file that implements detection, scanning,
normalisation, scoring or I/O;
- `package.json`, `pyproject.toml`, lockfiles, dependencies, build steps;
- an import of, or knowledge of, any framework, SDK or runtime;
- network access, model calls, dependence on the clock, or randomness.
Permitted: JSON data, normative specifications in Markdown, and conformance fixtures
(`input.txt` plus `expected.json`).
The reason is `fork-and-own`. A consumer on a runtime nobody here has thought of should be able
to vendor this repository without inheriting a single technology choice. A build step is a
technology choice; so is a test runner. The moment one exists, the set of runtimes that can
adopt this core shrinks to the set that tolerates it.
The consequence is that **this repository cannot validate itself**. There is no CI in this
organisation and nothing runs on push. The checks below are yours to run, and they are the only
ones there are.
## How a change gets in — and why not by pull request
Pull requests are switched off on the canonical repository at
`git.fromaitochitta.com/open/llm-security-commons`, and issues are not the reporting channel
either. That is deliberate, and the reason is stronger than a preference about tooling.
This repository is **vendored into independent runtimes** — a Claude Code plugin on Node/ESM, a
Python guard, an advisory surface — each pinning a tag. The contract between them is semver, and
a change to detection data changes what those runtimes *find*. A patch to a pattern table is not
a contribution that can be merged and then socialised; it is a contract change that has to be
coordinated with every consumer **before it exists**, because the moment it is tagged, the next
consumer to pull it gets different findings than the one that pulled yesterday. A merge button
does not do that, and nothing downstream of a merge button can.
So the routes in are:
1. **Fork and own it.** MIT, and an intended use rather than a tolerated one. If you need a
different value in your runtime, this is the fast path and it is fully supported.
2. **Report it privately** — see [SECURITY.md](SECURITY.md). A wrong entry in a detection table
is a silent false negative in every runtime that reads it, so a report about one is a
security report even though nothing here executes. That file also explains why a confirmed
defect in extracted data is usually decided in the runtime it came from before it changes
here.
If you maintain a consumer, the coordination channel is direct contact with the maintainer, not
this repository's forge surface.
## Data files (JSON)
- **Every JSON file carries a top-level `"version"`** — a semver string. No exceptions.
- **Every JSON file states what it is and where its data came from**, in a top-level
`"$comment"` or `"description"`. Provenance is not optional metadata here; it is what makes
the difference between a table and a rumour.
- 2-space indentation, LF line endings, a trailing newline, UTF-8 without BOM.
- **Code points are written as strings** — `"U+200B"` — never as the raw invisible character.
Review cannot see what it cannot render, and a reviewer who cannot see a character cannot
check it. The single exception is `conformance/*/input.txt`, which by definition contains the
actual bytes.
- **Keys are stable identifiers.** Consumers match on them. **Changing a key is a breaking
change** and is versioned as one.
The four files that carry detection material — `codepoints/carriers.json`,
`signatures/secret-egress.json`, `signatures/malware-signatures.json`,
`signatures/active-content.json` — take one further rule, which is the most important line in
this document:
> **They are changed only from verified source data** — a dump from the owning repository, the
> Unicode standard, published vendor documentation. Never from memory, never "filled in for
> consistency". A guessed code point or a regex with wrong escaping is a silent false negative:
> a detector that looks like it is working and is not looking. If an entry cannot be verified,
> leave it out, or mark it explicitly unverified in its `$comment`.
## Specifications (Markdown)
- A normative specification carries **`Status: normative`** at the top and uses RFC 2119 terms
(MUST / MUST NOT / SHOULD / MAY) in uppercase, deliberately. These files bind the
implementations that read them.
- An informative document (`docs/`) carries **`Status: informative`** and is **never ground
truth**. It records measurements, history and open disagreements; a runtime is not wrong for
disagreeing with one.
- Naming a file that does not exist yet is allowed where the layout is part of the contract —
`spec/decode-pipeline.md` is named in README.md and marked **Planned**. It is not a link, and
nothing depends on it. A normative spec guessed at would be worse than an absent one.
## Conformance cases
- One directory per case: `conformance/<case-id>/input.txt` and
`conformance/<case-id>/expected.json`.
- `<case-id>` is stable and descriptive. **Changing a case id is a breaking change** — a
published conformance result names it.
- `expected.json` is **ground truth**. If a runtime disagrees with it, the runtime is wrong.
- One way that reverses: the fixture is proven wrong. Then the fixture changes **in its own
commit, with the reason written down** — never folded into a change that does something else,
because a fixture edit is the one edit that can make every conforming runtime wrong
identically.
- The other, added in `v0.9.0`: where a case's scope is a table only one runtime implements and
that runtime authored the payload, a divergence by **that** runtime is neither a proven-wrong
fixture nor necessarily its bug. The fixture is not rewritten on the divergence alone — it is
recorded against the version pinned, and re-pinning is a separate release. Through `v0.8.1`
this list carried only the first way. See
[`spec/conformance-corpus.md` §7.1](spec/conformance-corpus.md).
- A case declares the data files it is `scope`d to. A runtime that does not implement a scoped
table reports the case `not-applicable` — a third verdict beside pass and fail, and one that
must be reported rather than dropped from the denominator. See
[`spec/conformance-corpus.md`](spec/conformance-corpus.md).
## When a value may change
Detection values do not change here because someone here judged them wrong. Three mechanisms
have moved a value so far, and each is recorded in the file itself rather than only in the
changelog:
1. **Re-extraction** — the owning runtime changed its own value, and this repository re-read the
source at a pinned public commit. Recorded in `source_fidelity.post_extraction_drift`.
2. **Retraction** — this repository described its own provenance wrongly. The stored value may
have been right all along; the account of where it came from was not. Recorded in
`source_fidelity.retracted`.
3. **Owner-directed authoring** — the owning runtime decided a value and asked this repository
to carry it, because the dependency has inverted: the source now reads *this* file and holds
no literal to re-read. Recorded separately, in `source_fidelity.owner_directed_changes`,
precisely because calling it drift would assert that the source moved and commons followed —
which would be false in the one direction that matters.
What is **not** on that list is merit. Data extracted from an implementation is kept
behaviour-identical to it on purpose, because a copy that disagrees with its source is the exact
failure this repository exists to prevent. Producing one as a *fix* would be self-defeating. If
you believe an extracted value is wrong, say so — and expect the decision to be taken in the
runtime where the pattern is under test.
Data **authored here** rather than extracted — conformance payloads, flagged as
`authored_payloads` in `conformance/manifest.json` — is this repository's own to correct.
## Versioning
Two version numbers, and they are not locked to each other:
- **The repository tag** (`vX.Y.Z`) versions **the contract**: the file set, the key names, the
case ids, the disposition semantics.
- **Each JSON file's own `"version"`** is bumped when *that file* changes.
What counts as which:
| Change | Bump |
| --- | --- |
| New data field, new entry | minor |
| Changed or removed key, case id, or layout | **major** — consumers break |
| A change to what a conforming runtime *finds* | minor in 0.x, and the changelog says so |
That last row is why **the changelog entry is the thing to read before upgrading, not the
version number**. Pre-1.0, a release that changes findings is still a minor bump; only the entry
tells you whether your assertions move.
Consumers vendor **a tag, never `main`** — a conformance result can only be attributed to a tag.
Nothing polls for updates; when a change moves detection data, the maintainer notifies known
consumers directly, and their upgrade remains their own action on their own schedule.
## Checks to run before proposing a change
There is no CI. These four are the validation surface, they run offline in a second, and each
one has been confirmed to go red on a violation rather than merely green on a clean tree.
```bash
# 1. Every JSON file is well-formed
find . -name '*.json' -not -path './.git/*' -print0 \
| xargs -0 -n1 python3 -m json.tool > /dev/null && echo OK
# 2. Every JSON file outside conformance/ carries a top-level "version"
find . -name '*.json' -not -path './.git/*' -not -path './conformance/*' -print0 \
| xargs -0 python3 -c 'import json,sys
missing=[p for p in sys.argv[1:] if "version" not in json.load(open(p))]
print("\n".join("MISSING version: "+p for p in missing) or "OK")'
# 3. Every spec carries its normative marker (prints offending files, nothing = clean)
grep -L 'Status: normative' spec/*.md || echo OK
# 4. Charter guard: no executable code has crept in
find . -type f \( -name '*.mjs' -o -name '*.js' -o -name '*.ts' -o -name '*.py' -o -name '*.sh' \) \
-not -path './.git/*' | grep . && echo 'CHARTER VIOLATION' || echo OK
```
They are written as shell one-liners rather than shipped as a script because a script would be
`.sh`, and check 4 would then fail on the tooling meant to enforce it.
What they do **not** check: whether a value is *correct*. Nothing offline can. That is what the
conformance corpus is for, and it runs in each consumer's own test suite against a pinned tag —
constraining two of the seven data files, which is a real limit and is stated in
[README.md](README.md) under **Known limitations**.