ADR 011: Cross-Instance Absolute Path Allowlist
Status: Accepted (May 2026) Decider: Tech Lead Date: 2026-05-03 (v0.7.0 "Quartz Maturity" / "Quarzo")
Context
The v0.7.0 documentation restructure introduced a multi-instance Docusaurus
architecture: /docs/* (User area) and /developers/* (Developer area) are
served by two separate @docusaurus/plugin-content-docs instances. This split
is a discoverability win — search, sidebar, and breadcrumbs no longer mix
user-level and engineering content — but it creates a structural friction with
the Zenzic core validator.
Each Zenzic adapter analysis pass operates on one Virtual Site Map (VSM)
at a time. Cross-plugin links (e.g. a User how-to that links to a Developer
reference) cannot be relative — Docusaurus refuses to resolve relative paths
across plugin boundaries. They must be absolute (/developers/how-to/...).
But absolute paths are exactly what Z105 ABSOLUTE_PATH was designed to
forbid: they break portability when a site is hosted in a subdirectory, and
they are environment-dependent. The validator, lacking knowledge of the
sibling plugin's VSM, sees a legitimate cross-plugin link as a broken absolute
reference.
The options examined were:
- Option A — Implement a manual allowlist in core configuration.
- Option B — Force the use of JSX components (e.g.
<Link>), binding the source to the build engine (violates Pillar 1: Lint the Source). - Option C — Auto-detect cross-instance routes via multiple scans (computationally expensive, risks Pillar 2: Zero Subprocesses).
Decision
We adopt Option A: a [link_validation] absolute_path_allowlist key in
zenzic.toml. The validator honors listed prefixes as Trusted Ghost
Routes — absolute paths whose targets are project-internal but live outside
the current VSM.
# zenzic.toml — declarative cross-plugin contract
[link_validation]
absolute_path_allowlist = ["/developers/", "/api/"]
The check runs immediately before the Z105 emission in validator.py: if the
parsed path starts with any allowlisted prefix, the link is treated as valid
and skipped — no other resolution is attempted, no error is recorded.
Rationale
This decision is governed by the Transparency Invariant:
- Explicit Declaration. Instead of silencing errors with inline
noqacomments scattered through Markdown, the architect declares — once, in config — which absolute prefixes the project owns. The configuration is the cross-instance map. - Linter Integrity. Zenzic still does its job: an absolute link that is neither on disk nor in the allowlist still fails the push. The allowlist narrows the scope of trust; it does not weaken Z105 itself.
- Engine Agnosticism. Markdown source remains agnostic of Docusaurus,
MkDocs, or any future multi-instance engine. No
<Link>import, no JSX prelude — the same.mdxfile would work in a single-instance migration.
Option B was rejected because JSX imports bleed engine-specific syntax into content (Pillar 1 violation). Option C was rejected because it would require either subprocess delegation to the build tool (Pillar 2 violation) or duplicating Docusaurus's plugin-resolution logic in Python (maintenance burden, parity drift).
Invariants
These constraints are permanent consequences of ADR-0011:
- Allowlist entries must start with
/. Relative entries are nonsense (relative paths never trigger Z105) and would silently broaden the bypass. - Match semantics are
startswithonly. No globbing, no regex, no wildcards. The semantics must remain inspectable at a glance. - The check runs before Z105 emission, not after. Allowlisted links must
never appear in the findings stream — not even as suppressed
info— because they represent intentional architectural contracts, not silenced problems. - Allowlist entries are not validated for existence. Z108
STALE_ALLOWLIST_ENTRY(config hygiene) is intentionally deferred to v0.8.0 to preserve Pillar 3: Pure Functions (no aggregate cross-worker state). See Technical Debt Ledger.
Consequences
Pros
- Pillar 1 preserved. Markdown source stays engine-agnostic.
- Pillar 2 preserved. Validation remains deterministic, no extra processes or network scans.
- Audit Trail. The
zenzic.tomlbecomes a documented map of inter- instance dependencies — readable by humans, parseable by tools, versioned in git. - Reversible. Removing an entry restores Z105 enforcement on that prefix; the architect can always re-tighten the perimeter.
Cons
- Manual Maintenance. If a satellite route changes (e.g.
/developers/→/dev/), the allowlist in the core repo must be updated by hand. The validator cannot detect a renamed route through the allowlist alone. - Scope Discipline Required. A reckless allowlist (
["/"]) would silently disable Z105 entirely. Code review ofzenzic.tomlchanges is the protection.
Transparency Analysis
The allowlist transforms a potential blind spot into a conscious choice. Zenzic's stance is unambiguous: we prefer the developer to write
"Zenzic, I know
/developers/is not in this VSM — trust me."
over hiding the same fact behind an inline suppression comment that degrades the global Quality Score without explaining the system topology. The first form is documentation; the second is technical debt disguised as silence.
This ADR establishes the precedent for how Zenzic will handle expansion toward micro-site architectures: every cross-boundary trust must be declared, named, and reviewable.
Suppression vs Configuration
Zenzic offers two distinct primitives for telling the linter "this is intentional." They are orthogonal and must not be conflated:
| Primitive | Scope | Use when |
|---|---|---|
[link_validation] absolute_path_allowlist | Project-wide structural contract | The fact is a systemic truth of the architecture (e.g. multi-instance routing, satellite domain prefix). |
<!-- zenzic:ignore Zxxx --> / {/* zenzic:ignore Zxxx */} | One source line | The rule is correct in general; this specific occurrence is a documented, local exception (e.g. a code sample that looks like a credential). |
The allowlist is a contract: it changes Z105's domain of validity by declaring premises about the project's URL space. The validator still evaluates the link — the evaluation simply has different inputs.
The inline ignore is surgery: it suppresses an emitted finding on a single line, leaves an audit comment in the source, and is reviewed at the diff level.
Anti-pattern (forbidden in v0.7.0+). Cross-plugin links must never
be handled with <!-- zenzic:ignore Z105 -->. Doing so would tacitly
admit that the routing is "broken and accepted"; in fact the routing is
correct by design, and that correctness deserves promotion to the
project's structural configuration. Inline suppression of cross-plugin
links also fragments the truth: a future contributor reading
zenzic.toml would see no record of the cross-boundary dependency.
Decision rule. If the same suppression would be needed in two or
more files, it is no longer a local exception — it is a systemic truth
and belongs in zenzic.toml. Promote it.
Related
- ADR 002: Zero Subprocesses Policy — forbids the auto-detection alternative (Option C).
- ADR 001: Lint the Source — forbids the JSX alternative (Option B).
- Technical Debt Ledger — records the Z108 deferral (Pillar 3 preservation).