Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

criv documentation

Browse the documentation, architecture decisions, and repository guides.

Architectural Decisions

Accepted decisions live in this directory as MADR-style notes named NNNN-kebab-title.md.

Index

  • ADR-0001 - Local CLI vault architecture
  • ADR-0002 - Docs and ADRs form the governance graph
  • ADR-0003 - Adopt proven foundation crates
  • ADR-0004 - Tree-sitter source graph
  • ADR-0005 - ast-grep policy search and enforcement
  • ADR-0006 - fff source index and incremental watch
  • ADR-0007 - Content-addressed state and diffing
  • ADR-0008 - Optional semantic note search
  • ADR-0009 - Obsidian plugin as state consumer
  • ADR-0010 - criv init installs agent runtime skills
  • ADR-0011 - Embed runtime skill templates as assets
  • ADR-0012 - ADR immutability enforcement
  • ADR-0013 - Mise managed hk hook toolchain
  • ADR-0014 - Tag triggered release binary workflow
  • ADR-0015 - Size optimized release profile
  • ADR-0016 - Conventional commit SemVer release automation
  • ADR-0017 - Deprecate Intel macOS release archives
  • ADR-0018 - Offline zizmor Actions security check
  • ADR-0019 - Export Usage CLI spec and help
  • ADR-0020 - Portable note wikilinks
  • ADR-0021 - Audit remediation boundaries
  • ADR-0022 - Hosted CI entry point
  • ADR-0023 - Do not track generated plugin bundles
  • ADR-0024 - Oxlint only JavaScript TypeScript enforcement
  • ADR-0025 - Init ships plugin source not generated bundle
  • ADR-0026 - Mermaid C4 diagrams as vault content
  • ADR-0027 - C4 diagram fenced block extraction scope
  • ADR-0028 - C4 standard alignment for Mermaid diagrams
  • ADR-0029 - Generated C4 Code architecture
  • ADR-0030 - DOT for generated Code architecture
  • ADR-0031 - Text first C4 architecture formats
  • ADR-0032 - .c4 files as architecture artifacts
  • ADR-0033 - Typed Wikilink source references
  • ADR-0034 - AST-aware source selectors
  • ADR-0035 - VS Code-compatible companion extension
  • ADR-0036 - VS Code extension test stack
  • ADR-0037 - VS Code JSON diagnostics in hooks
  • ADR-0038 - ADR Policy Rule Generation
  • ADR-0039 - Inline ADR Policy Rules
  • ADR-0040 - Inline Only ADR Policy Rules
  • ADR-0041 - ADR-Owned Policy Patterns
  • ADR-0042 - Shared Source Index Lifecycle
  • ADR-0043 - Hawk visibility analysis
  • ADR-0044 - Vault write confinement
  • ADR-0045 - Note line identity in generated state
  • ADR-0046 - No native linting in criv enforce
  • ADR-0047 - Semantic note search stays source only
  • ADR-0048 - 2026-07-25 audit findings not actioned
  • ADR-0049 - Checks defined in hk, not mise
  • ADR-0050 - Repeated Wikilinks are not a defect
  • ADR-0051 - Refresh generated agent skills explicitly
  • ADR-0052 - Harden generated skill refresh
  • ADR-0053 - Claude skills is a link
  • ADR-0054 - criv does not install Git hooks
  • ADR-0055 - Dependency auditing in hk checks
  • ADR-0056 - ADR policy patterns are the only registered patterns
  • ADR-0057 - Branch-local ADR publication and reconciliation
  • ADR-0058 - Embedded Git repository access
  • ADR-0059 - Accepted only ADR policy state
  • ADR-0060 - Parallel hosted validation and lean local hooks
  • ADR-0061 - Hook-owned local validation and direct CI profile
  • ADR-0062 - Canonical generated criv configuration
  • ADR-0063 - ADR reconciliation owns its commit
  • ADR-0064 - Typed query subcommands
  • ADR-0065 - Shared compiled policy scan plans
  • ADR-0066 - Capability-directed Wikilink and query resolution
  • ADR-0067 - Staged changes are a partial check scope
  • ADR-0068 - Bounded local snapshot lifecycle
  • ADR-0069 - Repeatable two tier performance evidence
  • ADR-0070 - Publish push performance evidence as Git notes
  • ADR-0071 - Make Wasm editor projections canonical
  • ADR-0072 - Keep performance observation outside core
  • ADR-0073 - Effective ADR governance and source reconciliation
  • ADR-0074 - LikeC4 as the architecture source and renderer
  • ADR-0075 - LikeC4 preview as the default C4 editor
  • ADR-0076 - Focused LikeC4 workspace navigation
  • ADR-0077 - C4 standard alignment for the LikeC4 workspace
  • ADR-0078 - The editor follows preview navigation
  • ADR-0079 - No fallback view in the C4 preview
  • ADR-0080 - Co-locate primary LikeC4 views with their models
  • ADR-0081 - Require material State store performance gains
  • ADR-0082 - No standalone search command
  • ADR-0083 - Own one loaded State revision per editor workspace
  • ADR-0084 - Require Windows hosted validation
  • ADR-0085 - Keep the optional editor viewer local
  • ADR-0086 - Bundle one editor viewer with criv
  • ADR-0087 - Keep editor setup out of init
  • ADR-0088 - Share the State wire document
  • ADR-0089 - Reconcile effective query and LikeC4 contracts
  • ADR-0090 - Enforce runtime boundary decisions
  • ADR-0091 - Enforce editor adapter boundaries

Domain Docs

How the engineering skills should consume this repo’s domain documentation when exploring the codebase. This is a single-context repo.

Before exploring, read these

  • CONTEXT.md at the repo root, if it exists.
  • docs/adr/ — read ADRs that touch the area you are about to work in. There are 47 of them and they are the primary record of how criv behaves and why.
  • AGENTS.md at the repo root — verification commands and repo conventions.

If CONTEXT.md doesn’t exist, proceed silently. Don’t flag its absence and don’t suggest creating it upfront. The /domain-modeling skill creates it lazily when terms actually get resolved.

File structure

/
├── AGENTS.md
├── docs/
│   ├── adr/                ← 47 decisions, criv-governed
│   ├── agents/             ← this configuration
│   ├── architecture/       ← generated C4 artifacts
│   └── query-reference.md
└── src/

Finding the right ADR

