Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

91 changes: 91 additions & 0 deletions spec/assets/adr/0001-validate-lazy-init-module-bootstrap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
---
id: ADR-0001
title: "validate lazy-installs default modules via quoin (no-network exception)"
type: ADR
---

# ADR 0001: `validate` lazy-installs default modules via quoin

**Status**: decided (v1)
**Date**: 2026-06-19
**Decision authority**: quire-cli maintainer

## Context

`quire validate` in scoped mode (no `--module`) resolves its archetype
registry from a set of module search roots. Historically those roots were the
`--scope` directory, `--scope/.ix/modules`, and `IX_SCHEMA_PATH`. The canonical
location where `quoin` materializes the default module set —
`~/.ix/filament/modules` (also the default root `quire-rs`
`loader::paths::default_module_root()` reads) — was **not** searched. As a
result, scoped validation on an otherwise correctly-provisioned machine failed
with "no modules found" unless `IX_SCHEMA_PATH` was set by hand.

Two fixes were considered to make scoped validation work out of the box:

1. **Discovery fallback** — add the default install root (and the preferred
`IX_FILAMENT_MODULES_PATH` env var) to the scoped search set. Pure
filesystem; no behavioural risk.
2. **Lazy-init** — additionally, when discovery finds *zero* modules, bootstrap
the default set before failing, so a fresh machine self-heals.

Lazy-init is the better UX, but it collides with
[NFR-004](../../non-functional/NFR-004-no-network.md): quire-cli is a static,
no-network CI gate, and `NFR-004` forbids opening any network socket "at any
point during any subcommand's execution," enforced by `IT-008`
(`strace -fe network`, which follows child processes) and `AUDIT-003`
(`cargo deny` bans HTTP-client crates). The default module set is installed by
**git-cloning public GitHub repos** — unavoidably network I/O.

## Decision

**Adopt both fixes, and bootstrap by delegating to `quoin` as a child process.**

- The discovery roots gain the default install root and
`IX_FILAMENT_MODULES_PATH` (preferred) alongside the legacy `IX_SCHEMA_PATH`.
This is pure filesystem work and remains fully within `NFR-004`.
- When scoped discovery yields zero modules, `validate` shells out **once** to
`quoin plugin ensure-defaults`, then reloads the registry one time. `quoin`
owns the default-module manifest (`default-modules.yaml`), the pinned tags,
and the `ts-plugin-kit` reconcile/clone logic — quire neither links a network
crate nor implements any clone logic itself.
- The network access therefore happens **only** in the `quoin` child, only on
the empty-discovery path, and only when `quoin` is present on `PATH`. If
`quoin` is absent or fails, `validate` falls through to an actionable error.

`NFR-004` is **amended** (CR note, 2026-06-19) to scope its guarantee precisely:
*quire-cli's own process* opens no network socket during any subcommand; the
scoped `validate` lazy-init MAY spawn `quoin`, whose network I/O for module
bootstrap is the single documented exception. `IT-008` keeps `strace -f` and
continues to assert zero sockets on the happy path (modules already present —
including the `--module` and populated-search-root cases); the empty-discovery
lazy-init path is explicitly out of its happy-path scope and verified by
demonstration plus the quoin-absent error test (`IT-082`).

## Consequences

- **Positive**: scoped validation self-heals on a fresh machine with no env
vars; in the normal `quoin`-first workflow (e.g. the `specify` skill runs
`quoin write` before `quire validate`) modules are already present and quire
never spawns anything. The static-binary / no-HTTP-crate posture
(`AUDIT-003`, `NFR-004-AC-1/AC-3`) is untouched — quire links no network
crate.
- **Negative**: `validate`'s worst-case behaviour now depends on an external
binary (`quoin`) and may, on the empty-discovery path only, cause network
I/O via that child. This is a deliberate, documented weakening of the
absolute "no socket ever" reading of `NFR-004`, accepted for the bootstrap
UX. Sandboxed/offline callers that must guarantee zero network can keep
modules pre-installed (the happy path never networks) — the lazy-init is a
fallback, not the primary path.

## Alternatives rejected

- **Discovery fallback only** (no lazy-init): preserves `NFR-004` verbatim but
a truly standalone `quire validate` on a bare machine still fails until the
user runs `quoin`. Rejected in favour of self-heal.
- **Opt-in flag** (`--install-missing`): keeps the default no-network, but adds
surface area and a footgun (silent no-install by default). Rejected as the
less ergonomic default.
- **Reimplement the clone in quire** (link a git/HTTP crate): directly violates
`AUDIT-003` / `NFR-004-AC-1` and duplicates `quoin`'s manifest + reconcile
logic. Rejected outright.
10 changes: 10 additions & 0 deletions spec/assets/adr/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
type: index
title: "Adr"
description: "Index of artifacts in this directory."
---
# Adr

