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
42 changes: 41 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,14 @@ jobs:
# clause (fictional acme/shop url; never a real PR — the
# synthetic-only fixture invariant).
config: tests/fixtures/config-pr-showcase.toml
# cute-dbt#386 — the showcase also emits the machine-readable
# findings envelope sidecar and byte-identity-gates it. The
# `--generated-at` override pins a far-future date so the golden
# is wall-clock-independent (golden-determinism rule). The
# envelope wraps the SAME in-scope findings the HTML report
# surfaces, so this row proves the sidecar shape stays stable.
findings_out: examples/diff-showcase-findings.json
generated_at: "2099-01-01"
# cute-dbt#265 Slice D — the GOLDEN macro-lens cap showcase. A
# synthetic heavy macro (`mask_pii`) reaches 14 root-project
# models (> the default inline-body cap of 10), so the committed
Expand Down Expand Up @@ -738,6 +746,11 @@ jobs:
# cute-dbt#346: optional --config (the diff-showcase row's
# synthetic [pr] section); empty = no --config arg.
CONFIG: ${{ matrix.config }}
# cute-dbt#386: optional findings-envelope sidecar golden (the
# diff-showcase row); empty = no --findings-out arg. GENERATED_AT
# pins a far-future date so the golden is wall-clock-independent.
FINDINGS_OUT: ${{ matrix.findings_out }}
GENERATED_AT: ${{ matrix.generated_at }}
run: |
set -euo pipefail
mkdir -p target/example-check
Expand Down Expand Up @@ -824,19 +837,46 @@ jobs:
config_args=(--config "$CONFIG")
config_hint=" --config $CONFIG"
fi
# cute-dbt#386: the optional findings-envelope sidecar (the
# diff-showcase row). Rendered into a scratch path and
# byte-diffed against the committed golden, the same gate shape
# as the HTML. --generated-at pins the envelope timestamp so the
# golden is wall-clock-independent.
findings_args=()
findings_hint=""
rendered_findings=""
if [ -n "$FINDINGS_OUT" ]; then
rendered_findings="target/example-check/$(basename "$FINDINGS_OUT")"
findings_args=(--findings-out "$rendered_findings" --generated-at "$GENERATED_AT")
findings_hint=" --findings-out $FINDINGS_OUT --generated-at $GENERATED_AT"
fi
cargo run --quiet --locked --bin cute-dbt -- report \
--manifest "$CURRENT_MANIFEST" \
"${scope_args[@]}" \
"${project_root_args[@]}" \
"${config_args[@]}" \
"${findings_args[@]}" \
--out "$rendered"
if ! diff -q "$rendered" "$OUTPUT_PATH" >/dev/null; then
echo "::error::example-report-up-to-date: $OUTPUT_PATH does not match the renderer output ($EXAMPLE_NAME). Regenerate with:"
echo " ${regen_env}cargo run --bin cute-dbt -- report --manifest $CURRENT_MANIFEST$scope_hint$regen_hint$config_hint --out $OUTPUT_PATH"
echo " ${regen_env}cargo run --bin cute-dbt -- report --manifest $CURRENT_MANIFEST$scope_hint$regen_hint$config_hint$findings_hint --out $OUTPUT_PATH"
echo "Sizes:"
ls -l "$rendered" "$OUTPUT_PATH"
exit 1
fi
# cute-dbt#386: byte-identity-gate the findings-envelope golden
# exactly like the HTML (same row, additive). The envelope is
# deterministic over (fixtures, pinned --generated-at), so
# byte-equality is the correct gate.
if [ -n "$FINDINGS_OUT" ]; then
if ! diff -q "$rendered_findings" "$FINDINGS_OUT" >/dev/null; then
echo "::error::example-report-up-to-date: $FINDINGS_OUT does not match the renderer output ($EXAMPLE_NAME findings envelope). Regenerate with:"
echo " ${regen_env}cargo run --bin cute-dbt -- report --manifest $CURRENT_MANIFEST$scope_hint$regen_hint$config_hint$findings_hint --out $OUTPUT_PATH"
echo "Sizes:"
ls -l "$rendered_findings" "$FINDINGS_OUT"
exit 1
fi
fi

