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
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.qual merge=union
1 change: 1 addition & 0 deletions .qualignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
site/examples/
9 changes: 9 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,15 @@
- Include results for `cargo fmt`, `cargo clippy --all-targets --all-features`, and `cargo test --all-features`; attach CLI examples when changing text output.
- For release work, note whether `./scripts/release.sh --execute` should be run.

## Keeping Things in Sync
When making changes, verify that all affected surfaces stay consistent:
- **SPEC.md** — Section 7 (Library API) must match public function signatures. Section 10 (File Discovery) must match discovery behavior. Update the spec version when semantics change.
- **README.md** — Core Concepts and CLI Commands table should reflect current behavior.
- **site/** — `site/js/playground.js` contains a JavaScript scoring engine for the web playground. If scoring logic, record format, or field names change, update it to match.
- **Cargo.toml** — Bump the crate version for any user-visible change (new feature, behavior change, bug fix). Coordinate with `SPEC.md` version when the spec itself changes.
- **Tests** — Many test files have local `make_att()`/`make_record()` helpers that construct records by hand. When adding or renaming fields on `Attestation`, `Epoch`, or `DependencyRecord`, update all helpers (~6 locations across `src/` and `tests/`). Run `cargo test --all-features` to catch any you miss.
- **Golden IDs** — `tests/integration.rs` pins BLAKE3 IDs for attestation, epoch, and dependency records. Any change to canonical form (field order, new envelope fields, MCF rules) will break these. Update the expected hashes after confirming the new values are correct.

## Slash Command Discovery
- Unrecognized slash commands should be looked up as files under `.claude/commands/` (e.g., `/foo` looks for `.claude/commands/foo.md`).
- If a matching file exists, treat its contents as the command definition; otherwise continue without adding anything to context.
119 changes: 119 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ cli = ["dep:clap", "dep:comfy-table", "dep:figment", "dep:rand"]
[dependencies]
blake3 = "1"
chrono = { version = "0.4", features = ["serde"] }
ignore = "0.4"
petgraph = "0.7"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ qualifier check --min-score 0

**.qual files** are JSONL files containing records. The recommended layout is one `.qual` file per directory. See [SPEC.md](SPEC.md) for layout options and trade-offs.

**File discovery** respects `.gitignore` and `.qualignore` (gitignore-compatible syntax) by default, so vendored or generated `.qual` files can be excluded from scoring. Pass `--no-ignore` to bypass all ignore rules. See [SPEC.md §10](SPEC.md#10-file-discovery) for details.

## CLI Commands

| Command | Description |
Expand Down
38 changes: 37 additions & 1 deletion SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -934,7 +934,7 @@ pub fn finalize_record(record: Record) -> Record;
pub struct QualFile { pub path: PathBuf, pub subject: String, pub records: Vec<Record> }
pub fn parse(path: &Path) -> Result<QualFile>;
pub fn append(path: &Path, record: &Record) -> Result<()>;
pub fn discover(root: &Path) -> Result<Vec<QualFile>>;
pub fn discover(root: &Path, respect_ignore: bool) -> Result<Vec<QualFile>>;

// qualifier::scoring
pub struct ScoreReport { pub raw: i32, pub effective: i32, pub limiting_path: Option<Vec<String>> }
Expand Down Expand Up @@ -1007,6 +1007,42 @@ The project root is determined by searching upward for VCS markers (`.git`,
`.hg`, `.jj`, `.pijul`, `_FOSSIL_`, `.svn`) or a `qualifier.graph.jsonl`
file, whichever is found first.

### 10.1 Ignore Rules

By default, qualifier respects ignore rules from two sources during file
discovery:

1. **`.gitignore`** — Standard Git ignore files, including:
- `.gitignore` files at any level of the tree
- `.git/info/exclude` (per-repo excludes)
- The global gitignore file (e.g., `~/.config/git/ignore`)
- `.gitignore` files in parent directories above the project root
(matching Git's own behavior in monorepos)

2. **`.qualignore`** — A qualifier-specific ignore file using the same
syntax as `.gitignore`. Place a `.qualignore` file anywhere in the tree
to exclude paths from qualifier's discovery walk. Useful for ignoring
vendored code, generated files, or example directories that have `.qual`
files you want qualifier to skip without affecting Git.

Paths matched by either source are excluded from all discovery commands:
`score`, `show`, `check`, `ls`, `compact`, and `praise`/`blame`.

### 10.2 `--no-ignore`

Pass `--no-ignore` to any discovery command to bypass all ignore rules.
This forces qualifier to walk every non-hidden directory and discover all
`.qual` files regardless of `.gitignore` or `.qualignore` entries.

### 10.3 Hidden Directories

Hidden directories (names starting with `.`) are always skipped during
discovery, regardless of ignore settings. This prevents qualifier from
descending into `.git`, `.vscode`, `.idea`, and similar tool directories.

Hidden *files* (like `.qual`) are not skipped — the per-directory `.qual`
layout depends on this.

## 11. Crate Structure

A single crate published as `qualifier` on crates.io.
Expand Down
6 changes: 5 additions & 1 deletion src/cli/commands/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,17 @@ pub struct Args {
/// Path to the dependency graph file
#[arg(long)]
pub graph: Option<String>,

/// Disable .gitignore and .qualignore filtering
#[arg(long)]
pub no_ignore: bool,
}

pub fn run(args: Args) -> crate::Result<()> {
let root = find_project_root(Path::new("."));
let graph = crate::cli::config::load_graph(args.graph.as_deref(), root.as_deref());
let discover_root = root.as_deref().unwrap_or(Path::new("."));
let qual_files = qual_file::discover(discover_root)?;
let qual_files = qual_file::discover(discover_root, !args.no_ignore)?;

let scores = scoring::effective_scores(&graph, &qual_files);

Expand Down
6 changes: 5 additions & 1 deletion src/cli/commands/compact.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ pub struct Args {
/// Preview without writing
#[arg(long)]
pub dry_run: bool,

/// Disable .gitignore and .qualignore filtering
#[arg(long)]
pub no_ignore: bool,
}

pub fn run(args: Args) -> crate::Result<()> {
Expand Down Expand Up @@ -48,7 +52,7 @@ pub fn run(args: Args) -> crate::Result<()> {
fn run_all(args: &Args) -> crate::Result<()> {
let root = find_project_root(Path::new("."));
let discover_root = root.as_deref().unwrap_or(Path::new("."));
let qual_files = qual_file::discover(discover_root)?;
let qual_files = qual_file::discover(discover_root, !args.no_ignore)?;

if qual_files.is_empty() {
println!("No .qual files found.");
Expand Down
6 changes: 5 additions & 1 deletion src/cli/commands/ls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,17 @@ pub struct Args {
/// Path to the dependency graph file
#[arg(long)]
pub graph: Option<String>,

/// Disable .gitignore and .qualignore filtering
#[arg(long)]
pub no_ignore: bool,
}

pub fn run(args: Args) -> crate::Result<()> {
let root = find_project_root(Path::new("."));
let graph = crate::cli::config::load_graph(args.graph.as_deref(), root.as_deref());
let discover_root = root.as_deref().unwrap_or(Path::new("."));
let qual_files = qual_file::discover(discover_root)?;
let qual_files = qual_file::discover(discover_root, !args.no_ignore)?;

let scores = scoring::effective_scores(&graph, &qual_files);

Expand Down
6 changes: 5 additions & 1 deletion src/cli/commands/praise.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ pub struct Args {
#[cfg(not(target_os = "emscripten"))]
#[arg(long)]
pub vcs: bool,

/// Disable .gitignore and .qualignore filtering
#[arg(long)]
pub no_ignore: bool,
}

/// Record-based praise output — works everywhere including emscripten.
Expand All @@ -33,7 +37,7 @@ pub fn run(args: Args) -> crate::Result<()> {
fn run_records(args: Args) -> crate::Result<()> {
let root = find_project_root(Path::new("."));
let discover_root = root.as_deref().unwrap_or(Path::new("."));
let all_qual_files = qual_file::discover(discover_root)?;
let all_qual_files = qual_file::discover(discover_root, !args.no_ignore)?;

let records: Vec<&crate::attestation::Record> =
qual_file::find_records_for(&args.artifact, &all_qual_files);
Expand Down
6 changes: 5 additions & 1 deletion src/cli/commands/score.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,17 @@ pub struct Args {
/// Path to the dependency graph file
#[arg(long)]
pub graph: Option<String>,

/// Disable .gitignore and .qualignore filtering
#[arg(long)]
pub no_ignore: bool,
}

pub fn run(args: Args) -> crate::Result<()> {
let root = find_project_root(Path::new("."));
let graph = crate::cli::config::load_graph(args.graph.as_deref(), root.as_deref());
let discover_root = root.as_deref().unwrap_or(Path::new("."));
let qual_files = qual_file::discover(discover_root)?;
let qual_files = qual_file::discover(discover_root, !args.no_ignore)?;

let scores = scoring::effective_scores(&graph, &qual_files);

Expand Down
6 changes: 5 additions & 1 deletion src/cli/commands/show.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,17 @@ pub struct Args {
/// Path to the dependency graph file
#[arg(long)]
pub graph: Option<String>,

/// Disable .gitignore and .qualignore filtering
#[arg(long)]
pub no_ignore: bool,
}

pub fn run(args: Args) -> crate::Result<()> {
let root = find_project_root(Path::new("."));
let graph = crate::cli::config::load_graph(args.graph.as_deref(), root.as_deref());
let discover_root = root.as_deref().unwrap_or(Path::new("."));
let all_qual_files = qual_file::discover(discover_root)?;
let all_qual_files = qual_file::discover(discover_root, !args.no_ignore)?;

let records = qual_file::find_records_for(&args.artifact, &all_qual_files);

Expand Down
Loading
Loading