## Contents

* [0001 Validate Lazy-Init Module Bootstrap](./0001-validate-lazy-init-module-bootstrap.md)
29 changes: 27 additions & 2 deletions spec/functional/FR-004-validate-subcommand.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,21 @@ relationships:
> printed). The CLI remains a thin wrapper ([StR-004](../stakeholder/StR-004-thin-boundary-over-quire-rs.md)) — all validation/composition
> logic lives in quire-rs.

> **CR note (scoped discovery roots + lazy-init, 2026-06-19):** Scoped
> validation now also searches the canonical install root
> `~/.ix/filament/modules` (the same default `quire-rs`
> `loader::paths::default_module_root()` reads, where `quoin` materializes the
> default module set) and the `IX_FILAMENT_MODULES_PATH` env var (preferred;
> `IX_SCHEMA_PATH` retained as the legacy alias). These additions are pure
> filesystem discovery — no network. Additionally, when scoped discovery finds
> **zero** modules, `validate` lazy-installs the default set by shelling out
> once to `quoin plugin ensure-defaults` and reloading the registry one time
> before failing; if `quoin` is absent or fails, it falls through to an
> actionable error. The lazy-install delegates all network I/O to the `quoin`
> child — quire links no network crate — and is the documented exception to
> [NFR-004](../non-functional/NFR-004-no-network.md). See
> [ADR-0001](../assets/adr/0001-validate-lazy-init-module-bootstrap.md).

## Description

The CLI SHALL expose a single-mode (markdown-only) `validate` subcommand that
Expand All @@ -85,8 +100,16 @@ When the positional argument is a document path, glob, or `-`:

Scoped validation resolves relative document globs under `--scope`. If `--scope`
itself contains `manifest.yaml`, it is loaded as one exact module; otherwise
Quire loads module search roots from the scope, `--scope/.ix/modules`, and
`IX_SCHEMA_PATH`. `--module` remains the exact single-module compatibility path.
Quire loads module search roots from the scope, `--scope/.ix/modules`, the
`IX_FILAMENT_MODULES_PATH` / `IX_SCHEMA_PATH` env vars, and the default install
root `~/.ix/filament/modules`. If that search yields **zero** modules, `validate`
lazy-installs the default module set by shelling out once to
`quoin plugin ensure-defaults` and reloading the registry a single time; only
this child performs network I/O (the [NFR-004](../non-functional/NFR-004-no-network.md)
exception, [ADR-0001](../assets/adr/0001-validate-lazy-init-module-bootstrap.md)).
When the set is still empty (e.g. `quoin` not installed), it exits 1 with an
actionable diagnostic. `--module` remains the exact single-module compatibility
path and never triggers discovery or lazy-init.

**Archetype-resolution failure paths** (all exit 1, structured diagnostic on
stderr, no stdout):
Expand All @@ -112,6 +135,8 @@ stderr, no stdout):
| FR-004-AC-10 | A document that is otherwise conformant but declares a frontmatter `object:` the registry cannot resolve produces a quire-rs **warning**. Without `--strict`, `validate` exits **0** and prints the warning to stderr, clearly marked (`warning:` prefix in human format) and distinct from any error; stdout stays empty | Test |
| FR-004-AC-11 | With `--strict`, the same unknown-`object:` warning becomes exit-failing: `validate` exits **1**, the warning still appears on stderr; stdout stays empty. A document with NO warnings and no errors still exits 0 under `--strict` | Test |
| FR-004-AC-12 | Under `--diagnostics-format json`, a warning is emitted as a distinct JSON object carrying a `severity`/`kind` field marking it a warning (not an error), so machine consumers can tell warnings from errors. An error retains its error `kind` | Test |
| FR-004-AC-13 | Scoped validation discovers modules from the default install root `~/.ix/filament/modules` and from `IX_FILAMENT_MODULES_PATH` (in addition to `--scope`, `--scope/.ix/modules`, and the legacy `IX_SCHEMA_PATH`) with no env var required and no network: a document validates against a module provided only via the default-root/env discovery path, and the run opens no inet socket | Test |
| FR-004-AC-14 | When scoped discovery finds zero modules and `quoin` is not available on PATH, `validate` exits 1 with an actionable diagnostic naming `quoin plugin ensure-defaults` (and `IX_FILAMENT_MODULES_PATH`); empty stdout. When `quoin` IS available, the empty-discovery path shells out to `quoin plugin ensure-defaults` once and reloads before validating (the [NFR-004](../non-functional/NFR-004-no-network.md) network exception, [ADR-0001](../assets/adr/0001-validate-lazy-init-module-bootstrap.md)) | Test (quoin-absent path) + Demonstration (lazy install) |

## Dependencies

