11  Governance and quality

11.1 Profiles

Quarto-Needs separates structural findings from semantic governance. Profiles control how quality-gate failures affect automation, while structural errors remain blocking.

profile = "strict"

The available profiles are default (the default), advisory, and strict. For quality and the JSON/CSV/SARIF/JUnit/Markdown exports, strict makes failed gates return an error; default and advisory keep the gate results visible without a failing gate verdict. Structural failures remain blocking in every profile.

scan and check fail on error-severity findings, but do not enforce numeric gates. A CI workflow that requires coverage thresholds should run quarto-needs quality as well. The command-line reference explains the separate exit behavior of change reports.

11.2 Rules

Rules are registered by code and can be enabled, disabled when allowed, and assigned a severity.

[rules.REQ011]
enabled = true

[rules.DEC001]
enabled = true
severity = "error"

11.2.1 How a rule becomes active

Not every registered rule runs in every project. A rule reaches the findings list in one of three ways:

Activation Meaning
Always Runs in every project, with or without .quarto-needs.toml.
Configured Runs as soon as the configuration it depends on exists — declaring required-attributes, id-prefix, allowed-statuses, an attribute-schema, or a [relations.*] policy activates the matching rule automatically.
Opt-in Runs only after an explicit [rules.<code>] enabled = true.

The opt-in group matters in practice: decision governance and the implementation, evidence, risk, orphan, and expiry rules stay silent until the project asks for them. A project that expects DEC001 to complain about an undriven decision must enable it.

Any active rule can be turned off with enabled = false, or re-graded with severity, except where the catalog below says otherwise.

11.2.2 Rule catalog

Code Checks Default severity Activation
REQ002 Requirement declarations should explain why they exist. warning Always
REQ004 Every engineering object needs a unique ID. error Always (structural)
REQ005 Relations must point at declared objects. error Always (structural)
REQ006 Approved requirements need a verification relation. warning Always
REQ008 Configured types may require attributes. warning Configured
REQ009 Configured relations restrict endpoint types. warning Configured
REQ010 Configured relations may constrain cardinality. warning Configured
REQ011 Approved requirements need an implementation-family relation. warning Opt-in
REQ012 Tests with status approved or passed need evidence-family relations. warning Opt-in
REQ013 High/critical risks need incoming mitigation relations. warning Opt-in
REQ014 Objects with no relations in either direction. info Opt-in
REQ015 Evidence carrying an expiry attribute must still be valid. warning Opt-in
ID001 Object IDs may be governed by a prefix per engineering type. warning Configured
OBJ001 Object status must belong to the configured lifecycle for its type. warning Configured
OBJ002 Object attributes may be constrained by a per-type JSON Schema. error Configured
DEC001 Accepted decisions should address at least one engineering driver. warning Opt-in
DEC002 Accepted decisions should identify affected architecture elements. warning Opt-in
DEC003 Accepted decisions should define how conformance is confirmed. warning Opt-in
DEC004 Superseded decisions need explicit lineage to another decision. warning Opt-in
DEC005 Supersession lineage must remain acyclic. error Opt-in
DEC006 Accepted decisions with revisit-after dates should be reviewed when due. warning Opt-in
ARC001 part-of must connect adjacent C4 layers (system → container → component → source-module). error Always

Severity may be error, warning, or info, except where the rule restricts it: REQ004 and REQ005 are structural, so they can be neither disabled nor re-graded; REQ006 accepts only error or warning; DEC005 and ARC001 accept only error. An unsupported value is a configuration error rather than a silently ignored setting.

Decision rules use the DECxxx namespace, identification and type policies use IDxxx/OBJxxx, and architecture layering uses ARCxxx. Evidence validation uses the separate EVDxxx namespace described in Executable evidence and Machine evidence providers; those codes are reported by the evidence commands, not by [rules.*].

11.3 Relations and cardinality

Policy can restrict endpoints and cardinality:

[relations."addresses"]
allowed-source-types = ["architecture-decision"]
allowed-target-types = [
  "stakeholder-need",
  "system-requirement",
  "non-functional-requirement",
  "risk",
]
minimum-per-source = 1
Setting Meaning
allowed-source-types Types permitted at the declaring end. Activates REQ009.
allowed-target-types Types permitted at the referenced end. Activates REQ009.
minimum-per-source Fewest edges of this relation each eligible source must declare. Activates REQ010.
maximum-per-source Most edges of this relation one source may declare. Activates REQ010.

Use maximum-per-source = 1 for relations that must stay single-valued — a component belonging to exactly one container, or a decision superseding exactly one predecessor:

[relations."part-of"]
allowed-source-types = ["container", "component", "source-module"]
allowed-target-types = ["system", "container", "component"]
minimum-per-source = 1
maximum-per-source = 1

Omitting a bound leaves that side unconstrained. Declaring any of these four keys activates the corresponding rule automatically; no [rules.*] entry is needed.

This turns modeling conventions into verifiable invariants.

11.4 Quality gates

[gates]
scope = "approved-requirements"
max-errors = 0
min-implementation-trace = 100.0
min-implementation-effective = 100.0
min-verification-trace = 100.0
min-verification-successful = 100.0
min-evidence = 100.0
require-risk-mitigation = true

Gates are appropriate for measurable properties of a set, while rules describe localized violations on objects or relations.