docs/adr/ is large enough that reading it linearly is the wrong move. Use criv:

criv query governing src/watch.rs      # which ADRs govern this file
criv query governs ADR-0007            # what an ADR controls
rg -n -i "<topic>" docs/               # search note text

ADR conventions specific to this repo

  • Accepted ADRs are immutable under ADR-0012. Never edit, delete, or rename one to change a decision. Write a new ADR with supersedes: instead.
  • Get the next number from criv query next-adr-id, never by guessing.
  • ADRs carry governs: path globs, and may carry inline policy.patterns ast-grep rules that criv check and criv enforce evaluate.
  • Everything under docs/ is a criv vault note and needs frontmatter with at least id, kind, and title, or criv check fails and blocks the commit.

Use the project’s vocabulary

When your output names a domain concept — an issue title, a refactor proposal, a hypothesis, a test name — use the term the ADRs already use. Don’t drift to synonyms.

If the concept you need isn’t named anywhere yet, that’s a signal: either you’re inventing language the project doesn’t use (reconsider) or there’s a real gap (note it for /domain-modeling).

Flag ADR conflicts

If your output contradicts an existing ADR, surface it explicitly rather than silently overriding:

Contradicts ADR-0007 (content-addressed state) — but worth reopening because…

Because accepted ADRs are immutable here, “reopening” concretely means writing a superseding ADR, not amending the old one.

Issue tracker: GitHub Issues

Issues and specs (you may know a spec as a PRD) for this repo live in GitHub Issues on TudorAndrei/criv, reached through the gh CLI.

When a skill says “publish to the issue tracker”

gh issue create --title "<title>" --body "<body>" --label "<label>"

When a skill says “fetch the relevant ticket”

gh issue view <number>
gh issue view <number> --comments
gh issue list --label ready-for-agent

The user will normally pass the issue number or URL directly.

When a skill says “comment on the ticket”

gh issue comment <number> --body "<body>"

PRs as a request surface

Off. Skills read and write issues only; incoming pull requests are not part of the triage queue. Flip this section on if that changes.

There is no second tracker

GitHub Issues is the only issue tracker for this repository. An ISSUES.md file at the repo root previously held a hand-curated audit index; it was migrated to GitHub Issues on 2026-07-30 and deleted. Do not recreate it, and do not add a parallel findings file — a second list drifts from the first.

Issues migrated from it carry a footer naming their original ISSUES.md number, because ADRs and commit messages written before the migration refer to those numbers.

Issue quality

The migrated issues set the bar for a substantial finding: a one-line statement of the defect, then Evidence with file:line citations, Impact, a Fix sketch, and Verification. Match that shape when filing something non-trivial. A finding without evidence someone else can check is not ready to file.

Closing an issue

Terminal outcomes are decisions, and decisions in this repository live in docs/adr/. See docs/agents/triage-labels.md for the rule: closing an issue as done or as wontfix requires an ADR when it settles a question about how criv behaves.

Triage Labels

The skills speak in terms of five canonical triage roles. This file maps those roles to the label strings used in this repo’s GitHub Issues.

Label in mattpocock/skillsLabel in our trackerMeaning
needs-triageneeds-triageMaintainer needs to evaluate this issue
needs-infoneeds-infoWaiting on reporter for more information
ready-for-agentready-for-agentFully specified, ready for an AFK agent
ready-for-humanready-for-humanRequires human implementation
wontfixwontfixWill not be actioned

When a skill mentions a role (e.g. “apply the AFK-ready triage label”), use the corresponding label string from this table.

Only wontfix exists in the repository today; the other four are the GitHub defaults’ gap and must be created before first use:

gh label create needs-triage --description "Maintainer needs to evaluate this issue"
gh label create needs-info --description "Waiting on reporter for more information"
gh label create ready-for-agent --description "Fully specified, ready for an AFK agent"
gh label create ready-for-human --description "Requires human implementation"

Terminal states are decisions

This repository records decisions as ADRs under docs/adr/, and the two terminal triage outcomes are decisions.

Closing as done. When the work settled a question about how criv behaves — what a command does, what it refuses to do, what a generated artifact looks like — write an ADR before closing. Pure implementation of an already-decided behavior does not need one; changing or establishing a behavior does.

Closing as wontfix. A wontfix is a decision not to do something, and the reasoning is worth more than the label. Write an ADR recording what was proposed and why it was rejected, then close with a link to it.

Both cases follow the normal ADR workflow:

  • criv query next-adr-id for the number; never guess it.
  • Required frontmatter is id, kind: decision, title, status, date.
  • Use governs: for the path globs the decision controls.
  • Accepted ADRs are immutable under ADR-0012. To change a past decision, write a new ADR with supersedes: pointing at the old one — never edit it.
  • Run criv check before finishing.

An issue closed as done or wontfix should link the ADR that settled it, so the reasoning survives the issue being closed.

Dependency evaluations

These evaluations cover foundation crates that are useful but not required for the current parser-backed implementation.

miette

Decision: defer.

miette is a good fit for future source-span diagnostics because it provides a diagnostic protocol, source snippets, labels, related diagnostics, and optional fancy reports. criv diagnostics currently carry line numbers but not byte spans or source snippets, so adopting it now would mostly add dependency weight without improving output. Revisit once check diagnostics store source offsets.

Reference: https://lib.rs/crates/miette

infer

Decision: defer until plugin asset previews are implemented.

infer detects file types from magic-number signatures and returns MIME and extension metadata. The CLI already skips binary source files with content_inspector and records cheap extension MIME hints with mime_guess. Magic-number detection becomes useful when the Obsidian plugin previews non-source assets from state.

Reference: https://lib.rs/crates/infer

serde_yaml_ng

Decision: keep serde_norway; do not add serde_yaml_ng now.

serde_yaml_ng is a serde-yaml fork and a viable alternate YAML backend, but the current implementation already replaced deprecated serde_yaml with serde_norway. Keep a single YAML parser until frontmatter compatibility tests show a concrete gap.

Reference: https://lib.rs/crates/serde_yaml_ng

camino

Decision: defer until repo-relative path APIs are refactored.

camino provides UTF-8 path types that avoid repeated lossy conversions. That matches criv’s repo-relative path invariant, but adopting it cleanly should be done as a focused path-type refactor across config, vault, state, search, and query modules.

Reference: https://docs.rs/camino

