Skip to content

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.

  • expose MUST be tri-state. Unset MUST fall back to dataspace.expose, so a file written before the split keeps its catalogue behaviour unchanged.
  • dataspace.expose MUST NOT fall back in either direction.
  • expose: false with dataspace.expose: true MUST 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: false MUST 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 fnmatch applied in both directions, so either the dependency or the sources key may be the glob.
  • An entry resolving to no producer MUST be reported, unless it declares external: true. An entry declaring external: true that 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_on MUST remain distinct from depends_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: false MUST 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.

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.