Expand Down
1 change: 1 addition & 0 deletions spec/log.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ description: "Chronological log of structural changes to this bundle."
* **2026-06-15** — Adopted OKF-compatible bundle structure with directory indexes.
* **2026-06-16** — Added [FR-014](./functional/FR-014-validate-okf-bundle.md) (`quire validate --okf` permissive OKF bundle posture: `type` required, unknown-type/broken-link/index-incompleteness warn). Added [FR-003-AC-5](./functional/FR-003-extract-subcommand.md) (extract emits shared `[frontmatter]` untyped-document diagnostic). Backsynced the `artifact_type` → `type` discriminator rename across [FR-003](./functional/FR-003-extract-subcommand.md)/004/007/013 and spec.md via CR notes. Mapped IT-069..072 (`tests/cli_okf.rs`) + IT-026 reuse in tests.md.
* **2026-06-17** — Added [FR-015](./functional/FR-015-fix-subcommand.md) (`quire fix` subcommand, ADR 0007): surfaces quire-rs unlinked-reference suggestions (FR-039) and, with `--write`, applies the auto-fixable ones via byte-exact writeback. Dry-run lists `would-fix`/`warning` and exits 1 when auto-fixes remain (CI gate); `--write` is idempotent; warn-only (unresolved/ambiguous) tokens are never written. Mapped IT-076..080 + AUDIT-002 (thin boundary) in tests.md.
* **2026-06-19** — [FR-004](./functional/FR-004-validate-subcommand.md) scoped discovery now also searches the default install root `~/.ix/filament/modules` and `IX_FILAMENT_MODULES_PATH` (preferred over the legacy `IX_SCHEMA_PATH`); on zero discovered modules it lazy-installs the default set via `quoin plugin ensure-defaults` and reloads once (FR-004-AC-13/AC-14). Added [ADR-0001](./assets/adr/0001-validate-lazy-init-module-bootstrap.md) and amended [NFR-004](./non-functional/NFR-004-no-network.md) via CR note: the no-network guarantee is scoped to quire's own process; the lazy-init's `quoin` child is the sole documented network exception. Mapped IT-081 (scoped discovery network-free) + IT-082 (quoin-absent actionable error) in tests.md.
19 changes: 17 additions & 2 deletions spec/non-functional/NFR-004-no-network.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,25 @@ relationships:
cardinality: "1:1"
---

> **CR note (lazy-init delegation exception, 2026-06-19):** This NFR is scoped
> to *quire-cli's own process*. quire links no network client crate and opens no
> socket in its own process. The one documented exception is
> [FR-004](../functional/FR-004-validate-subcommand.md) scoped `validate`: when
> module discovery finds zero modules it MAY spawn `quoin plugin ensure-defaults`
> as a **child process**, whose network I/O bootstraps the default module set.
> The clone happens entirely in `quoin`, so the static-binary / no-HTTP-crate
> guarantee (AC-1, AC-3) is unchanged; only the absolute "no socket in any
> descendant" reading of AC-2 is relaxed for that one bootstrap path. See
> [ADR-0001](../assets/adr/0001-validate-lazy-init-module-bootstrap.md).

## Statement

The CLI SHALL NOT depend on any HTTP, gRPC, or other network client crate, and
SHALL NOT open any network socket at any point during any subcommand's execution.
SHALL NOT open any network socket **in its own process** at any point during any
subcommand's execution. The sole exception is the
[FR-004](../functional/FR-004-validate-subcommand.md) scoped-`validate` lazy-init,
which may spawn `quoin` as a child process to bootstrap modules (network confined
to that child; [ADR-0001](../assets/adr/0001-validate-lazy-init-module-bootstrap.md)).
`quire-rs` already pins `jsonschema` with `default-features = false, features =
["resolve-file"]` to drop `reqwest`; this CLI SHALL maintain that constraint and
add no network dependencies of its own.
Expand All @@ -38,5 +53,5 @@ confirms none are present (cross-platform); on Linux, each subcommand is run und
| ID | Criteria | Verification |
|----|----------|--------------|
| NFR-004-AC-1 | `cargo deny check bans` rejects `reqwest`, `hyper`, `tonic`, `surf`, `ureq` | Inspection |
| NFR-004-AC-2 | An IT runs each subcommand under `strace -e network` (or equivalent) and asserts zero `socket()` calls | Test |
| NFR-004-AC-2 | An IT runs each subcommand under `strace -fe network` (or equivalent) on its happy path (registry present — including scoped discovery that finds modules) and asserts zero `socket()` calls in quire's own process and descendants. The scoped-`validate` empty-discovery lazy-init (which spawns `quoin`) is the documented exception ([ADR-0001](../assets/adr/0001-validate-lazy-init-module-bootstrap.md)) and is out of this happy-path scope | Test |
| NFR-004-AC-3 | `Cargo.lock` audit (CI gate) lists no HTTP client crate | Inspection |
Loading
Loading