Cargo Audit Snapshot, 2026-07-23

Decision: document and monitor; do not add a failing cargo audit gate yet.

The pinned cargo-audit v0.22.2 command was run as cargo audit --no-fetch. It loaded 1,169 advisories from the local advisory database at commit 1abf7a8c1822223a38e99f652bc232071c44a86d (2026-07-23 09:15:03 +02:00) and scanned 461 locked packages. It reported four allowed warnings, all listed below. None was classified as a vulnerability by this run.

This is a dated, local posture snapshot rather than a hosted-audit baseline: --no-fetch intentionally does not update the advisory database, and the command warned that it could not open the crates.io index cache lock. A future policy gate needs a reproducible advisory-database update path before it can be relied on in CI.

Unsound APIs: git2 v0.20.4

RUSTSEC-2026-0183 reports potential undefined behavior when Remote::list() is called, and RUSTSEC-2026-0184 reports potential undefined behavior for a Signature obtained from a buffer-created BlameHunk. Both affect git2 v0.20.4, which reaches criv only through fff-search v0.10.1:

git2 v0.20.4 <- fff-search v0.10.1 <- criv

criv directly depends on git2 v0.21.0 with default features disabled for local repository discovery, tree/index/worktree diffs, commit traversal, and blob reads. It does not create transports or invoke the advisory APIs. The older git2 v0.20.4 remains an independent transitive dependency of fff-search; cargo tree -i git2@0.21.0 and cargo tree -i git2@0.20.4 distinguish the two paths.

The locally installed fff-search v0.10.1 source was inspected. Its git path uses Repository::open, status enumeration, workdir, and status_file; a source search found no invocation of Remote::list, BlameHunk, or blame APIs. That is evidence that the two advisory call paths are not reached by the currently inspected source, not proof that text search alone can rule out every runtime path or upstream behavior.

Unmaintained crate: bincode

RUSTSEC-2025-0141 marks bincode v1.3.3 unmaintained. Its active default dependency path is:

bincode v1.3.3 <- heed-types v0.21.0 <- heed v0.22.1 <- fff-search v0.10.1 <- criv

Policy conclusion

The monitor-only decision is unchanged. The current findings are two unmaintained crates and two potentially unsound-but-unreached APIs, not a new vulnerability classification or demonstrated runtime exploit path. Do not add an audit ignore list or failing gate, and do not replace fff-search, without a separate approved decision. The embedded repository backend is governed by its own ADR; accepted audit-policy ADRs remain unmodified.

ADR-0055 subsequently runs this same command as a visible, non-blocking hk monitor. It does not change this Rust decision or make cargo audit a failing gate.

Embedded Git backend measurement, 2026-08-02

git2 v0.21.0 is a direct MIT OR Apache-2.0 dependency with default features disabled. Its native libgit2-sys v0.18.7+1.9.6 dependency has the same license expression. The resolved graph deliberately contains both git2 v0.21.0 for criv’s local repository boundary and git2 v0.20.4 through fff-search; Cargo resolves both wrappers onto the same libgit2-sys version.

Same-toolchain, clean, size-optimized release builds on this macOS host measured the main binary at 12,702,432 bytes in 1:14.62 and this branch at 12,737,152 bytes in 1:59.65. The embedded backend therefore adds 34,720 bytes (0.27%). The branch build is slower by 45.03 seconds in this cold local comparison; this is recorded for release review, not treated as a release blocker.

cargo audit --no-fetch on 2026-08-02 reported the same four allowed warnings: the two git2 v0.20.4 advisory paths and the existing bincode and paste maintenance warnings. It reported no advisory for direct git2 v0.21.0.

Evidence commands:

cargo audit --no-fetch
cargo metadata --format-version 1 | jq -r '.packages[] | select(.name == "git2" or .name == "libgit2-sys") | "\(.name) \(.version): \(.license // "NOASSERTION")"'
cargo tree -i git2@0.21.0
cargo tree -i git2@0.20.4
cargo tree -i bincode@1.3.3
cargo tree --all-features --target all -e features -i paste@1.0.15

LikeC4 Integration

ADR-0074 defines the architecture contract. This document gives the implementation specification.

Source and ownership

One vault has one LikeC4 workspace at docs/architecture/. All architecture source files use the .c4 extension. LikeC4 owns the language grammar, model rules, layout, and visual output. Agents write the source. criv does not read a Mermaid C4 or DOT format and does not provide a migration command.

The workspace can contain manual models at every C4 level. When a vault enables [architecture.code], criv writes the configured Code file. A generated file contains only language modules and import relations:

  • Rust crates and mod declarations
  • TypeScript and JavaScript modules and namespaces
  • Python modules and packages
  • Go packages

The generator does not make nodes for files, classes, functions, methods, or calls. A file is only a source location for a module. Each language gets a focused named view. There is no full Code view.

This repository keeps hand-authored module models and their focused views under docs/architecture/code/. It does not enable [architecture.code]. Each view explains one component or workflow instead of copying the complete source index.

A hand-authored Code model must stay a true roll-up of the component model. If a module in component A imports a module in component B, the component model must also show a relationship from A to B. Cross-cutting helper modules, re-export barrels, and bundler shims stay outside the architecture; name them in a comment at the top of the Code model file.

The workspace has one LikeC4 project. LikeC4 merges every source file into one model. A domain file declares its elements and relationships once and also owns the primary named views that explain that domain. A large Code domain can own more than one focused view. Cross-domain runtime workflows stay in separate view files. This ownership lets an editor select the correct preview from the opened file path without copying model declarations.

docs/architecture/
  specification.c4        element kinds, tags, and global style groups
  systems.c4              people, systems, and the System Context view
  cli.c4                  CLI components and their primary view
  interactions.c4         cross-container relationships and Container view
  deployment.c4           deployment nodes and Deployment views
  obsidian.c4             Obsidian components and their primary view
  vscode.c4               VS Code components and their primary view
  shared-renderer.c4      renderer components and their primary view
  state-projection.c4     WebAssembly components and their primary view
  code/                   language modules and focused Code views
  views/dynamic/          cross-domain runtime sequences

Scoped views provide System Context to Container to Component navigation. Selected containers and components use explicit navigateTo targets. View title paths group diagrams under Overview, Components, Code, Dynamic, and Deployment.

