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:

  1. a named query selects the source objects;
  2. the canonical relation catalog defines the required relation view;
  3. 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.4 Direct and inverse authoring

Policies describe a logical relation view, not a particular spelling location. If a requirement is linked with:

FUN-001 --verified-by--> TC-001

or the test authors the inverse:

TC-001 --verifies--> FUN-001

then a policy asserting verified-by sees the same logical relationship. The inverse is derived from the canonical relation catalog; the policy engine does not maintain a second inverse-relation table.

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.