# Engine/config red lines

This is the enforceable edge of
[Config-driven clean structure](./config-driven-clean-structure.md): that
principle says *the engine decides how, source config decides what*. This one
says how we keep it true — a short list of **red lines** that must not appear in
engine code, the **decision tests** a reviewer applies to a new fact, and the
**scan** that makes the boundary falsifiable instead of aspirational.

The key boundary is that reusable engine code must not carry source or product
facts. Those facts live in source config and contracts, with a greppable
red-line scan plus an explicit, justified exception list for rare cases that
cannot be externalized yet.

## Red lines

Scope: reusable engine code — every `engine/` directory under `ingestion/` and
`transform/`, `orchestration/airflow/engine/`, and `ingestion/common/`. Tests
and fixtures are exempt; they legitimately name specific sources.

Block these in engine code:

- a **source or product identifier as a literal token** — a `sources/<name>/`
  or `products/<name>/` directory name (`nexuscrm`, `data_foundation_finance`,
  `cloud_revenue`, `salesforce_orders`, …) written into engine logic, a path, a
  branch, or a lookup table.
- a **per-source value baked into the engine** — bucket names, database/table
  names, watermark columns, classification rules, schedules — anything that
  belongs in `source.yaml` or a per-product contract.
- a **branch on a specific source/product** — `if source == "metadb"`, a
  `{name: ...}` map keyed by source, a source-specific code path in otherwise
  generic CRUD/load/transform.
- a **second read path for a fact a contract already models** — re-deriving a
  product's layer, domain, classification, or lineage in engine code instead of
  reading the one authored/served contract (see
  [ADR 0009](../adr/0009-contract-engine-collates-contracts-and-asset-metadata.md),
  [0015](../adr/0015-contracts-have-a-kind-and-the-producer-owns-the-contract.md)).

Allowed in engine code (generic substrate, not source facts):

- generic structural tokens that apply to every source: the literal directory
  names `sources`, `products`, `contracts`, `engine`, `raw`/`staging`/`curated`
  layer names, snapshot suffixes.
- discovery over the config tree — `glob("sources/*/seeds")`,
  iterate `products/*/` — so the next source is picked up without an edit.
- generic helpers parameterised by config: `classify_table(table, rules)`,
  watermark candidates read from `source.yaml`.

## Decision tests

Apply these to any new fact before putting it in engine code. A "no" means the
fact is source/product config, not engine substrate.

1. **Greenfield.** Would this engine still work for a brand-new source that ships
   only a `source.yaml` and contracts, with no engine edit?
2. **Config self-service.** Can someone onboard or change a source by editing
   Git-authored config alone — no engine PR?
3. **Source-neutral.** Would the same code run identically for an unrelated
   source (a Jira-like system, a new finance hub) with no source-specific branch?
4. **One fact, one read path.** If a contract already models this fact (layer,
   domain, classification, lineage, raw source table), is the engine *reading*
   that one representation rather than re-encoding it?

These mirror the conformance review's intent; run them at design time, not just
at review.

## The scan

`scripts/boundary_scan.py` makes the first red line mechanical. It derives the
protected identifiers from the config tree itself — every `sources/*` and
`products/*` directory name — so the denylist maintains itself as sources are
added, and greps engine code for those identifiers as whole tokens.

```bash
python scripts/boundary_scan.py          # exit 1 on any un-allowlisted hit
python scripts/boundary_scan.py --list   # show derived identifiers + scanned roots
```

Run it before merging a change to ingestion, transform, or orchestration engine
code. It is part of the
[design conformance review](../agents/design-conformance-review.md) — the
code-level guardrail that check 3 ("documented invariants are enforced in code")
asks for.

Escape hatches, both requiring a stated reason:

- inline `# boundary-scan: allow <reason>` on the offending line, for a one-off
  justified mention.
- an entry in `scripts/boundary_scan_allow.txt`, for a pre-existing
  concentration. Each entry is **visible debt**, not a silent exclusion — the
  point is that the gate still catches every *new* leak elsewhere.

A few source/product names are also common English words (`core`, `events`,
`customers`); the scan skips those as bare tokens to avoid noise, and this prose
still governs them.

### Known concentration

`transform/dbt/engine/generate_contracts.py` carries a central registry of
per-product specs (id, layer, domain, classification, upstream lineage, raw
source table). It pre-dates this gate and is recorded in the allowlist. It is
the clearest candidate for externalisation — moving each product's spec beside
the product (or deriving it from the served contract) would let the generator
become source-neutral. Tracked as a follow-up, not resolved here.