Two rules keep the levels readable. External people and systems carry the external tag, and every view greys them, so the system boundary is visible. A relationship label starts with a capital letter and a present-tense verb, and does not end with a preposition; it carries a technology when it crosses a process, a language, or a storage boundary.

Hosting belongs to the deployment model. A container diagram shows what a container depends on. It does not show which application process contains it.

Module identity and nesting use these rules:

  • Rust starts at the nearest Cargo.toml package. Hyphens become underscores. lib.rs and main.rs identify the crate, file-backed modules follow their path below src/, and nested mod declarations append their AST nesting. A src/bin target starts a separate crate identity.
  • TypeScript and JavaScript use the repository-relative ES module path. Declared namespaces append their AST nesting to that module.
  • Python strips the longest configured source root. A normal file is an importable module. An __init__.py file identifies its package.
  • Go groups all files with the same directory and package declaration into one package node.

Private and public modules are both architecture nodes. Architecture describes ownership, not only a public API. A repeated module or Go package has one node; the first repository path in lexical order is its source anchor. Module ids are stable hashes of the language and normalized module identity. Import edges are present only when the imported module resolves to another generated node. External package imports do not create placeholder nodes.

CLI validation flow

When the vault has at least one .c4 file, src/likec4.rs starts the embedded assets/likec4-bridge.mjs program with the local Node.js command. The bridge resolves the local likec4 package. It does not use a global LikeC4 command and does not download a package.

The exact runtime contract is:

ItemRequired value
Node.js26.5.1
LikeC41.59.2
React19.2.8
React DOM19.2.8
Bridge protocol1
State schemacriv.state.v1
Process limit60 seconds
Standard output limit16 MiB
Standard error limit16 MiB

The bridge uses LikeC4.fromWorkspace, getErrors, and layoutedModel. It returns one JSON response with the exact runtime versions, revision, errors, layout model, sorted elements, sorted relations, sorted views, and sorted source links. Rust reads standard output and standard error at the same time. This prevents a pipe deadlock when a large layout model fills an operating-system pipe.

Rust rejects a timeout, too much output, invalid JSON, a protocol difference, or any runtime version difference. LikeC4 errors use repository paths and one-based lines in criv output. A vault with no .c4 file does not need Node.js or LikeC4.

A LikeC4 element can have one criv source anchor:

link ../../src/likec4.rs 'source'

The source label is case-insensitive. The target is relative to the LikeC4 workspace. The bridge changes it to a repository-relative target. criv then uses its normal source-target resolver. A missing file, line, symbol, or pattern is an invalid-likec4-source error. Other LikeC4 links stay normal links.

State and editor flow

criv watch --once writes the normalized layout model to the top-level architecture field in .criv/state.json. The field has the bridge protocol, LikeC4 version, workspace path, revision, raw layout model, elements, relations, views, and source links.

The shared packages/criv-likec4 package adapts this state to LikeC4 React. It owns model replacement, stale-revision rejection, view selection, pan, zoom, search, source-link events, disposal, and SVG export. SVG export puts the LikeC4 shadow DOM in an SVG foreignObject, so the exported file contains the same LikeC4 view and styles.

The Obsidian and VS Code packages are host adapters. They read criv state, attach a monotonic host revision, select the host color scheme, and send source link events to the host file API. Normalized view records contain LikeC4’s optional sourcePath. Both hosts use it to select the view owned by the opened file without parsing .c4 source. The shared renderer handles LikeC4 onNavigateTo events and reports the selected view to the host control. VS Code remembers that selection across state refreshes.

There is no fallback diagram. A domain file opens the named views that it owns. A shared file such as specification.c4 can own no view and shows an explicit status message. The status message points to another architecture file that declares a named view; it does not assume that every view is under views/.

VS Code registers the read-only preview as the default .c4 editor; Reopen Editor With → Text Editor exposes the DSL. The repository recommends the official likec4.likec4-vscode extension for DSL language services and maps .c4 text documents to its likec4 language ID. That extension remains optional and does not own the default preview. Neither host starts Node.js. Their installed browser bundles contain the renderer and its assets. The VS Code webview permits only its local resources under a strict content security policy.

After an agent saves a .c4 file, the normal watch task validates the complete workspace and writes a new state model. Each host replaces the old model. A response with an old revision cannot replace a new model. A closed view always disposes its React root.

Installation and CI

The root package-lock.json is the only npm lockfile. Use the pinned Node.js version and run npm ci. Normal validation can then run without network access. CI must run the Rust workspace tests, the two editor test suites, criv check, and npm audit.

The 2026-08-04 implementation measurements used Node.js 26.5.1 on an Apple arm64 development host and the repository model that existed during the test. Architecture changes can change its element, view, and source-link counts:

MeasureResult
Node.js empty-process start0.02 s
Full criv check with LikeC4 layout3.80 s
Obsidian production JavaScript2,764,888 bytes
VS Code extension JavaScript26,311 bytes
VS Code LikeC4 webview JavaScript2,294,423 bytes
Cold browser bundle transfer17.5 ms
Cold state transfer2.9 ms
Cold first LikeC4 view112.2 ms
Test SVG export188,499 bytes

The cold browser test used a local server and a new headless Chromium session. The first-view point was the first LikeC4 React Flow node in the renderer shadow root.

The lockfile license scan found no package without license data except the three local workspaces. The shared workspace now declares MIT. Production bundles keep esbuild legal comments. GPL and MPL packages in the lockfile are development tools and are not in the production renderer bundles. The npm audit after the lockfile update reports zero known vulnerabilities.

Repeatable Performance Evidence

Performance claims in criv use correctness invariants plus repeated external timing samples from an explicit release binary. One timing run on the development checkout is useful for exploration but is not project evidence.

Canonical workloads

The canonical workload set has two maintainer-approved shapes derived from observed criv vaults:

ManifestTierNotesSource filesSource bytesLinks and referencesPoliciesC4 artifactsChanged sources
barrs-small.tomlsmall2312273,9483001 of 12
criv-medium.tomlmedium771191,291,218212441 of 119

The checked-in manifests retain repository revision, note split, language/file extension distribution, symbols, link categories, and exact changed fraction. Generated workload content is sanitized and deterministic; it reproduces the shape, not proprietary text or source.

A large tier is deliberately absent. No observed large criv vault was available for approval on 2026-08-03, and extrapolating either current shape would create the invented average prohibited by the evidence policy. Adding a large tier requires a separately reviewed observed manifest.