example-report-up-to-date:
# Stable aggregator. Presents a single check name to branch
Expand Down
18 changes: 13 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,12 @@ Enforced by module convention + clippy + review (a single crate cannot fail
to compile on an inward `use`). The full layering invariant, the two-stage
fail-closed contract, the StateComparator strategy, and the conscious
design simplifications (no workspace, no per-crate versioning, no API shim,
no AST-purity grep, no JSON envelope) are recorded in [`ARCHITECTURE.md`](ARCHITECTURE.md).
no AST-purity grep) are recorded in [`ARCHITECTURE.md`](ARCHITECTURE.md).
The sixth original simplification — **no JSON wire envelope** — was
**consciously reversed** at cute-dbt#386 / epic #261 (founder ADR-4
amendment, 2026-06-11): a machine-readable findings-envelope **sidecar**
now lands via `--findings-out` (additive — the HTML output is unchanged).
See [`ARCHITECTURE.md`](ARCHITECTURE.md) §2 (row 5 + the row-5-reversal note).

```
domain -> (no outward imports; std + serde derive only)
Expand All @@ -86,10 +91,13 @@ cli/ -> clap derive, ExitCode mapping, run loop composition
main.rs -> thin entry
```

**Never import inward.** The five "conscious design simplifications" (no
workspace, no per-crate versioning, no API shim, no AST-purity grep, no
JSON envelope) are documented absences, not omissions. Adding any of them
is a regression, not a "pattern completion."
**Never import inward.** The "conscious design simplifications" (no
workspace, no per-crate versioning, no API shim, no AST-purity grep) are
documented absences, not omissions. Adding any of them is a regression, not
a "pattern completion." The one-time exception — the **JSON wire envelope**
— was sanctioned-reversed at cute-dbt#386 (findings-envelope sidecar); that
reversal is authorized by the founder's ADR-4 amendment + the #261 locked
decisions, not a contributor "completing the pattern."
The `non-mirror-guard` CI job rejects:
- a `[workspace]` table in `Cargo.toml`
- `bans.deny.wrappers` in `deny.toml`
Expand Down
80 changes: 70 additions & 10 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,34 +77,94 @@ output and exactly one parser in the dependency graph. Several pieces of
common Rust apparatus exist for projects whose shape cute-dbt does not have
— multi-crate workspaces for crates with multiple linkage-level consumers,
public-API shims for library consumers, AST-purity bans for shared cores
with rival adapter parsers, JSON wire envelopes for machine-readable output.
cute-dbt deliberately does **not** adopt them. The absences are documented
architectural choices, not accidents — recording them stops a future
contributor (human or agent) from "completing the pattern" by adding
machinery that guards no invariant here.
with rival adapter parsers. cute-dbt deliberately does **not** adopt them.
The absences are documented architectural choices, not accidents —
recording them stops a future contributor (human or agent) from "completing
the pattern" by adding machinery that guards no invariant here. (One former
member of this list — a JSON wire envelope for machine-readable output —
was **consciously reversed** at cute-dbt#386; it is now present as the
findings-envelope sidecar. See row 5 and the row-5-reversal note below.)

