Requirements
What this repository must do, as opposed to what it currently does. Each
requirement carries an identifier, and a test declares what it verifies with a
@verifies REQ-#### tag on its own line directly above the test. The trace matrix is
the projection of the two — generated, never hand-maintained.
Scope is deliberately the shared surface: the properties other repositories depend on, where a regression here breaks something there. Pipeline execution is out of scope for now — it has integration coverage but no unit evidence, so requirements there would need evidence written alongside them. Platform administration is out of scope permanently: it was removed in 2.3.0 (ADR-0004).
A requirement here states a property and its consequence. It is not an ADR — the reasoning for a decision lives in decisions — and it is not documentation of behaviour, which lives in the pages these link to.
The register
| ID | Requirement | Verified by |
|---|---|---|
| REQ-0001 | celine.governance imports on core dependencies alone |
tests/test_governance_contract.py, .github/workflows/governance-thin.yaml |
| REQ-0002 | Catalogue and dataspace exposure are separate gates, ANDed | tests/test_governance_exposure.py |
| REQ-0003 | The package works on every Python version it claims | tests/test_supported_python_range.py, .github/workflows/test.yaml |
| REQ-0004 | The published facet schema URL keeps resolving | tests/test_governance_contract.py |
| REQ-0005 | An unknown key never silently changes a dataset's governance | tests/test_governance_contract.py, tests/test_governance_subclassing.py |
| REQ-0006 | A declared dependency resolves to a producer, or is reported | tests/test_governance_graph.py |
| REQ-0007 | A schedule contradicting the graph is reported, and an uncertain one is not asserted | tests/test_governance_graph.py |
| REQ-0008 | An ownership name leaves the library as an organisation id | tests/test_governance_ownership.py |
| REQ-0009 | Outside dev a pipeline config never falls back to the client id as its secret | tests/test_pipeline_config_posture.py |
| REQ-0010 | A row filter says whether it binds a person or an organization; undeclared is a person, anything else is refused | tests/test_governance_row_filter_binds.py |
| REQ-0011 | A governance tree is checked as it will be served, and any failure fails the run | tests/test_governance_check.py |
| REQ-0012 | Every exposed, non-open dataset states both row-policy layers, with known handlers | tests/test_governance_check.py |
| REQ-0013 | A dataspace offer's consent declarations agree with its person layer | tests/test_governance_check.py |
| REQ-0014 | The owners registry is validated strictly, and every dataspace offer's ownership names resolve | tests/test_governance_check.py |
REQ-0001 — celine.governance imports on core dependencies alone
celine.governance and every module beneath it MUST import successfully with only
pydantic, pyyaml and jsonschema installed.
It MUST NOT import celine.utils, SQLAlchemy, OpenLineage, Keycloak, pandas, dbt,
Meltano or Prefect, at module scope or inside a function. Where a governance module
needs something from the fat side, the dependency inverts: celine.utils may import
celine.governance, never the reverse.
Consequence if violated: dataset-api, ds and celine-superset cannot import
this package without inheriting an orchestration stack, which is the condition that
produced four disagreeing parsers. The violation appears at a consumer's install,
not here — a developer environment has every extra installed.
Rationale: ADR-0001. Behaviour: the governance library.
REQ-0002 — Catalogue and dataspace exposure are separate gates, ANDed
expose MUST gate catalogue listing and API serving. dataspace.expose MUST gate
the dataspace offer. Dataspace access requires both.
exposeMUST be tri-state. Unset MUST fall back todataspace.expose, so a file written before the split keeps its catalogue behaviour unchanged.dataspace.exposeMUST NOT fall back in either direction.expose: falsewithdataspace.expose: trueMUST be reported as a conflict rather than resolved. It MUST be reported rather than raised, so a caller can collect every conflict in one run.- An override that states
expose: falseMUST withdraw a dataset the baseline exposed.
Consequence if violated: either data is published that the catalogue never advertised, or an offer someone deliberately made is silently dropped. Both are security-relevant, which is why neither direction may be chosen implicitly.
Rationale: ADR-0003. Behaviour: the two gates.
REQ-0003 — The package works on every Python version it claims
The Programming Language :: Python :: 3.x classifiers in pyproject.toml are the
single declaration of the supported range. requires-python MUST agree with them,
and CI MUST build its matrix from them rather than from a second list.
Every submodule of celine.governance and celine.utils MUST import on every
version in that range, with the extras installed.
Consequence if violated: the supported range becomes aspirational. celine-sdk
lost two of 553 submodules to a version-gated import while its own suite passed, and
this package once declared a 3.12 floor it did not need — inherited through an
extras-only dependency — which locked celine-superset out entirely, since it ships
inside apache/superset:6.0.0 on Python 3.10.
REQ-0004 — The published facet schema URL keeps resolving
celine.governance.facet.SCHEMA_URL MUST remain
https://celine-eu.github.io/schema/GovernanceDatasetFacet.schema.json, and that URL
MUST keep serving a schema the emitted facets validate against.
The schema files MUST stay at schema/ in the repository root, and MUST remain
readable from the installed package through importlib.resources — not by walking
from __file__, which fails inside a wheel or a container.
Consequence if violated: no build breaks. Every OpenLineage event already emitted and sitting in Marquez carries this URL, so changing or moving it silently invalidates historical lineage, and nothing reports it.
Rationale: ADR-0002.
REQ-0005 — An unknown key never silently changes a dataset's governance
A key the grammar does not define MUST be preserved in rule.extra rather than
dropped, and MUST be reported by validate — as a warning by default, and as an
error under strict=True.
The split between a field and extra MUST follow the model class being parsed into,
including a consumer's subclass. A subclass split against the base grammar's key set
has the same failure as a forgotten KNOWN_KEYS entry once had: the field reads as its
default forever, and nothing warns.
KNOWN_KEYS MUST list every field of GovernanceRule that a file may declare. It is
what validate reports unknown keys against, so a field present in the model and absent
from it is reported as unknown — a legitimate field called a mistake.
Parsing MUST go through model_validate on the keys a file actually declared, so
model_fields_set distinguishes unset from set to a falsy value.
Consequence if violated: a misspelled key — access_levl: open — validates
against the schema, is discarded by the model, and the dataset silently takes the
default. A model class narrower than the file it is handed does the same thing to
fields that are spelled correctly. Under kwargs construction the merge degrades to
"override always wins" and expose: false becomes inexpressible, which is how a
withdrawal that validated clean left a dataset in the catalogue.
Behaviour: unknown keys, the companion's knowledge.
REQ-0006 — A declared dependency resolves to a producer, or is reported
depends_on MUST name datasets, never pipelines, and the producing pipeline MUST
be resolved by matching each entry against the sources keys of every governance file
in the scanned set.
- Matching MUST be case-sensitive
fnmatchapplied in both directions, so either the dependency or thesourceskey may be the glob. - An entry resolving to no producer MUST be reported, unless it declares
external: true. An entry declaringexternal: truethat does resolve MUST be reported as informational rather than as a fault. - A dataset declared by two governance files MUST be reported.
- A pipeline that cannot be placed in a run order MUST be reported and MUST NOT be emitted in a tier.
- Absent
depends_onMUST remain distinct fromdepends_on: []: the first states that a pipeline has not declared its inputs, the second that it has none. - An unknown key at the root of a governance document MUST be reported by
validate, on the same terms as an unknown key inside a block. - The graph MUST be computable within the dependencies of REQ-0001.
active: falseMUST mark a pipeline as not meant to run, and an active pipeline reading from an inactive one MUST be reported.
Consequence if violated: each app is its own dbt project, so ref() never crosses
an app boundary and nothing else in the platform can see the ordering across pipelines
— it lives in cron offsets and hand-maintained tier tables. A dependency that resolves
to nothing and is not reported produces a graph that looks authoritative and is
silently incomplete, which is worse than no graph: an ordering is read as an
instruction. The both-directions rule is what keeps this from happening per
deployment: ds_dev_* is the default everywhere, but a consumer resolves the schema
through CELINE_SILVER_SCHEMA / CELINE_GOLD_SCHEMA, which a deployment may point
anywhere.
Behaviour: depends_on,
the CLI.
REQ-0007 — A schedule contradicting the graph is reported, and an uncertain one is not asserted
Given a deployment's scheduled flows, the graph MUST report a producer/consumer pair whose crons contradict the declared edge:
- Collision — the two can start in the same minute, so which run the consumer sees depends on which container starts first.
- Inversion — both fire hourly, never in the same minute, and the consumer is always earlier in the hour, so it always reads the previous run's output.
Two things it MUST NOT do:
- Assert an ordering it cannot know. Governance is per app; a deployment schedules
flows, and one app can deploy several on independent crons. Where either side
deploys more than one, the pairing may not be the one that moves the data, and the
finding MUST be reported as advisory and excluded from
--strict. - Compare minutes across periods. Pairs on different periods MUST be reported only on an identical cron expression; reasoning about a real offset between an hourly and a daily flow needs the run duration, which is in no file.
Schedules MUST NOT be read from governance.yaml. A cron is a deployment fact — one app
runs several flows, and the same pipeline runs on different schedules in different
deployments — while a governance file is one per app and is shared across every
deployment that installs it.
Consequence if violated: the failure this catches is silent by construction. A consumer that runs before its producer succeeds every time, on stale data, and no test or alert fires — the ordering was only ever expressed as a cron offset, which is a convention nothing enforces. The advisory split matters just as much: a check that reports a pairing it cannot verify as though it were certain gets ignored, and then so does the one that was right.
Behaviour: governance graph.
REQ-0008 — An ownership name leaves the library as an organisation id
Given an owners registry, resolution MUST replace every ownership[].name with the id
of the owner it resolves to, so a resolved rule names organisations and never a
placeholder such as rec or dso.
- A name that is already an owner id MUST resolve to itself. An owner id MUST win over a placeholder or alias of the same name.
- A placeholder map MUST be accepted separately from the owners file. It belongs to the deployment's local configuration, so public governance files keep generic placeholders and an owners file lists organisations only.
- Resolution MUST run after a deployer overlay is merged, never per file.
- Whether an unresolved name raises or is kept with a warning MUST be stated by the caller; there is no default. Under the raising mode every unresolved name MUST be reported, with where it was declared. A report-only check MUST exist that never raises.
- Resolution MUST NOT modify its input, MUST keep a consumer's subclass, and MUST NOT mark as set any field the file did not declare, so a resolved rule still merges as declared (REQ-0005).
- Without an owners registry, names MUST be kept as written.
Consequence if violated: consumers use the name as written — as an asset's owner label, an ODRL assigner or a recipient — so a placeholder reaches the wire, matches no organisation, and the dataset is unreachable while every file validates. Resolving before the overlay fails a load on a placeholder the deployment has already withdrawn.
Behaviour: resolving ownership names.
REQ-0009 — Outside dev a pipeline config never falls back to the client id as its secret
Building a PipelineConfig MUST raise celine.sdk.posture.InsecureConfiguration when the
OIDC client secret is empty or equal to the client id (svc-pipelines, the value
CELINE_OIDC_CLIENT_SECRET falls back to), unless the posture signal is exactly dev:
CELINE_ENV, then ENVIRONMENT. Unset, prod, staging, test or a typo is hardened. In
dev the fallback MUST be accepted with a warning. PREFECT_MODE MUST NOT influence it: it
selects how a flow is scheduled, not what it may authenticate with.
The secret MUST be read when the config is built, not when the module is imported.
Consequence if violated: a deployed pipeline authenticates to the broker with a credential anyone can derive from the client list, and nothing downstream objects.
Behaviour: environment.
REQ-0010 — A row filter says whether it binds a person or an organization
Each row_filters entry MAY carry binds: person | organization. An entry without it
MUST read as person (row_filter_binds, DEFAULT_ROW_FILTER_BINDS). Any other value
MUST be refused: by governance.schema.json (an enum) and by GovernanceRule at parse
time, naming the entry. The value MUST survive a merge and reach the facet unchanged.
Consequence. Consumers decide on it: ds gates a dataset on consent only for a
filter binding a person, and dataset-api checks it against the handler. A misspelled
value read as the default would silently change what gates a dataset, which is why it
fails instead; an absent one errs towards gating.
REQ-0011 — A governance tree is checked as it will be served, and any failure fails the run
celine-governance validate MUST check every governance.yaml its arguments name,
each materialized with its deployer overlay by the resolver the exporter uses. An
overlay MUST NOT be validated alone against the root's required keys; it MUST be
type- and key-checked and then checked as part of its base. Exactly one overlay MAY
apply to a base: the same name found in two places MUST be refused. Every finding MUST
be collected, and the command MUST exit non-zero if any error remains — including a
file that does not load and an argument that matches no file. The command MUST run on
the core dependencies alone (REQ-0001).
Consequence if violated: a file that fails to load is skipped, a missing checkout checks fewer files, and the run passes — the catalogue then serves fewer datasets, or more permissively, with nothing to say why.
REQ-0012 — Every exposed, non-open dataset states both row-policy layers
A dataset listed in the catalogue (expose) whose access level is neither open nor
secret MUST declare at least one row filter in each layer, person and
organization (its binds). allow_all and deny_all MUST state binds — refused
by the schema and by GovernanceRule otherwise — and MUST NOT both appear in one
layer. Every handler MUST be a built-in or named by the caller, and a built-in MUST be
declared in the layer it binds. An open dataset with an undeclared layer MUST be
reported as a note, not a failure.
Consequence if violated: the serving deployment's default — deny — refuses the dataset at query time, or, under the report-only default, serves the layer whole. Caught here, it is a line in a governance file; caught there, it is an outage or a leak.
REQ-0013 — A dataspace offer's consent declarations agree with its person layer
For a dataset offered into the dataspace (dataspace.expose, not secret): a person
filter other than allow_all/deny_all MUST come with dataspace.consent_required or
classification: pii; personal data narrowed only by organization filters MUST be
refused; a person filter MUST name its column. A person layer of deny_all admits no
row and is coherent with any declaration. Ported from ds' check_consent_coherence.
Consequence if violated: the dataspace serves a dataset on an agreement alone that the producer meant to gate on consent, or gates on a filter that binds nobody.
REQ-0014 — The owners registry is validated strictly, and every dataspace offer's ownership names resolve
Each owners file passed to the check MUST validate against owners.schema.json before
it is merged, later files shadowing earlier by id. The schema MUST define the
dataspace identity registry's keys as optional properties and keep
additionalProperties: false. Every ownership name of a dataspace offer
(dataspace.expose) MUST resolve to an owner; a dataset only listed in the catalogue is
not checked, its owners being attribution rather than dataspace parties. With
--dataspace, every owner of a dataspace offer narrowed by an organization filter MUST
have a DID.
Consequence if violated: a mistyped key or a missing owner produces a smaller registry that loads cleanly, and the missing organization surfaces later as an authorization failure with nothing to connect it to.