Docker is an optional execution environment, not a workload. An explicitly invoked Testcontainers for Rust lane builds and runs the harness inside a digest-pinned image, then supplies these same manifests and generated vaults. A future observed large manifest can use that lane without making the container image, an arbitrary checked-out codebase, or container storage into the vault definition.

Required run identity

Harness runs require an explicit executable path and profile identity. Evidence records the canonical binary path and BLAKE3 digest, repository revision and dirty status, Cargo profile, rustc --version --verbose, operating system, release, architecture, processor model when available, UTC start time, harness schema, workload manifest bytes and digest, command case, declared cache state, and sample count.

Results from different binaries, workload manifests, profiles, or cache-state contracts are not combined. Machine differences remain visible metadata rather than being normalized into a claim of universal performance.

Containerized evidence additionally records the immutable image digest, Docker server identity, and container architecture. The release binary is built inside that pinned environment; a macOS or Windows host executable is never copied into a Linux container and mislabeled as the same artifact. Host and container rows are distinct machine identities and are not merged.

Sampling and isolation

Every command case declares cold or warm initial local state. A cold sample starts from a newly generated vault with no .criv/. A warm sample starts from another newly generated vault, performs its declared untimed seed command, and then measures the requested command. Each timed sample gets a separate vault; no sample inherits .criv files or generated outputs from another sample.

An untimed disposable warm-up precedes the recorded samples for each workload and case. The default sample count is five and the minimum evidence count is three. Failed commands remain raw sample rows with exit status and captured output references, make the harness fail, and are excluded from successful timing summaries rather than disappearing.

Raw and summarized evidence

Each run writes a new result directory. samples.jsonl contains one row per sample, preserving identity, cache state, exit status, elapsed/user/system seconds, output digests, generated-state and snapshot hashes when present. summary.json groups compatible successful rows and reports sample count, minimum, median, maximum, and median absolute deviation without discarding raw values. report.html is a self-contained derived view with shared-scale timing ranges, exact-value tables, run identity, and workload provenance. It uses no external assets or services, and JSON remains the canonical evidence.

Cross-commit comparisons use separate result directories and freshly generated vaults. They compare rows only when workload digest, command case, cache state, sample count, profile, and machine identity match. Results are local artifacts and stay outside source control.

Core boundary

The harness observes criv only as a subprocess. Core code contains no performance environment protocol, spans, counters, or measurement artifact writer, and the harness uses the same ordinary release binary distributed to users. Process resource usage and output identities are the complete runtime observation surface.

Correctness tests may count work behind cfg(test) to prove invariants such as partition reuse or a single source enumeration. Those assertions are compiled only for tests and are not a runtime measurement API. Repeated timing samples support such claims but do not identify internal operations on their own.

Validation boundary

Performance runs are deliberately absent from normal hk hooks and hosted CI, as established by ADR-0049. Harness and generator smoke tests validate their contracts, while contributors invoke measurements explicitly through mise run perf.

The Docker/Testcontainers lane is ignored by default and has its own explicit entry point because it requires a Docker-API-compatible runtime and may need to acquire the pinned image. Testcontainers owns container startup and cleanup; the harness still owns sample isolation inside the container.

Push notes

Every successful repository push runs a separate, non-gating host measurement workflow and publishes its compact JSON summary to refs/notes/criv-performance. The GitHub job summary shows the headline values and links to a 30-day workflow artifact containing report.html alongside the complete raw result directory. The note names the artifact and report path. Fetch and inspect the durable note with:

git fetch origin refs/notes/criv-performance:refs/notes/criv-performance
git log --notes=criv-performance
git notes --ref=criv-performance show <commit>

The publisher uses the workflow’s scoped token, never force-pushes the notes ref, and retries after concurrent note updates. The job is not part of the CI aggregate or local hooks and does not require Docker.

The governing decision is ADR-0072.

Pre-commit Manager Integration

criv does not install Git hooks or change core.hooksPath. Add criv to the hook manager already used by your repository. Run the commit commands in order:

criv watch --once
criv check --changed
criv enforce --stage commit

Add this command to a push hook:

criv enforce --stage push

Keep the commit commands serial. criv check --changed reads the state written by criv watch --once.

hk

local criv_check = new Step {
  check = "criv watch --once && criv check --changed"
}

local criv_enforce = new Step {
  check = "criv enforce --stage commit"
}

hooks {
  ["pre-commit"] {
    steps {
      ["criv-check"] = criv_check
      ["criv-enforce"] = criv_enforce
    }
  }

  ["pre-push"] {
    steps {
      ["criv-enforce"] = new Step { check = "criv enforce --stage push" }
    }
  }
}

lefthook

pre-commit:
  parallel: false
  commands:
    criv-watch:
      run: criv watch --once
    criv-check:
      run: criv check --changed
    criv-enforce:
      run: criv enforce --stage commit

pre-push:
  commands:
    criv-enforce:
      run: criv enforce --stage push

Husky

Run npx husky init, then replace .husky/pre-commit with:

criv watch --once
criv check --changed
criv enforce --stage commit

Add .husky/pre-push:

criv enforce --stage push

Python pre-commit

Add repository-local hooks to .pre-commit-config.yaml:

repos:
  - repo: local
    hooks:
      - id: criv-watch
        name: Refresh criv state
        entry: criv watch --once
        language: system
        pass_filenames: false
        stages: [pre-commit]
      - id: criv-check
        name: Check changed documentation
        entry: criv check --changed
        language: system
        pass_filenames: false
        stages: [pre-commit]
      - id: criv-enforce
        name: Enforce criv policy
        entry: criv enforce --stage commit
        language: system
        pass_filenames: false
        stages: [pre-commit]
      - id: criv-enforce-push
        name: Enforce criv push policy
        entry: criv enforce --stage push
        language: system
        pass_filenames: false
        stages: [pre-push]

Then run pre-commit install --hook-type pre-commit --hook-type pre-push.

simple-git-hooks

Add hook commands to package.json:

{
  "simple-git-hooks": {
    "pre-commit": "criv watch --once && criv check --changed && criv enforce --stage commit",
    "pre-push": "criv enforce --stage push"
  }
}

Run the manager install command after you update package.json.

See Tooling and Git Hooks for the hook-stage contract.

criv query reference