Setting Type Default Meaning
scope Named query approved-requirements The population every percentage gate measures.
max-errors Integer 0 Maximum error-severity findings allowed in the whole project.
min-implementation-trace Percent unset Share of the scope with an implementation-family relation.
min-implementation-effective Percent unset Share of the scope whose implementation endpoint is not in an ineffective status.
min-verification-trace Percent unset Share of the scope with a verification-family relation.
min-verification-successful Percent unset Share of the scope verified by a test in a successful status.
min-evidence Percent unset Share of the scope reaching unexpired evidence.
require-risk-mitigation Boolean false Requires zero REQ013 findings.

The paired -trace and -effective/-successful gates are the difference between a link existing and that link meaning something. min-verification-trace accepts a requirement that names a test case; min-verification-successful additionally demands that the test carries a successful status. Which statuses count is [governance] policy — see Practical reference.

An unset percentage gate is not evaluated. The gate scope must be catalog, approved-requirements, or a declared named query; a misspelling is a configuration error listing valid scopes. Queries are declared at load time, even when their results depend on the graph.

A percentage gate with zero eligible requirements is unmeasured and fails by default, including a zero-percent threshold. A risk gate with no high/critical risks is likewise unmeasured. Projects intentionally starting empty may set [gates] allow-empty-scopes = true: the verdict is allowed, but the CLI says [WAIVED], JUnit records a skipped check, and Markdown identifies the waiver. Without that opt-in the CLI says [UNMEASURED]. Both explain that the scope matched no eligible objects. Missing measurements and disabled dependencies cannot be waived.

11.4.1 When allow-empty-scopes is legitimate

It is legitimate when the emptiness is a true statement about a project that has not filled in yet: a repository adopting Quarto-Needs before authoring requirements, or a new component whose gates are declared up front so that they are already in place when content arrives. The claim being waived is “we know this measures nothing right now”, and it is meant to stop being true.

It is a patch when it hides a scope that should be matching and is not. The symptom looks identical — zero eligible objects — but the cause is drift: a named query whose predicate no longer matches how statuses are written, a scope pointing at the wrong query, or a risk model that was never authored while require-risk-mitigation claims risk is managed. Waiving that converts a broken configuration into a permanent green check.

Two habits keep the two apart:

  • Before waiving, confirm the emptiness by hand. quarto-needs query <scope> returns the population; if you expected objects and got none, the scope is broken, not empty.
  • Treat the waiver as temporary and review it. A waiver that has outlived several releases is describing drift, not a beginning.

Note that the waiver is project-wide rather than per gate. Enabling it to accept one empty population also accepts every other empty population in the same project, so a scope that breaks later is waived without anyone deciding to waive it. This is the main reason to remove the waiver once the project has content, rather than leaving it in place because it is harmless today.

Quality JSON v1 exposes optional measurementStatus: measured, empty, or unavailable. Unmeasured gates have actual: null; the coverage metric itself still reports 100.0 for 0/0 for compatibility. max-errors measures findings from a completed analysis: zero findings is a valid zero count. Structural analysis failure blocks every profile before population gates are evaluated.

require-risk-mitigation counts findings from rule REQ013, which is opt-in. Enabling the gate alone could only ever pass, so that pair is rejected as a configuration error rather than reporting a vacuous success. Enable both:

[rules.REQ013]
enabled = true

[gates]
require-risk-mitigation = true

11.5 Baseline, diff, and impact

A baseline captures the analyzed state and configuration fingerprint. Diff distinguishes added/removed objects, modified content, relocation, and relation changes. When policy changes, incompatible derived results are explicitly suppressed instead of producing misleading comparisons.

Impact analysis uses semantic direction declared in the catalog. ADR relations participate in this mechanism exactly like implementation, verification, and evidence relations.

See the command-line reference for baseline creation/inspection, comparisons with current sources, --recompute-with current, committed Git ranges, suspect claims, and PR/GitHub report generation.

11.6 Verified determinism

Determinism is a checked property, not a claim. tools/reproducibility.py runs the same analysis over the same sources several times and compares the resulting artifacts as raw bytes: needs.json, the generated Lua index, the baseline payload, every deterministic export (json, csv, markdown, reqif, jsonld), a materialized source index, and the three fingerprints compared as values in addition to bytes.

Each run is a fresh interpreter. This is not a stylistic choice: PYTHONHASHSEED is read once at interpreter start-up, so a test that sets it through os.environ in the current process measures nothing. Every variation therefore executes as a subprocess with an explicitly constructed environment rather than an inherited one.

The matrix varies the hash seed, the time zone, the collation locale, the working directory, the form of the --root argument (absolute, relative from inside the tree, relative from outside it), and the order in which sources are discovered. Each worker writes a probe recording what it actually observed, and the harness asserts those probes differ where the perturbation was supposed to bite — a locale that silently fell back to C fails the run instead of passing a test that never exercised collation. SOURCE_DATE_EPOCH freezes the reference date, so no field needs to be excluded from comparison; the allowlist of volatile fields lives in one module, is currently empty, and is locked by a reviewed digest so it cannot grow without a deliberate change.

The reproducibility workflow repeats the whole matrix on Ubuntu, macOS, and Windows for Python 3.10 through 3.14, then compares the artifacts of all fifteen runtimes against each other. Its aggregate job is required for merge.