schema_version: '0.7'

intent: >
  A CLI tool that validates NotarAI spec files against the JSON Schema
  and integrates with Claude Code via hooks so specs are automatically
  validated when written or edited. Distributed as a single static binary
  with no runtime dependencies, configured with a single `notarai init`
  command.

behaviors:
  - name: validate_single_file
    given: 'a path to a .spec.yaml file'
    then: 'validates it against the schema, prints PASS or FAIL with errors, exits 0 or 1'

  - name: validate_directory
    given: 'a path to a directory (or no args, defaulting to .notarai/)'
    then: 'recursively finds all .spec.yaml files, validates each; exits 1 if any fail; exits 0 with a stderr warning if no .spec.yaml files are found'

  - name: hook_validate_spec_file
    given: 'PostToolUse JSON on stdin with a file_path matching .notarai/**/*.spec.yaml'
    then: 'reads the file from disk, validates it, exits 1 with errors on stderr if invalid'

  - name: hook_ignore_non_spec
    given: 'PostToolUse JSON on stdin with a file_path that is not a spec file'
    then: 'exits 0 silently without validation'

  - name: hook_graceful_on_bad_input
    given: 'invalid JSON on stdin or a missing file'
    then: 'exits 0 silently without error'

  - name: init_fresh
    given: 'no existing .claude/settings.json'
    then: >
      creates .claude/settings.json with the PostToolUse validation hook (command: notarai
      hook validate); copies notarai-reconcile.md and notarai-bootstrap.md to
      .claude/commands/ (always overwriting); copies notarai.spec.json to
      .notarai/notarai.spec.json; writes .notarai/README.md from the bundled template with
      the current CLI version injected; creates or updates CLAUDE.md with the NotarAI section
      (@.notarai/README.md and @.notarai/notarai.spec.json); appends .notarai/.cache/ to
      .gitignore; writes .mcp.json registering notarai mcp as a local MCP server

  - name: validate_warns_stale_schema
    given: 'notarai validate runs and .notarai/notarai.spec.json exists but its $id differs from the bundled schema'
    then: 'prints a warning telling the user to run `notarai init` to update; validation continues normally'

  - name: mcp_schema_staleness_note
    given: 'MCP server initializes and .notarai/notarai.spec.json exists but its $id differs from the bundled schema'
    then: 'includes a schemaNote field in the initialize response containing the local and bundled $id values and a suggestion to run `notarai init`; parallels validate_warns_stale_schema for MCP clients'

  - name: init_idempotent
    given: 'notarai init has already been run'
    then: >
      detects existing hook and MCP entry and reports already configured; always overwrites
      .claude/commands/ (no existence check); always overwrites .notarai/notarai.spec.json and
      .notarai/README.md; replaces the ## NotarAI section in CLAUDE.md with the current
      template content; idempotently checks .gitignore and .mcp.json

  - name: init_replace_claude_section
    given: "CLAUDE.md already contains a '## NotarAI' heading"
    then: 'replaces the content between ## NotarAI and the next ## heading (or EOF) with the current template; does not skip or warn'

  - name: schema_bump
    given: 'notarai schema-bump is invoked in a project with an older schema version'
    then: >
      detects the old version from .notarai/notarai.spec.json, overwrites it with the bundled
      schema, updates the schema_version field in all .notarai/*.spec.yaml files, validates all
      updated specs, and prints a summary; exits 0 on success, 1 on validation failure

  - name: schema_version_archive
    given: 'a developer bumps the schema version in notarai.spec.json'
    then: >
      before editing notarai.spec.json, copies the current notarai.spec.json to
      schemas/<old_version>/spec.schema.json to create a frozen snapshot; the docs workflow
      then copies each schemas/<version>/spec.schema.json to its corresponding
      docs/book/schema/<version>/ path, so every version URL serves the correct schema;
      notarai.spec.json always reflects the current (newest) version only

  - name: cache_status
    given: 'notarai cache status is invoked'
    then: 'prints the DB file path, the number of cached entries, and the newest entry timestamp; creates an empty DB if none exists'

  - name: cache_clear
    given: 'notarai cache clear is invoked'
    then: 'deletes .notarai/.cache/notarai.db and exits 0; no-op if the file does not exist'

  - name: mcp_cache_filtering
    given: 'get_spec_diff is called with bypass_cache unset or false'
    then: 'files whose BLAKE3 hash matches the cache are listed in the skipped field and excluded from the diff; a cold or absent cache diffs all governed files as a safe fallback'

  - name: mcp_spec_aware_splitting
    given: 'get_spec_diff is called and the diff includes .notarai/*.spec.yaml files'
    then: 'spec files are returned in a separate spec_changes field with full file content (not diff hunks); the system spec is always included in system_spec when any spec changed (with full content if it did not change, or just the path reference if it appears in spec_changes); the diff field contains only non-spec, non-binary artifact diffs'

  - name: mcp_binary_file_handling
    given: 'get_spec_diff is called and governed files include binary files (detected by extension: .png, .jpg, .pptx, .pdf, etc., or by "Binary files ... differ" in the git diff output)'
    then: 'binary files are excluded from diff and listed by path in binary_changes; file_categories maps each changed artifact file path to its artifact category from the spec (e.g. "code", "docs", "assets"); both fields are always present in the response'

  - name: mcp_server
    given: 'notarai mcp is invoked and JSON-RPC 2.0 messages are sent on stdin'
    then: 'responds to initialize with serverInfo and 6 tool definitions (list_affected_specs, get_spec_diff, get_changed_artifacts, mark_reconciled, clear_cache, snapshot_state); dispatches tools/call to the appropriate implementation; exits 0 on stdin EOF; get_spec_diff accepts an optional exclude_patterns array of glob strings passed as git :(exclude) pathspecs to suppress noisy files from the diff output; cache-filtering semantics are described by the mcp_cache_filtering behavior; spec-aware splitting semantics are described by the mcp_spec_aware_splitting behavior; binary file handling semantics are described by the mcp_binary_file_handling behavior; schema-staleness semantics are described by the mcp_schema_staleness_note behavior'

  - name: state_show
    given: 'notarai state show is invoked'
    then: 'prints last reconciliation timestamp, git hash, branch, file count, and spec count; prints a message if no state file exists'

  - name: state_reset
    given: 'notarai state reset is invoked'
    then: 'deletes .notarai/reconciliation_state.json if it exists; no-op otherwise'

  - name: state_snapshot
    given: 'notarai state snapshot is invoked (or snapshot_state MCP tool is called)'
    then: 'reads all entries from the BLAKE3 cache, captures git HEAD and branch, writes .notarai/reconciliation_state.json with file_fingerprints, spec_fingerprints, and metadata including cli_version'

  - name: mcp_snapshot_state
    given: 'the snapshot_state MCP tool is called'
    then: 'snapshots cache + git state into reconciliation_state.json; returns summary with file/spec counts and git hash'

  - name: validate_v07_schema
    given: 'a spec file with schema_version 0.7 using any new optional block (compliance, feedback triggers, pipeline env/condition/on_failure/depends_on, design print/responsive/modes, content connections/duration/depends_on/evidence, state guard/action, variants_resolved) or extended enum values (domain: legal/education/infrastructure/research, output type: document/course/api/infrastructure/dataset/design-system/campaign/template, layout: paginated/canvas/timeline/tabbed, interaction trigger: schedule/external_signal/threshold/manual/lifecycle)'
    then: 'validates successfully; all new fields are optional and additive'

  - name: update_check
    given: 'notarai update --check is invoked'
    then: 'queries GitHub API for latest release, compares semver against current version, prints status (current version, latest version, whether update is available), exits 0'

  - name: update_install_github
    given: 'notarai update is invoked and install method is GithubRelease'
    then: 'downloads the platform-appropriate binary from GitHub Releases, atomically replaces the current executable, exits 0 on success or 1 on failure'

  - name: update_install_cargo
    given: 'notarai update is invoked and install method is CargoInstall'
    then: 'prints cargo install notarai instruction and exits 0'

  - name: update_install_dev
    given: 'notarai update is invoked and install method is DevBuild'
    then: 'prints cargo install --path . instruction and exits 0'

  - name: passive_update_hint
    given: 'notarai validate or notarai init completes'
    then: 'checks for updates using a global cache with 24h TTL and 5s timeout; if a newer version is available, prints a stderr hint; silently swallows all errors'

  - name: project_staleness_check
    given: 'notarai validate runs, or MCP server initializes, and .notarai/README.md exists with a version older than the running CLI (or is missing/unparseable)'
    then: 'prints a stderr hint (validate) or includes a projectNote field in the initialize response (MCP) suggesting notarai init to update project configs'

  - name: reconcile_parallel_subagents
    given: 'the notarai-reconcile or notarai-bootstrap slash commands are invoked'
    then: >
      the orchestrating agent calls get_spec_diff per affected spec to gather all diffs
      upfront; for small changesets (<=10 changed files and <=500 diff lines total) it
      analyzes all specs inline; for larger changesets it spawns one sub-agent per
      affected spec in parallel using the Agent tool, passing pre-gathered diff data so
      sub-agents do not call get_spec_diff again; sub-agents call get_changed_artifacts,
      read their governed files, and return a structured report; the orchestrating agent
      collects all reports before producing the final reconciliation output

constraints:
  - 'Exit codes: 0 for success/no-op, 1 for validation failure or errors'
  - 'CLI uses clap 4.x derive API for argument parsing and help generation'
  - 'The jsonschema validator is compiled once at process init via OnceLock (not per call); runtime schema reads (e.g. freshness checks) are permitted'
  - 'All bundled assets (schema, templates, commands) are embedded at compile time via include_str!'
  - 'The crate README should contain valid links'
  - 'If certain files are linked in the crate README, they must be bundled with the crate'
  - 'Batch database operations -- loops over DB calls must use a single transaction with a prepared statement, not implicit per-call transactions'
  - 'Order by cost -- when a function tries multiple strategies, the cheapest must come first (e.g. check a known path before scanning a directory)'
  - 'Binary crate visibility -- pub means cross-module only (no external consumers); test-only helpers must be gated with #[cfg(test)]'
  - 'Avoid needless allocations -- prefer borrowing (&str, &Value) over .to_string() / .cloned() when the owned value is not needed'
  - 'Avoid .unwrap() in production code -- use ?, .expect("reason"), or combinators; .unwrap() is acceptable in #[test] functions and #[cfg(test)] modules'
  - 'Schema version enum must include all supported versions for backward compatibility (currently 0.7, 0.6, and 0.5)'
  - 'Before bumping the schema version: copy notarai.spec.json to schemas/<old_version>/spec.schema.json first; the docs workflow reads each version from its snapshot, not from notarai.spec.json'
  - 'Update checks use a global cache with 24h TTL; network requests use a 5s timeout for version checks and 120s for binary downloads'

invariants:
  - 'A valid spec file must never be reported as FAIL'
  - 'An invalid spec file must never be reported as PASS'
  - 'The hook must never block non-spec file writes (must exit 0)'

decisions:
  - date: '2026-02-20'
    choice: 'Reconcile command named notarai-reconcile.md'
    rationale: "Avoids colliding with a user's own /reconcile command"
  - date: '2026-02-23'
    choice: 'Rust over Node.js/TypeScript for the CLI'
    rationale: 'Single static binary with no runtime dependencies enables cross-platform distribution via direct download instead of requiring Node.js; project has not been released yet so this is a clean replacement'
  - date: '2026-02-23'
    choice: 'clap 4.x derive API for CLI parsing'
    rationale: 'Replaces raw process.argv; clap provides structured subcommands, auto-generated help text, and typed argument parsing with minimal boilerplate'
  - date: '2026-02-23'
    choice: 'include_str! for bundled assets instead of runtime file reads'
    rationale: 'Embeds schema, templates, and command files at compile time so the binary is fully self-contained with no adjacent file dependencies'
  - date: '2026-02-23'
    choice: 'assert_cmd + tempfile for integration tests'
    rationale: 'assert_cmd runs the compiled binary as a subprocess matching real usage; tempfile provides isolated directories for init tests'
  - date: '2026-02-24'
    choice: 'BLAKE3+SQLite hash cache for reconciliation context reduction'
    rationale: 'Hashing governed files and storing results in a local SQLite DB (.notarai/.cache/notarai.db) lets the reconcile command skip doc artifacts that have not changed since the last run, keeping context window usage proportional to actual change volume rather than the full repo'
  - date: '2026-02-24'
    choice: 'Synchronous JSON-RPC 2.0 over stdio for the MCP server (no tokio)'
    rationale: 'The MCP stdio transport is a simple request/response loop -- no async I/O is needed. Synchronous BufRead line-by-line is sufficient and avoids pulling in a tokio runtime'
  - date: '2026-02-24'
    choice: 'rusqlite bundled feature for SQLite'
    rationale: 'Compiles SQLite from source as part of the crate, keeping the binary fully self-contained without requiring a system libsqlite3'
  - date: '2026-02-25'
    choice: 'get_spec_diff filters governed files via hash cache before diffing'
    rationale: 'Files already reconciled (per mark_reconciled) are skipped; only files whose on-disk hash differs from the cache are passed to git diff, making response size proportional to what actually changed since the last reconcile run'
  - date: '2026-02-25'
    choice: 'clear_cache added as a 5th MCP tool alongside bypass_cache on get_spec_diff'
    rationale: 'Cache clearing was CLI-only; exposing it via MCP lets the reconcile command reset state without dropping to the shell; bypass_cache provides a per-call escape hatch without destroying the cache'
  - date: '2026-02-26'
    choice: 'Remove cache update and cache changed CLI subcommands'
    rationale: 'These subcommands were only used to seed the MCP cache from the CLI, but the correct way to seed the cache is via mark_reconciled (which uses relative paths as keys, matching how get_spec_diff queries it). The CLI subcommands used absolute paths as keys, creating a separate namespace. Removing them eliminates the confusion and reduces CLI surface.'
  - date: '2026-02-26'
    choice: 'init always overwrites .claude/commands/ and replaces the CLAUDE.md NotarAI section'
    rationale: 'Previous behavior skipped existing commands and warned on CLAUDE.md drift, requiring manual intervention to keep installed commands in sync with the bundled version. Always overwriting and always replacing makes init idempotent without user involvement -- the section is small and always correct after a run.'
  - date: '2026-02-26'
    choice: 'Move schema copy from .claude/notarai.spec.json to .notarai/notarai.spec.json'
    rationale: '.notarai/ is the natural home for NotarAI config; the old .claude/ path mixed project config with Claude-specific settings. The new location is more predictable and survives .claude/ resets. schema-bump also writes to .notarai/notarai.spec.json for consistency.'

  - date: '2026-03-04'
    choice: 'Reconciliation state as committed JSON file (Tier 3) separate from SQLite cache'
    rationale: >
      The SQLite cache tracks per-file hashes for within-session skip-unchanged
      optimization. The state file snapshots the cache at reconciliation time plus
      git context, enabling cross-run delta detection and shared baselines across
      collaborators. Using BTreeMap ensures deterministic output for clean diffs.

  - date: '2026-03-04'
    choice: 'Schema 0.6 consolidates all enhancement phases into a single bump'
    rationale: >
      Four design phases (output/content, interactions/states, design/audience/variants,
      pipeline/feedback) are all additive optional fields with the same implementation
      pattern. A single version bump avoids four near-identical release cycles.

  - date: '2026-03-04'
    choice: 'Versioned schema snapshots in schemas/<version>/spec.schema.json; notarai.spec.json is always current'
    rationale: >
      The docs site serves each schema version at its canonical $id URL
      (notarai.dev/schema/<version>/spec.schema.json). notarai.spec.json can only
      represent one version at a time, so old versions must be frozen as static
      snapshots in the repo. The schemas/ directory holds these snapshots; the docs
      workflow copies each independently. Pre-bump procedure: copy notarai.spec.json
      to schemas/<old_version>/spec.schema.json before editing it.

  - date: '2026-03-05'
    choice: 'ureq (blocking) for HTTP in update command'
    rationale: 'Project is sync throughout with no async runtime; ureq is a minimal blocking HTTP client that fits the existing architecture without pulling in tokio'

  - date: '2026-03-05'
    choice: 'Project staleness detected via .notarai/README.md header version'
    rationale: 'The README is already stamped with the CLI version at init time. Parsing the header is simpler than adding a new metadata file, and if parsing fails, suggesting notarai init is the correct recovery action anyway.'

  - date: '2026-03-05'
    choice: 'MCP get_spec_diff returns file_categories and binary_changes'
    rationale: >
      Non-software artifacts include binary files (images, PPTX, PDF) that
      produce unusable unified diffs. binary_changes lists them separately;
      file_categories maps each changed file to its spec artifact category
      for category-aware reconciliation.

artifacts:
  code:
    - path: 'src/main.rs'
      role: 'CLI entry point -- clap definition and command dispatch'
    - path: 'src/commands/*.rs'
      role: 'Command implementations (validate, init, hook_validate, cache, mcp, schema_bump, state, update)'
    - path: 'src/core/*.rs'
      role: 'Core library (schema embedding, YAML parsing, jsonschema validation, hash cache, MCP tool implementations, update logic)'
  configs:
    - path: 'Cargo.toml'
      role: 'Rust package manifest -- dependencies, build metadata, and binary target'
    - path: 'notarai.spec.json'
      role: 'JSON Schema for the current schema version; always reflects the newest version'
    - path: 'schemas/*/spec.schema.json'
      role: 'Frozen snapshots of old schema versions; copied by the docs workflow to serve each version at its canonical $id URL'
    - path: 'commands/notarai-reconcile.md'
      role: 'Bundled Claude Code slash command copied by init'
    - path: 'commands/notarai-bootstrap.md'
      role: 'Bundled Claude Code slash command for bootstrap interview, copied by init'
    - path: 'templates/notarai-readme.md'
      role: 'Bundled .notarai/README.md template written by notarai init to target projects'
    - path: '.claude/rules/rust.md'
      role: 'Claude rule equivalent -- proactive enforcement of Rust coding constraints during generation'
    - path: '.claude/rules/testing.md'
      role: 'Claude rule equivalent -- test workflow conventions for AI-assisted development'
  tests:
    - path: 'tests/**/*.rs'
      role: 'Integration tests (assert_cmd CLI tests, tempdir-based init tests)'