criv query asks the loaded vault graph focused questions and prints one row per result. Add --format json to any query for a JSON array of rows:

criv query nodes --kind decision --format json

criv query

  • Usage: criv query <SUBCOMMAND>

criv query next-adr-id

  • Usage: criv query next-adr-id [--format <FORMAT>]

Print the next ADR id after the highest existing ADR id

Flags

--format <FORMAT>

Select text rows or a JSON array of rows

Choices:

  • text
  • json

Default: text

criv query callers

  • Usage: criv query callers [--format <FORMAT>] <SYMBOL>

List source symbols that call the requested symbol

Arguments

<SYMBOL>

Source path or symbol selector

Flags

--format <FORMAT>

Select text rows or a JSON array of rows

Choices:

  • text
  • json

Default: text

criv query callees

  • Usage: criv query callees [--format <FORMAT>] <SYMBOL>

List source symbols called by the requested symbol

Arguments

<SYMBOL>

Source path or symbol selector

Flags

--format <FORMAT>

Select text rows or a JSON array of rows

Choices:

  • text
  • json

Default: text

criv query attack-surface

  • Usage: criv query attack-surface [--format <FORMAT>]

List exported or public source symbols in the source graph

Flags

--format <FORMAT>

Select text rows or a JSON array of rows

Choices:

  • text
  • json

Default: text

criv query targets

  • Usage: criv query targets [--format <FORMAT>] <NOTE_ID>

List source and pattern targets declared or linked by a note

Arguments

<NOTE_ID>

Note id or unique note name

Flags

--format <FORMAT>

Select text rows or a JSON array of rows

Choices:

  • text
  • json

Default: text

criv query cites

  • Usage: criv query cites [--format <FORMAT>] <NOTE_ID>

List notes, sources, and patterns cited by a note

Arguments

<NOTE_ID>

Note id or unique note name

Flags

--format <FORMAT>

Select text rows or a JSON array of rows

Choices:

  • text
  • json

Default: text

criv query cited-by

  • Usage: criv query cited-by [--format <FORMAT>] <NOTE_ID>

List notes that cite the requested note

Arguments

<NOTE_ID>

Note id or unique note name

Flags

--format <FORMAT>

Select text rows or a JSON array of rows

Choices:

  • text
  • json

Default: text

criv query orphan-docs

  • Usage: criv query orphan-docs [--format <FORMAT>]

List documentation notes without incoming or outgoing note citations

Flags

--format <FORMAT>

Select text rows or a JSON array of rows

Choices:

  • text
  • json

Default: text

criv query references

  • Usage: criv query references [--format <FORMAT>] <SYMBOL>

List notes that reference a source path or symbol

Arguments

<SYMBOL>

Source path or symbol selector

Flags

--format <FORMAT>

Select text rows or a JSON array of rows

Choices:

  • text
  • json

Default: text

criv query governs

  • Usage: criv query governs [--format <FORMAT>] <ADR_ID>

List source files governed by a decision

Arguments

<ADR_ID>

ADR id

Flags

--format <FORMAT>

Select text rows or a JSON array of rows

Choices:

  • text
  • json

Default: text

criv query governing

  • Usage: criv query governing [--format <FORMAT>] <SYMBOL>

List decisions that govern a source path or symbol

Arguments

<SYMBOL>

Source path or symbol selector

Flags

--format <FORMAT>

Select text rows or a JSON array of rows

Choices:

  • text
  • json

Default: text

criv query coverage

  • Usage: criv query coverage [--by <BY>] [--format <FORMAT>]

Summarize source governance coverage

Flags

--by <BY>

Group coverage rows by module or ADR

Choices:

  • module
  • adr

--format <FORMAT>

Select text rows or a JSON array of rows

Choices:

  • text
  • json

Default: text

criv query nodes

  • Usage: criv query nodes [FLAGS]

List source, note, or decision nodes

Flags

--kind <KIND>

Restrict nodes to code, documentation, or decisions

Choices:

  • code
  • doc
  • decision

--without-docs

Restrict code nodes to symbols that no note references

--format <FORMAT>

Select text rows or a JSON array of rows

Choices:

  • text
  • json

Default: text

criv query c4-code

  • Usage: criv query c4-code [--format <FORMAT>] <PATH_GLOB>

Emit focused LikeC4 source for modules in a source path glob

Arguments

<PATH_GLOB>

Source path or component/module glob

Flags

--format <FORMAT>

Select text rows or a JSON array of rows

Choices:

  • text
  • json

Default: text

criv query diff

  • Usage: criv query diff [--format <FORMAT>] <REF_A> <REF_B>

Compare two state snapshots or git refs

Arguments

<REF_A>

Left snapshot hash, latest, or git ref

<REF_B>

Right snapshot hash, latest, or git ref

Flags

--format <FORMAT>

Select text rows or a JSON array of rows

Choices:

  • text
  • json

Default: text

diff resolves latest through .criv/latest, hex-like values through .criv/snapshots/<hash>.json, and any other value through an embedded lookup of .criv/state.json in the requested repository ref. It does not invoke the git executable.

Releasing criv

Use Cocogitto to calculate the next SemVer version from conventional commits, then use cargo-release to update workspace Cargo versions.

Before preparing a release, run:

cargo test --workspace
cargo fmt --check
target/debug/criv check
target/debug/criv enforce --stage ci
target/debug/criv watch --once
target/debug/criv query diff latest latest

target/debug/criv check embeds rumdl as a Rust crate, so Markdown formatting does not require a separate rumdl executable.

Build plugin artifacts when the Obsidian plugin is part of the release:

npm --prefix .obsidian/plugins/criv run build

This wraps the plugin’s npm run build script in mise x rust@1.97.1 so wasm-pack sees the mise-managed Rust and Cargo toolchain, including the installed wasm32-unknown-unknown target. If only the Rust CLI is being released and the generated plugin scaffold is unchanged, document that choice in the release notes.

Preview the next automatically selected version:

mise run release-plan

For a tag-only release:

mise run release-auto

release-auto is implemented by scripts/release-auto.sh. It requires a clean main branch, asks Cocogitto for the next version with cog bump --dry-run --auto, updates workspace Cargo versions, runs the pre-release checks above, commits the version bump, creates vX.Y.Z and criv-wasm-vX.Y.Z, and pushes the commit and tags.