| # | Apparatus | cute-dbt | Why N/A | Enforcement |
|---|---|---|---|---|
| 1 | Multi-crate Cargo workspace + per-crate `Cargo.toml` | Single crate `cute-dbt` (lib + bin) | No second linkage-level consumer in the v0.x horizon. A workspace exists to serve >1 crate; importing the apparatus here would be a project-value violation (R7: "not overly complex"). | **CI:** `non-mirror-guard` job rejects a `[workspace]` table in `Cargo.toml`. |
| 2 | Per-crate independent versioning | Single artifact version | Moot — one crate, one version. The release cadence is whole-product, not per-component. | Absence (no second crate to version independently). |
| 3 | `public-api-shim` re-export pattern (`pub use crate::…::…` from `lib.rs` curating a stable surface for library consumers) | None | The binary is the product; there are no library consumers to shield from internal renames. An API shim with no consumer would add indirection that guards nothing. | **CI:** `non-mirror-guard` job rejects `pub use crate::…::…` in `src/lib.rs`. |
| 4 | AST-purity `cargo-deny` bans + `ast-purity` CI grep (keep adapter AST libraries out of a shared core) | None | The AST-purity invariant exists to protect a *shared* core crate from adapter parser dependencies when several adapter crates each pull in different AST libraries. cute-dbt has exactly one parser (`sqlparser-rs`) and one consumer of it; there is no shared core and no rival AST surface — no invariant to enforce. (Bonus: `bans.deny.wrappers` is fragile under proc-macro dependency chains; this project never needs to depend on that mechanism.) | **CI:** `non-mirror-guard` job rejects `bans.deny.wrappers` in `deny.toml`. |
| 5 | `nested-json-envelope` ADR for wire output | None | Output is HTML-primary, single self-contained file. There is no JSON wire envelope to version. `--format json` is explicitly deferred to v0.2+; if it lands it will be a new ADR, not a retroactive shim. | Absence (no JSON output in v0.1). |
| 5 | `nested-json-envelope` ADR for wire output | **Reversed — now present** (the findings-envelope **sidecar**, cute-dbt#386 / epic #261) | Originally absent: output was HTML-primary, single self-contained file, with no JSON wire envelope to version. The conscious simplification has been **deliberately reversed** under the founder's ADR-4 amendment (2026-06-11) + the #261 locked decisions — see the note below the table. The reversal stays minimal: the envelope is an **additive `--findings-out <path.json>` sidecar** (mirroring dbt's manifest/run_results sidecars), **never a `--format json` swap**, so the HTML output and its byte-identity goldens are untouched. | New-output-path ADR (#261), not a retroactive shim. The envelope is byte-identity-gated like the HTML goldens (`examples/diff-showcase-findings.json` in `example-report-check`). |
| 6 | `proc-macro2 span-locations` toolchain gate | None | No direct `proc-macro2` dependency. The `sqlparser` tokenizer carries its own spans, and the tokenizer pass itself (for `-- @desc` per-CTE breakdowns) is v0.2-deferred. There is no proc-macro span-precision invariant in scope to gate. | Absence (no `proc-macro2` direct dep). |

**Enforcement layering.** Three of the six rows have a literal CI grep
backing them — rows 1, 3, and 4 are guarded by the
[`non-mirror-guard`](.github/workflows/ci.yml) job, which rejects the
specific tripwires (`[workspace]`, `pub use crate::…::…`,
`bans.deny.wrappers`) that would silently reintroduce the apparatus. Rows
2, 5, and 6 are enforced by **absence** — there is no second crate to
version, no JSON output path, and no `proc-macro2` direct dependency, so
the apparatus cannot be added incidentally; adding any of them would
require a discrete code change visible at review.
2 and 6 are enforced by **absence** — there is no second crate to
version and no `proc-macro2` direct dependency, so the apparatus cannot be
added incidentally; adding either would require a discrete code change
visible at review. Row 5 was absence-enforced until cute-dbt#386 reversed
it; the envelope's own shape is now pinned by a **byte-identity golden**
(`examples/diff-showcase-findings.json`), the same gate class as the HTML
examples.

This is deliberate. The strongest tripwires get CI; the absence-enforced
ones get this section.

### Row-5 reversal — the findings-envelope sidecar (cute-dbt#386, epic #261)

Row 5's "no JSON wire envelope" simplification was **consciously
reversed** — a sanctioned reversal, not a "pattern completion." The
authority is the founder's **ADR-4 amendment (2026-06-11)** plus the four
**decisions locked** in the [#261 Wave-1 shaping
session](https://github.com/breezy-bays-labs/cute-dbt/issues/261). The
canonical decision record is the ops-repo ADR
(`decisions/cute-dbt/`); this section is the public-repo narrative.

The reframe that made the reversal cheap: the `Finding` POD already derives
`Serialize` and already emits the full vocabulary (`check` / `tier` /
`verdict` / `evidence` / `recommendation` / `degraded` / `suppressed`), so
this is not "design a findings schema" — it is "wrap the already-serializing
PODs in a versioned header, emit them as a sidecar, and make four policy
commitments." The four locked decisions:

1. **Delivery = sidecar.** A new `--findings-out <path.json>` flag emits the
envelope **alongside** the HTML report in one run — additive, **not** a
`--format json` swap. The HTML output is byte-for-byte unchanged (the
HTML goldens stay green); CI gets the human report *and* the machine
envelope from one invocation.
2. **check-id stability: v0.x churns, freeze at v1.0.** Individual
check-ids are **unstable** in v0.x and only freeze at v1.0. The envelope
carries this loudly and machine-readably: `metadata.id_stability:
"unstable-v0.x"`. **Consumers pin `metadata.schema_version` (an integer,
starting at `1`) — the only stability anchor pre-v1.0 — never an
individual check-id, and never a gate config hard-keyed to specific ids,
until v1.0.**
3. **Gate = Total-tier `Uncovered` only, not configurable.**
`--fail-on-uncovered` exits a dedicated non-zero code (distinct from the
usage-error and fail-closed codes) iff ≥1 `Tier::Total` +
`Verdict::Uncovered` finding is in the in-scope set. `Total` checks are
zero-false-positive by construction, so the gate never trips on a
heuristic guess. No tier knob — Total-only is the design tenet.
4. **OpenLineage: design-compatible, deferred.** The envelope fields are
shaped so a future OpenLineage-facet projection is a clean mapping, but
**no** OpenLineage output is emitted in this slice.

Severity is the existing `Tier` enum (no new field). SARIF is a later,
lossy projection — never canonical, not here. Owner fields (soft-dep #256)
reserve their slot and are populated later, additively.

The envelope POD + the gate predicate live in `src/domain/findings_envelope.rs`
(pure: `std` + serde derive only); the findings-collection + emit-to-file
live in the `src/adapters/findings_emit.rs` adapter (it parses each in-scope
model's CTE graph and runs the **same** `model_findings → apply_check_policy`
pipeline the renderer runs, so the envelope's findings match the report's
exactly). `generated_at` is computed at the CLI I/O boundary and threaded in
as a parameter (the golden-determinism rule — std-only, no `chrono`/`time`),
so the committed envelope golden is byte-stable. The envelope is emitted as
an **RFC3339 date** (`YYYY-MM-DD`): a finer-grained timestamp would need a
date-time formatting crate cute-dbt's std-only posture forbids, and the date
is the deterministic granularity the golden gate needs.

## 3. Two-stage fail-closed contract

Fail-closed inputs (a `dbt parse`-only manifest, a pre-1.8 manifest, an
Expand Down
18 changes: 14 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,10 +145,20 @@ is **strict**:
mapping, and the run-loop composition. Application orchestration lives
here (single-crate composition choice — see ARCHITECTURE).

The five **conscious design simplifications** (no workspace, no per-crate
versioning, no public-API shim, no AST-purity grep, no JSON envelope ADR)
are documented in `ARCHITECTURE.md` and enforced by the `non-mirror-guard`
CI job. Adding one is a regression, not a "pattern completion."
The four **conscious design simplifications** (no workspace, no per-crate
versioning, no public-API shim, no AST-purity grep) are documented in
`ARCHITECTURE.md`. The `non-mirror-guard` CI job enforces three of them by
rejecting their tripwires (`[workspace]`, `pub use crate::…::…`,
`bans.deny.wrappers`); per-crate versioning is enforced by absence. Adding
one is a regression, not a "pattern completion."

A sixth original simplification — **no JSON wire envelope** — was
**consciously reversed** at cute-dbt#386 (founder ADR-4 amendment): the
machine-readable findings-envelope sidecar (`--findings-out`) now lands. Its
shape is pinned not by absence but by a **byte-identity golden**
(`examples/diff-showcase-findings.json`, gated in `example-report-check`),
the same gate class as the HTML examples. See `ARCHITECTURE.md` §2 (row 5 +
the row-5-reversal note).

## Exclusions and tracking-issue rule

Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,8 +258,10 @@ reach `compiled_code`.
Single-crate Rust CLI, hexagonal **inward-dependency discipline**.
`src/{domain, ports, adapters, cli}` + `main.rs`. The full architecture
invariants, the two-stage fail-closed contract, and the conscious design
simplifications (no workspace, no public-API shim, no JSON envelope) are in
[`ARCHITECTURE.md`](ARCHITECTURE.md).
simplifications (no workspace, no public-API shim) are in
[`ARCHITECTURE.md`](ARCHITECTURE.md) — including the one that was
consciously reversed at cute-dbt#386 (the machine-readable findings-envelope
sidecar, the former "no JSON envelope" line).

## Documentation

Expand Down
Loading
Loading