12 Declarative engineering policies
Built-in rules cover stable product semantics, but projects often need organization- or domain-specific governance. Quarto-Needs therefore supports a deliberately bounded declarative policy layer in .quarto-needs.toml.
The policy layer is not a scripting engine. It cannot execute Python, Lua, JavaScript, shell commands, templates, or arbitrary expressions. A policy composes three semantic capabilities already owned by the core:
- a named query selects the source objects;
- the canonical relation catalog defines the required relation view;
- configured type roles optionally constrain the relation target.
12.1 A minimal policy
[policies.APPROVED_REQUIRES_TEST]
scope = "approved-requirements"
assert-relation = "verified-by"
target-role = "verification"
minimum = 1
severity = "error"This means: for every object selected by approved-requirements, require at least one verified-by relation whose target has semantic role verification.
A violation becomes an ordinary engineering finding with code:
POLICY:APPROVED_REQUIRES_TEST
It therefore participates in the same quality reports, Git diffs, PR reports, GitHub projections, and gates as built-in findings.
12.2 Grammar
Each [policies.<NAME>] table currently accepts only:
| Key | Required | Meaning |
|---|---|---|
scope |
yes | Existing safe named query, including the built-in approved-requirements query. |
assert-relation |
yes | Relation name resolved through the canonical relation catalog. |
target-role |
no | Semantic role required on matching targets. |
minimum |
no | Minimum number of matching relations; integer >= 1, default 1. |
severity |
no |
error, warning, or info; default error. |
Unknown keys are rejected. This is intentional: extending the grammar requires an explicit Quarto-Needs capability rather than silently creating an escape hatch for code execution.
12.3 Named scopes
Policies reuse the existing bounded query language instead of inventing another selector language. For example:
[queries.approved-high-nfr]
all = [
{ field = "type", op = "eq", value = "non-functional-requirement" },
{ field = "status", op = "eq", value = "approved" },
{ field = "priority", op = "in", values = ["critical", "high"] },
]
[policies.HIGH_NFR_REQUIRES_TEST]
scope = "approved-high-nfr"
assert-relation = "verified-by"
target-role = "verification"
minimum = 1
severity = "error"The query is compiled by the same safe query evaluator used by generated tables and graph views.
12.5 Configuration fingerprints
Policies are part of NeedsConfig.canonical_document(). Changing policy scope, relation, target role, minimum, or severity changes the configuration fingerprint. Consequently:
- baselines can detect policy changes;
- evidence attestations cannot silently survive a governance change;
- Git-range analysis can distinguish states governed by different policy configurations.
This is why policies are not loaded as an external side file or applied only inside CI.
12.6 Self-hosted example
examples/quarto-needs/.quarto-needs.toml dogfoods the feature with:
[policies.APPROVED_REQUIRES_TEST]
scope = "approved-requirements"
assert-relation = "verified-by"
target-role = "verification"
minimum = 1
severity = "error"The self-hosted model already requires 100% verification coverage. This declarative policy independently expresses the project-specific governance expectation, so a future approved requirement without a verification relation produces POLICY:APPROVED_REQUIRES_TEST and blocks the strict quality gate.
12.7 Where this layer ends
[policies.*] deliberately covers one question: does a scoped object hold enough of a required relation? Anything structurally richer belongs to a sibling declarative layer rather than to an expanded policy grammar:
| Need | Layer |
|---|---|
| Attribute value and shape constraints |
[types.<type>.attribute-schema] — per-type attribute schemas
|
| Required paths, forbidden cycles, connectivity, relation ceilings |
[constraints.*] — graph constraints
|
| Computed values and selected build slices |
[derived.*] and [variants.*] — derived fields and build variants
|
Each of these is bounded the same way policies are: a declarative table compiled by the core, never an expression evaluator. Extending governance means adding an explicit capability of this kind, not opening an escape hatch for code execution.