Conventional commits drive the automatic bump: fix produces a patch release, feat produces a minor release, and ! or BREAKING CHANGE: produces a major release. While criv is still in 0.y.z, Cocogitto will not automatically select 1.0.0; cut that intentionally with a manual versioned release if needed. This decision is captured in ADR-0016.

The next release remains git-tag-only. Do not publish criv to crates.io until the CLI API, state schema compatibility policy, and installer story are stable enough to support registry consumers. The tag-triggered GitHub binary release is the authoritative distribution path for now.

When crates.io publishing is intentionally enabled later, confirm the package metadata first:

cargo package --workspace --allow-dirty
cargo publish --dry-run

Then run the matching cargo release command without --no-publish. Crates.io publishing remains manual for now; release-auto only cuts the tag-triggered binary release.

Current tag names use:

  • vX.Y.Z for the root CLI crate.
  • criv-wasm-vX.Y.Z for the WASM helper crate.

Release binary automation should run only when a v* root CLI release tag is pushed. The workflow should build criv archives named by Rust target triple:

  • criv-x86_64-unknown-linux-gnu.tar.gz
  • criv-aarch64-unknown-linux-gnu.tar.gz
  • criv-aarch64-apple-darwin.tar.gz
  • criv-x86_64-pc-windows-msvc.zip

Intel macOS release archives are deprecated by ADR-0017 because the hosted Intel macOS runner is the slowest release job. Apple Silicon macOS remains the supported macOS binary target. Reintroduce criv-x86_64-apple-darwin.tar.gz only if there is measured user demand or a faster runner path.

Release assets should include SHA256SUMS.txt, GitHub build provenance attestations, and criv --version as the installer smoke test for future aqua and mise registry entries. This decision is captured in ADR-0014.

Each platform archive also includes vscode-criv.vsix next to the executable. The release workflow builds that package once and adds the same local-only viewer to every archive, as required by ADR-0087.

Release binaries use the workspace release profile in Cargo.toml: symbols are stripped, size optimization is enabled, LTO runs at link time, codegen uses one unit, and release panics abort. This keeps downloadable artifacts smaller without requiring nightly Rust or post-build binary packing. The profile decision is captured in ADR-0015.

The CLI embeds local Git repository access through its Rust dependency graph; the release artifact does not require a git executable for query-diff or enforcement repository reads. Release verification should keep a PATH-without- Git smoke test alongside the hosted core validation profile. The backend scope and dependency evidence are recorded by the embedded-repository-access ADR.

Local State And Snapshot Reference

criv watch --once and live watch publication write the current graph to .criv/state.json. They also keep content-addressed local snapshots for criv query diff. The snapshot store is ignored local data; source control remains the durable history boundary.

Configure the maximum number of distinct local snapshots in criv.toml:

[state]
keep = 20

keep must be a positive integer and defaults to 20. The bound is applied after every successful state publication. Publishing identical content again does not consume another slot, but makes that hash the newest publication. The snapshot named by .criv/latest is always protected.

Inspect retained snapshots newest first:

criv state list
criv state list --format json

Preview or apply pruning with the configured bound:

criv state prune --dry-run
criv state prune
criv state prune --keep 5
criv state prune --keep 5 --format json

--keep is a one-command positive override; it does not rewrite criv.toml. --dry-run selects and reports the same snapshots without changing local files. Corrupt recognized snapshots fail closed and are not deleted automatically.

criv query diff <a> <b> resolves latest and retained hashes from the local store. Other values remain embedded Git-ref lookups of .criv/state.json. Pruning local snapshots does not inspect or alter Git refs.

The lifecycle and compatibility boundary are defined by ADR-0068.

Tooling and Git Hooks

The repository uses mise.toml to install the project hook runner and hk.pkl to define the hook behavior. criv.toml also declares project-level tooling files, including Hawk’s hawk.toml, as source roots. The decision record is ADR-0013.

Run the initial setup with:

mise install

The mise postinstall hook runs hk install --mise, so Git hook execution goes through mise x and uses the pinned tool versions, including hk, Rust 1.97.1, and actionlint. GitHub Actions security analysis is pinned here too: zizmor checks workflow, composite action, and Dependabot definitions for risky CI/CD patterns. Hawk uses the same Rust toolchain to check whether public Rust APIs are required by the shipped criv binary. Release tooling is also pinned here: Cocogitto calculates the next SemVer version from conventional commits, and cargo-release updates Cargo workspace versions. HK_PKL_BACKEND=pklr keeps hk self-contained by avoiding a separate pkl CLI requirement.

Workflow YAML under .github/workflows/ is checked with actionlint in pre-commit and the full check hook. zizmor runs in offline mode in the same hooks so local validation does not require a GitHub token or network access. This follow-up hook decision is ADR-0018.

Running criv from a hook runner

criv does not install Git hooks and does not set core.hooksPath, per ADR-0054. Setting that config replaces the hook directory wholesale, which silently disables whichever runner the repository already uses. Wire criv into your own runner instead.

The commands are stable:

StageCommands
commitcriv watch --once, criv check --changed, criv enforce --stage commit
pushcriv enforce --stage push

watch --once refreshes .criv/state.json before the changed check. The changed check is a staged, read-only partial fast path; plain criv check remains the full manual and hosted authority per ADR-0067. Run commit commands in the listed order.

With hk, as this repository does in hk.pkl:

local criv_check_changed = new Step {
  glob = List("**/*.md", "criv.toml")
  check_first = true
  check = "criv check --changed"
  fix = "criv check --fix"
}

local criv_enforce_commit = new Step {
  check = "criv enforce --stage commit"
}

hooks {
  ["pre-commit"] {
    fix = true
    steps {
      ["criv-check"] = criv_check_changed
      ["criv-enforce"] = criv_enforce_commit
    }
  }
}

With lefthook, in lefthook.yml:

pre-commit:
  parallel: false
  commands:
    criv-watch:
      run: criv watch --once
    criv-check:
      run: criv check --changed
    criv-enforce:
      run: criv enforce --stage commit

pre-push:
  commands:
    criv-enforce:
      run: criv enforce --stage push

Keep parallel: false for the commit stage: check reads the state watch writes, so they must not overlap.

Manual task entry points are:

mise run commit-msg -- .git/COMMIT_EDITMSG
mise run pre-commit
mise run pre-push
mise run fix
mise run perf
mise run vscode-build
mise run vscode-package
mise run release-plan
mise run release-auto

