Thanks for considering a contribution. Patina is a pattern-based tool, so the most impactful contributions are usually new patterns, better examples, Document Type policies, or Persona refinements.
User-facing documentation lives in README*.md, docs/, examples/, patterns/, document-types/, personas/, and the skill entrypoints. Maintainer or agent notes (backlogs, runbooks, launch playbooks) are kept out of this repository entirely — they live in the maintainer's private workspace, and docs/internal/ is gitignored as a guard. Only real user-facing facts get promoted into the public docs list.
When moving a root-level Markdown file, link it from README.md if it is public; if it is maintainer-internal, it does not belong in the repository.
Feature branches (bot/* / feat/*) branch from dev → PR into dev
(integration/staging) → at release, dev → main via a merge (not squash)
PR with a version bump. Keep dev at or ahead of main. Run parallel work in
separate git worktrees (one branch each, branched from dev). Full guide:
docs/WORKFLOW.md.
Primary user docs should keep a Korean companion when they explain installation, support, contribution, examples, or troubleshooting. The required pairs are:
README.md→README_KR.mdCONTRIBUTING.md→CONTRIBUTING_KR.mddocs/FAQ.md→docs/FAQ_KR.mddocs/AUTHENTICATION.md→docs/AUTHENTICATION_KR.mddocs/EXAMPLES.md→docs/EXAMPLES_KR.md
When a PR changes one of these English files, update the Korean pair in the same PR or explain why the translation can safely lag. Keep commands, paths, config keys, issue numbers, and code fences unchanged unless the source file itself changes them.
-
Pick the right pack. Patterns live in
patterns/{lang}-{category}.md. Categories: content, language, style, structure, communication, filler. -
Follow the template. Each pattern needs:
- Number (next available, e.g. #30)
- Watch words
- Fire condition (when should it trigger?)
- Exclusion condition (when should it NOT trigger?)
- Problem description
- Before/after example
-
Add to all languages you can. We have 4 language packs (ko, en, zh, ja). If you only know one, that's fine — file the PR for that language and note the others need translation.
-
Update counts. After adding a pattern:
- Pack header: increment
patterns:count - README.md and README_KR.md: update pattern tables and totals
- SKILL.md description: update total if hardcoded
- Pack header: increment
-
Add an example. If possible, add
examples/{lang}-{number}-success-01.mdandexamples/{lang}-{number}-failure-01.md(false positive case).
The most common improvement: better before/after examples. The "after" text should preserve the original meaning — not rewrite it into something different.
Good test: if someone read only the "after" text, would they get the same takeaway as the "before"? If the sentiment flips, the example is bad.
Before opening a pattern PR, check:
- Fire condition: would at least 2-3 real AI-generated examples trigger it?
- Exclusion condition: can a human-written, domain-appropriate example avoid the hit?
- Semantic risk: what facts, numbers, polarity, causation, or domain terms could be damaged by the rewrite?
- Before/after pair: does the after version preserve the same claims without merely swapping synonyms?
- Freshness evidence: link a 50-document hot/cold fixture, manifest, or collection plan when proposing an emerging model-era tell.
- Count sync: pack frontmatter
patterns:must match numbered### N.pattern headings.
False positives are expected, especially for academic, encyclopedic, legal, corporate, or heavily edited prose. To report one:
- Use the false-positive issue template.
- Include language, genre/register, score/audit excerpt, and the specific pattern that over-fired.
- Remove private text or replace it with a minimal redistributable excerpt.
- Suggest whether the fix should be an exclusion rule, lower severity, Document Type
pattern-overrideschange, or benchmark fixture.
Maintainers should prefer tightening exclusions over deleting patterns outright.
Suspect-zone fixtures live under tests/fixtures/suspect-zones/{lang}/{ai|natural}/.
Each fixture needs YAML frontmatter:
---
fixture_id: en-ai-07-example
language: en
class: ai
expected_hot: true
why_designed_this_way: |
Explain which deterministic signal should fire and why.
expected_metrics:
cv_band: low
---Then run:
npm run benchmark:reportThis regenerates tests/quality/results.json, docs/benchmarks/latest.json, and docs/benchmarks/latest.md.
Patterns (above) are LLM-executed catalog entries. A detection signal is
different: a deterministic hot/cold input computed in src/features/* and
folded into the per-paragraph hot OR rule (burstiness, MATTR, lexicon density,
the Korean diagnostics composite, the ending-monotony signal, etc.). The
analysis layer stays LLM-free, so a new signal is real engineering with a
calibration bar. Follow this loop:
- Diagnose the miss with evidence. Find where detection fails on a labeled
manifest, not by intuition.
score_review.trigger_countsin the scored manifests shows which signals fire on which rows;npm run benchmark:signal-impactreports each existing signal's marginal catch/FP so you can see the gap. - Find a false-positive-safe discriminator. Compare AI misses against human
controls at matched length/register (a short-text confound is not an AI
tell). The discriminator must separate AI from the human register that
shares the surface feature — e.g. plain
-다AI vs formal-human-다needed a burstiness conjunct, not-다alone. - Implement it first-class, never by coupling an advisory payload. Advisory
signals (
translationese,koPostEditese.v1) must not feed the hot verdict (see docs/TRANSLATIONESE-KO.md). Add a dedicated computation insrc/features/stylometry.js, wire it into the hot OR insrc/features/index.js, and add a precision gate (length/count floors) if it over-fires on short or corner-case text. - Keep runtime surfaces aligned. A signal is not done until it is consistent across:
src/features/index.js/ server-side feature callers such assrc/web-rewrite-stream.js,scripts/rebaseline-score.mjstrigger_counts, the hot-rule prose incore/stylometry.mdandSKILL.md, and unit tests (including precision guards). The browser playground is aplayground/chatgpt.jsUI over server-side detection/scoring, not a separate deterministic-analysis mirror. - Measure with the harness, not by hand. Run
npm run benchmark:signal-impactfor the marginal before/after,npm run benchmark(the 49-fixture suite must stay 100% — natural fixtures must not flip hot), and confirm the human-control false-positive rate stays within the published CI indocs/benchmarks/rebaseline-latest.md. Record the measured numbers in the changelog. The frozen public claim manifests are refreshed in a separate rebaseline pass, not in the signal PR. - Version it. A new detection signal changes hot behavior → minor bump (it is additive, not a removal). Bump all version surfaces and add a changelog entry with the measured catch/FP deltas and the failure mode you guarded.
Acceptance bar (mirrors the roadmap's deterministic-feature-expansion criteria): recall or precision improves on the labeled manifest, the human-control false-positive rate stays within the published tolerance, and the signal ships with a documented failure mode plus before/after examples.
See docs/HARNESS.md for the full measurement-tool map.
- Preserve the original semantic anchors: numbers, entities, negation, causation, and modality.
- Do not translate an English AI tell literally if it is not a tell in the target language.
- Add a target-language false-positive note when a phrase is normal in that register.
- Keep examples redistributable; do not paste private user text.
Document Types live in document-types/{name}.md. Copy an existing policy
(for example blog.md), set document-type: to the filename stem, and define
scope, purpose, audience, structure, style, avoid, and
language-scoped pattern-overrides. suppress is applied deterministically;
reduce/amplify document policy intent but do not currently change a runtime
weight. Keep Persona voice, casual/professional Register markers, and
verification thresholds out of this axis.
For reusable voice, create a Persona v2 with patina persona new. Persona
frontmatter is validated to reject document policy, Register, and safety fields.
AI writing patterns evolve as models get fine-tuned. Some patterns decay (e.g. "delve" after it became a meme), while new ones emerge.
How we handle this:
- Community reporting: If you notice a pattern that's no longer a reliable signal, open an issue
- New pattern proposals: If you spot a new AI tell, file an issue with 3+ real-world examples and a 50-document evaluation fixture or collection plan
- Quarterly review: Maintainers follow
process/pattern-freshness.mdfor corpus freeze windows, promotion thresholds, and frontmatter metadata - Lexicon provenance: Newly mined or re-mined lexicon entries need
added,source, andlast_validatedprovenance before changing shipped behavior; runnpm run lexicon:freshnessto verify sidecars match the shipped entries - Version notes: Each pattern pack has a
versionfield — bump it when patterns change - No deletion without replacement: We don't remove patterns outright; we mark them as
lowseverity or move them toreducein a Document Type policy
Patina uses semantic versioning for both CLI behavior and pattern-pack compatibility.
- Major: remove or renumber patterns, break config/result schemas, change public CLI semantics, or make existing pattern packs incompatible.
- Minor: add a pattern, language, Document Type, Persona, mode, backend, benchmark schema field, or contributor-facing workflow.
- Patch: fix bugs, adjust severity/exclusions, clarify examples, update docs, refresh benchmark fixtures without changing schemas.
Every changelog entry should include a short semver rationale line so downstream users know whether to pin, test, or upgrade normally.
Be helpful. Don't be a jerk. AI writing patterns are not moral failings — we're building a tool, not a tribunal.
- Fork and branch from
main - Make your changes
- Verify pattern counts are consistent
- Open a PR with a clear description
- Bonus: include before/after examples that demonstrate your change