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
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[canon]
expected_workspace_version = "0.72.5"
expected_workspace_version = "0.72.6"
install_command = "follow the install guide at https://github.com/apply-the/canon#install"
install_guidance_ref = "https://github.com/apply-the/canon#install"
release_surface = "https://github.com/apply-the/canon/releases"
Expand Down
2 changes: 1 addition & 1 deletion .antigravity-plugin/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "canon",
"displayName": "Canon Assistant Support for Antigravity",
"version": "0.72.5",
"version": "0.72.6",
"description": "Canon provides semantic governance for AI-assisted engineering work: structured packets, governed evidence, approvals, lineage, project memory, and reusable delivery knowledge.",
"author": {
"name": "Apply The",
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "canon",
"displayName": "Canon Assistant Support for Claude Code",
"version": "0.72.5",
"version": "0.72.6",
"description": "Canon provides semantic governance for AI-assisted engineering work: structured packets, governed evidence, approvals, lineage, project memory, and reusable delivery knowledge.",
"author": {
"name": "Apply The",
Expand Down
2 changes: 1 addition & 1 deletion .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "canon",
"displayName": "Canon Assistant Support for Codex",
"version": "0.72.5",
"version": "0.72.6",
"description": "Canon provides semantic governance for AI-assisted engineering work: structured packets, governed evidence, approvals, lineage, project memory, and reusable delivery knowledge.",
"author": {
"name": "Apply The",
Expand Down
2 changes: 1 addition & 1 deletion .cursor-plugin/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "canon",
"displayName": "Canon Assistant Support for Cursor",
"version": "0.72.5",
"version": "0.72.6",
"description": "Canon provides semantic governance for AI-assisted engineering work: structured packets, governed evidence, approvals, lineage, project memory, and reusable delivery knowledge.",
"author": {
"name": "Apply The",
Expand Down
8 changes: 4 additions & 4 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ resolver = "2"
edition = "2024"
license = "MIT"
rust-version = "1.96.0"
version = "0.72.5"
version = "0.72.6"

[workspace.dependencies]
assert_cmd = "2"
Expand Down
2 changes: 1 addition & 1 deletion assistant/plugin-metadata.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "canon",
"displayName": "Canon Assistant Support",
"version": "0.72.5",
"version": "0.72.6",
"description": "Governed packet runtime for AI-assisted engineering work",
"author": {
"name": "Apply The",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ impl EngineService {

write_run_state_finalize(&run_dir, RunState::Finalized)?;

// ── Publish artifacts to .canon/artifacts/<RUN_ID>/pr-review/ ──
publish_artifacts(self, run_id, &run_dir)?;

Ok(())
}
}
Expand Down Expand Up @@ -483,6 +486,36 @@ fn write_run_state_finalize(run_dir: &Path, state: RunState) -> Result<(), Strin
Ok(())
}

/// Copies the finalized review artifacts from the run directory to
/// `.canon/artifacts/<RUN_ID>/pr-review/` so they are discoverable
/// as governed review packet outputs.
fn publish_artifacts(service: &EngineService, run_id: &str, run_dir: &Path) -> Result<(), String> {
let artifacts_dir = service.project_layout().artifacts_dir().join(run_id).join("pr-review");
fs::create_dir_all(&artifacts_dir).map_err(|e| format!("create artifacts directory: {e}"))?;

// Artifacts that constitute the published pr-review packet.
const PUBLISHABLE_ARTIFACTS: &[&str] = &[
"01-review-summary.md",
"06-review-report.md",
"review-findings.json",
"missing-tests.md",
"manifest.toml",
"canonical-review-output.json",
"packet-metadata.json",
"coverage-accounting.md",
];

for filename in PUBLISHABLE_ARTIFACTS {
let src = run_dir.join(filename);
if src.exists() {
fs::copy(&src, artifacts_dir.join(filename))
.map_err(|e| format!("copy artifact `{filename}`: {e}"))?;
}
}

Ok(())
}

/// Produces a `coverage-accounting.md` artifact listing all 7 layers
/// with their status (reviewed/deferred/skipped) and explicit reasons.
///
Expand Down Expand Up @@ -863,5 +896,89 @@ mod tests {
// Verify run state was updated to finalized
let state_content = fs::read_to_string(run_dir.join("run-state.json")).unwrap();
assert!(state_content.contains("finalized"), "state not finalized");

// Verify artifacts were published to .canon/artifacts/
let artifacts_dir =
workspace.path().join(".canon").join("artifacts").join(run_id).join("pr-review");
assert!(
artifacts_dir.join("01-review-summary.md").exists(),
"summary not published to artifacts"
);
assert!(
artifacts_dir.join("06-review-report.md").exists(),
"report not published to artifacts"
);
assert!(
artifacts_dir.join("review-findings.json").exists(),
"findings not published to artifacts"
);
assert!(
artifacts_dir.join("manifest.toml").exists(),
"manifest not published to artifacts"
);
assert!(
artifacts_dir.join("coverage-accounting.md").exists(),
"coverage accounting not published to artifacts"
);
}

// ── publish_artifacts unit tests ─────────────────────────────────────

#[test]
fn publish_artifacts_copies_all_expected_files() {
let workspace = TempDir::new().unwrap();
let run_id = "test-publish-artifacts";
let run_dir = workspace.path().join(".canon").join("runs").join(run_id).join("pr-review");
fs::create_dir_all(&run_dir).unwrap();

// Create each expected artifact in the run directory
let artifact_files = [
"01-review-summary.md",
"06-review-report.md",
"review-findings.json",
"missing-tests.md",
"manifest.toml",
"canonical-review-output.json",
"packet-metadata.json",
"coverage-accounting.md",
];
for file in &artifact_files {
fs::write(run_dir.join(file), format!("content of {file}")).unwrap();
}

let service = EngineService::new(workspace.path());
publish_artifacts(&service, run_id, &run_dir).unwrap();

let artifacts_dir =
workspace.path().join(".canon").join("artifacts").join(run_id).join("pr-review");
for file in &artifact_files {
let dest = artifacts_dir.join(file);
assert!(dest.exists(), "artifact {file} was not published");
let content = fs::read_to_string(&dest).unwrap();
assert_eq!(content, format!("content of {file}"));
}
}

#[test]
fn publish_artifacts_skips_missing_source_files() {
let workspace = TempDir::new().unwrap();
let run_id = "test-publish-missing";
let run_dir = workspace.path().join(".canon").join("runs").join(run_id).join("pr-review");
fs::create_dir_all(&run_dir).unwrap();

// Only create a subset of artifacts; missing files should be skipped
fs::write(run_dir.join("01-review-summary.md"), "summary").unwrap();
fs::write(run_dir.join("manifest.toml"), "manifest").unwrap();

let service = EngineService::new(workspace.path());
let result = publish_artifacts(&service, run_id, &run_dir);
assert!(result.is_ok(), "publish should succeed even with missing source files");

let artifacts_dir =
workspace.path().join(".canon").join("artifacts").join(run_id).join("pr-review");
assert!(artifacts_dir.join("01-review-summary.md").exists());
assert!(artifacts_dir.join("manifest.toml").exists());
// Missing files should not be created
assert!(!artifacts_dir.join("06-review-report.md").exists());
}
}
51 changes: 32 additions & 19 deletions crates/canon-engine/src/review/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

use serde::Deserialize;

use crate::review::onion::{CANONICAL_LAYERS, LayerStatus};
use crate::review::onion::LayerStatus;

/// Result of validating a reviewer output.
#[derive(Debug, Clone)]
Expand Down Expand Up @@ -71,11 +71,13 @@ struct RawFinding {

/// Validates a reviewer output JSON string against the review context.
///
/// Checks schema, severities, recommendation, comment IDs, paths, lines, and layer coverage.
/// Checks schema, severities, recommendation, comment IDs, paths, and lines.
/// Layer coverage validation is delegated to the filesystem-based check in
/// `mode_pr_review_accept::check_layer_coverage`.
pub fn validate_reviewer_output(
json: &str,
changed_files: &[String],
layer_states: &[(String, LayerStatus)],
_layer_states: &[(String, LayerStatus)],
) -> ValidationResult {
let mut errors = Vec::new();
let mut downgrades = Vec::new();
Expand All @@ -89,7 +91,6 @@ pub fn validate_reviewer_output(
check_comment_id_uniqueness(&output, &mut errors);
check_severities(&output, &mut errors);
check_paths_and_lines(&output, changed_files, &mut downgrades);
check_layer_coverage(layer_states, &mut errors);

ValidationResult { valid: errors.is_empty(), errors, downgrades }
}
Expand Down Expand Up @@ -164,17 +165,6 @@ fn check_paths_and_lines(
}
}

fn check_layer_coverage(layer_states: &[(String, LayerStatus)], errors: &mut Vec<String>) {
for layer in CANONICAL_LAYERS {
if *layer == "global" {
continue;
}
if !layer_states.iter().any(|(name, _)| name == *layer) {
errors.push(format!("Layer '{layer}' is missing from layer coverage"));
}
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -259,17 +249,23 @@ mod tests {
}

#[test]
fn test_missing_layer_coverage_blocks() {
fn test_layer_coverage_delegated_to_filesystem_check() {
// Layer coverage is validated by the filesystem-based check in
// mode_pr_review_accept.rs, not by validate_reviewer_output.
// Passing a partial layer state should NOT cause validation failure.
let json = r#"{
"schema_version": "1.0",
"findings": [],
"recommendation": "Comment",
"layer_coverage": {}
}"#;
let partial = vec![("diff".to_string(), LayerStatus::Completed)];
let partial = vec![("early-signal".to_string(), LayerStatus::Completed)];
let result = validate_reviewer_output(json, &[], &partial);
assert!(!result.valid);
assert!(result.errors.iter().any(|e| e.contains("whole_file")));
assert!(
result.valid,
"Layer coverage validation is delegated to filesystem check; partial layer_states should not block here. Errors: {:?}",
result.errors
);
}

#[test]
Expand All @@ -284,4 +280,21 @@ mod tests {
assert!(!result.valid);
assert!(result.errors.iter().any(|e| e.contains("Invalid recommendation")));
}

#[test]
fn test_unsupported_schema_version_rejected() {
let json = r#"{
"schema_version": "0.9",
"findings": [],
"recommendation": "Comment",
"layer_coverage": {}
}"#;
let result = validate_reviewer_output(json, &[], &completed_layers());
assert!(!result.valid);
assert!(
result.errors.iter().any(|e| e.contains("Unsupported schema version")),
"Errors: {:?}",
result.errors
);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[canon]
expected_workspace_version = "0.72.5"
expected_workspace_version = "0.72.6"
install_command = "follow the install guide at https://github.com/apply-the/canon#install"
install_guidance_ref = "https://github.com/apply-the/canon#install"
release_surface = "https://github.com/apply-the/canon/releases"
Expand Down
2 changes: 1 addition & 1 deletion docs/governance/evidence.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Typical meanings:
- approval rejected when downstream use should stop
- approval expired when prior approval no longer applies

Exact machine-facing values are part of the adapter contract. Canonical source: [Governance Adapter Integration](https://github.com/apply-the/canon/blob/0.69.0/tech-docs/integration/governance-adapter.md).
Exact machine-facing values are part of the adapter contract. Canonical source: [Governance Adapter Integration](https://github.com/apply-the/canon/blob/0.72.6/tech-docs/integration/governance-adapter.md).

## Readiness States

Expand Down
4 changes: 2 additions & 2 deletions docs/guide/introduction.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Canon

> [!TIP]
> This wiki is aligned with **Canon 0.69.0**. For older versions, refer to the repository tags.
> This wiki is aligned with **Canon 0.72.6**. For older versions, refer to the repository tags.

![Canon - Semantic Governance Runtime](https://github.com/apply-the/canon/blob/0.69.0/tech-docs/images/canon-banner.jpg?raw=true)
![Canon - Semantic Governance Runtime](https://github.com/apply-the/canon/blob/0.72.6/tech-docs/images/canon-banner.jpg?raw=true)


**The governance runtime for AI-assisted engineering.** Keep AI agents bounded, inspectable, and safely restricted to approved work zones.
Expand Down
2 changes: 1 addition & 1 deletion docs/roadmap/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ The goal of Canon is to transform AI-assisted engineering activity into a proces
## <i class="fa-solid fa-rocket"></i> Upcoming Features & Topics

### Current 0.68.0
- **Systematic Debugging Mode**: Canon 0.69.0 introduces the systematic debugging mode, establishing a green-zone workflow with a five-phase verification gate (context, reproduction, root-cause, fix-decision, and verification) for tracking codebase incidents.
- **Systematic Debugging Mode**: Canon 0.72.6 introduces the systematic debugging mode, establishing a green-zone workflow with a five-phase verification gate (context, reproduction, root-cause, fix-decision, and verification) for tracking codebase incidents.

### Recently Landed
- **Assistant Surface Expansion**: `canon init` now supports Cursor and Antigravity alongside the existing assistant targets, while repository-level assistant package metadata remains aligned with the current release.
Expand Down
2 changes: 1 addition & 1 deletion roadmap/Next - forward-roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Where Canon Is Today

Canon 0.64.0 governs packet-based workflows across 18 first-class modes
Canon 0.72.6 governs packet-based workflows across 18 first-class modes
covering the full lifecycle from discovery through operations. What the runtime
does well: bounded packet authoring, evidence-linked approval, fail-closed
validation surfaces, and release-aware contract publication.
Expand Down
2 changes: 1 addition & 1 deletion specs/005-cli-release-ux/validation-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ Planned validation for this feature covers:
| Surface | Expected Version | Observed | Result | Notes |
| --- | --- | --- | --- | --- |
| `Cargo.toml` workspace version | `0.5.0` | `0.5.0` | PASS | verified during structural validation and local dry run metadata render |
| Rendered release notes | `0.5.0` | `Canon 0.5.0` and `Tag: v0.5.0` | PASS | rendered into `.canon/tmp/release-dry-run/release-notes.md` |
| Rendered release notes | `0.5.0` | `Canon 0.72.6` and `Tag: v0.5.0` | PASS | rendered into `.canon/tmp/release-dry-run/release-notes.md` |
| Archive names | `0.5.0` | all six release-surface files are versioned `0.5.0` | PASS | verified by `scripts/release/verify-release-surface.sh` |
| Checksum manifest name and entries | `0.5.0` | `canon-0.5.0-SHA256SUMS.txt` with one entry per archive | PASS | verified by `scripts/release/verify-release-surface.sh` |
| `canon --version` output per built binary | `0.5.0` | `canon 0.5.0` | PASS | native host binary executed locally; release workflow enforces per-artifact version sidecars |
Expand Down
2 changes: 1 addition & 1 deletion specs/039-authoring-packet-readiness/validation-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,4 @@

## Final Commit Message

- `feat: deliver authoring packet readiness as Canon 0.39.0`
- `feat: deliver authoring packet readiness as Canon 0.72.6`
Loading
Loading