Pre-commit and pre-push are the automatic local validation boundary. Agents and contributors do not need to replay them with an aggregate command after each commit. The former local aggregate task has been removed.

Hosted CI invokes the complete hk check profile directly. Every step hk runs is defined in hk.pkl with its command inline, per ADR-0049. To inspect or debug one core step explicitly:

hk check --all --plan                        # list every step
hk check --all --step hawk

Pre-push runs Clippy, workspace tests, Hawk, and criv push enforcement. Hawk is kept in both pre-push and the hosted CI profile so unnecessary public Rust APIs fail before hosted CI during normal local development.

mise run perf generates isolated vaults from the checked-in barrs-small and criv-medium manifests, then measures an explicit release binary. Build that binary first and identify its profile at the command line:

cargo build --release
mise run perf -- --binary target/release/criv --profile release

The script records repeated samples for cold, warm, and changed watch --once, source-index file search startup, validation, CI enforcement, docs-only next-adr-id, orphan-docs, and nodes --kind doc queries, and a no-op snapshot diff. Five samples are collected by default; use --samples to change the count or repeat --case to select cases:

mise run perf -- --binary target/release/criv --profile release --samples 9
mise run perf -- --binary target/release/criv --profile release --case check

Every recorded sample receives a fresh generated vault. Warm cases perform an untimed state-building run inside that vault before the recorded command, and the changed case mutates the manifest’s declared number of supported source files without changing their sizes. A unique result directory preserves the run identity, exact manifest copies, raw JSONL samples, stdout/stderr, and a JSON summary with min/median/max and median absolute deviation. Failed samples remain in the raw evidence and make the harness fail.

mise run perf-container runs one ignored smoke case through Testcontainers. It builds criv and the harness inside a digest-pinned Rust Linux image. Docker is only an optional execution environment: the same checked-in manifests and generator define the vaults, results have their own container machine identity, and this task is not part of hooks or hosted CI. A Docker-API-compatible runtime is required. Large-workload measurement remains deferred.

Obsidian and VS Code validation are hosted-CI suites rather than automatic hk steps. Contributors changing a companion should run its package scripts directly. Run npm ci from the repository root first. The Obsidian companion commands are:

npm audit --workspace criv-obsidian-plugin --audit-level=high
npm --prefix .obsidian/plugins/criv run format:check
npm --prefix .obsidian/plugins/criv run lint
npm --prefix .obsidian/plugins/criv test
npm --prefix .obsidian/plugins/criv run build:wasm
npm --prefix .obsidian/plugins/criv run build:plugin
npm --prefix .obsidian/plugins/criv run build

The combined build preserves the release and manual entry point by running the Wasm build followed by the plugin build. The Wasm command invokes wasm-pack through mise exec cargo:wasm-pack@0.15.0, so the repository’s pinned tools are used rather than whichever Rust or wasm-pack appears first in the shell. It writes criv_wasm.js, criv_wasm_bg.js, and criv_wasm_bg.wasm under .obsidian/plugins/criv/pkg/. The plugin bundle keeps that runtime import external, so a distributable plugin must carry the generated pkg/ directory beside main.js.

If Obsidian reports that the criv Wasm runtime is unavailable, run the combined build, confirm those three runtime files exist, and reload the plugin. A state schema or parse error is separate: regenerate .criv/state.json with criv watch --once instead of rebuilding Wasm.

The VS Code-compatible extension lives in extensions/vscode-criv. Its local tasks are exposed through mise and npm:

npm audit --workspace vscode-criv --audit-level=high
npm --prefix extensions/vscode-criv run build
npm --prefix extensions/vscode-criv run test
npm --prefix extensions/vscode-criv run test:integration
npm --prefix extensions/vscode-criv run lint
npm --prefix extensions/vscode-criv run format:check
npm --prefix extensions/vscode-criv run package

The integration command launches the VS Code extension test host and fails on JSON diagnostics for extension manifest and language configuration files. Its short temporary user-data and extension-installation paths avoid macOS IPC path limits. Hosted CI runs this command under Xvfb; it is not part of local hooks.

The extension uses a read-only LikeC4 preview as the default .c4 editor. It selects the named view owned by the opened file. There is no fallback view: a file that owns none states so and points at a file that declares a view. Navigating inside the diagram opens the file that owns the target view in the same editor group, so the tab tracks the diagram. Use Reopen Editor With → Text Editor to edit the DSL. The preview webview uses packaged extension resources and does not use a CDN or a global LikeC4 command. Run criv watch --once after architecture or extension source changes. This keeps architecture state current. The repository Code model and views are hand-authored under docs/architecture/code/, so watch does not replace them.

npm --prefix extensions/vscode-criv run package and mise run vscode-package build a local vscode-criv.vsix without publishing. mise run vscode-install builds that VSIX and installs it into the local editor; reload the window afterwards. The prepublish hook builds the Node.js Wasm target into extensions/vscode-criv/pkg/; the VSIX includes criv_wasm.js and criv_wasm_bg.wasm from that directory. If the extension reports an unavailable Wasm runtime, rebuild build:wasm, recreate the VSIX, confirm those files are present in the package, and reload the editor window. Regenerate invalid state with criv watch --once; do not treat a state-validation error as a runtime packaging failure.

The optional viewer is local-only under ADR-0087. The project does not publish it to the VS Code Marketplace or Open VSX. Release archives put the one viewer package next to the criv executable. Install it with an explicit editor selection:

criv install-editor --editor code
criv install-editor --editor cursor

Use --dry-run to validate the bundled viewer and local editor command without changing editor state.

Use hk validate after editing hk.pkl. Prefer hk built-ins when one exists; the commit message hook uses Builtins.check_conventional_commit instead of a shell wrapper around hk util check-conventional-commit.

The hawk hk step runs Hawk with warnings denied against the shipped criv CLI. The criv-wasm crate is excluded because its exported functions are consumed by the separately built WASM artifacts, outside the CLI binary’s Cargo graph. The pre-push and full check hooks run Hawk whenever Rust sources, Cargo metadata, or its configuration change. Hawk uses target/hawk for its instrumented Cargo artifacts so its compiler work stays isolated from the other parallel checks. This policy is captured in ADR-0043 and the local/hosted validation boundary is ADR-0061.