specular - deterministic spec-driven development

Verify a repository against a machine-readable JSON spec. specular reads the spec,
checks repository state, and reports one evidence object per item.

USAGE
  specular lint   <spec.json>   lint the spec file for correctness
  specular verify <spec.json>   check the repository against the spec
  specular --help               print this text (also: -h, help)

EXIT CODES
  0  spec valid (lint) / repository conforms (verify)
  1  repository does not conform (verify only)
  2  spec invalid, verifier error, protocol error, timeout, or runtime error
  No flags exist. Output is always JSON.

THREE WAYS TO USE A SPEC
  1. Predefined category, built-in verifier.

     Use `tree` or `content`. Write requirements only. Specular already knows
     how to verify these categories.

       "requirements": {
         "tree": { "required": ["src/lib.rs"], "forbidden": ["tests/**"] },
         "content": [
           { "files": ["**/*.rs"], "forbidden": ["#[test]"] }
         ]
       }

  2. Predefined category, no built-in verifier.

     Use `dependencies`, `exports`, or `enumerations`. These categories have
     fixed fields, but Specular needs a verifier command that you write.
     Specular runs that command and checks that it emits exactly one evidence
     line for each item in the block.

       "verifiers": { "dependencies": ["scripts/verify_deps.py"] },
       "requirements": {
         "dependencies": [
           { "manifests": ["Cargo.toml"], "required": ["serde"] }
         ]
       }

  3. Opaque custom checks.

     Use `custom` when no predefined category fits. Each entry can contain any
     JSON object fields you need. Specular only checks that each entry is an
     object. Your verifier interprets those objects and emits evidence.

       "verifiers": { "custom": ["scripts/verify_custom.py"] },
       "requirements": {
         "custom": [
           { "check": "version-sync", "files": ["README.md", "Cargo.toml"] }
         ]
       }

  Predefined categories have predefined fields. Unknown fields fail lint.
  Custom entries are opaque objects. Put data there only when no predefined
  category describes the requirement.

SPEC FORMAT
  Strict JSON, no comments. This example uses every category; copy and delete
  what you do not need. Any category may be omitted. List fields default to [].

  {
    "version": 1,
    "verifiers": {
      "dependencies": ["scripts/verify_deps.py"],
      "exports": ["scripts/verify_exports.py"],
      "enumerations": ["scripts/verify_enums.py"],
      "custom": ["scripts/verify_custom.py"]
    },
    "requirements": {
      "tree": {
        "reason": "plan: repository layout",
        "required": ["src/lib.rs", "src/main.rs"],
        "forbidden": ["tests/**"]
      },
      "content": [
        {
          "files": ["**/*.rs"],
          "required": ["// SPDX"],
          "forbidden": ["#[test]"],
          "reason": "plan: licensing, test policy"
        },
        {
          "files": ["docs/*.txt"],
          "exists": ["INVOICE"]
        }
      ],
      "dependencies": [
        {
          "manifests": ["Cargo.toml"],
          "required": ["serde"],
          "forbidden": ["openssl", "guardrail*", "g3*"]
        }
      ],
      "exports": [
        {
          "package": "specular",
          "required": ["Spec", "lint", "verify"]
        }
      ],
      "enumerations": [
        {
          "name": "Status",
          "values": ["Pass", "Fail"]
        }
      ],
      "custom": [
        {
          "check": "version-sync",
          "files": ["README.md", "Cargo.toml"],
          "reason": "plan: release process"
        }
      ]
    }
  }

FIELDS
  tree          reason, required, exists, forbidden
  content       files, reason, required, exists, forbidden
  dependencies  manifests, reason, required, exists, forbidden
  exports       package, reason, required, exists, forbidden
  enumerations  name, reason, values
  custom        array of objects; specular does not interpret their keys

SEMANTICS
  required   each item is present in every matched place
  exists     each item is present in at least one matched place
  forbidden  each item is present in no matched place

  required and exists items are exact strings.
  forbidden items may be globs in tree and dependencies.
  content items are fixed substrings for all quantifiers.
  enumerations are exact sets: the named set's values equal values.
  Zero matched places plus non-empty required or exists fails.
  exists is rejected by lint on tree and exports because each has one place.

VERIFIERS
  Every category is judged by exactly one verifier.

    builtin when no verifier is declared:  tree, content
    no builtin:                            dependencies, exports,
                                           enumerations, custom

  A verifier is declared in the top-level verifiers map:

    "verifiers": { "dependencies": ["scripts/verify_deps.py"] }

  Typed categories run once per block:

    <command...> <spec.json> <category> <blockIndex>

  The executable reads requirements.<category>[blockIndex] from the spec and
  prints one JSON object per item in that block. Required keys: item, status.

    {"item":"serde","status":"pass"}
    {"item":"guardrail*","status":"fail","message":"guardrail3 declared","path":"Cargo.toml"}

  Custom runs once:

    <command...> <spec.json> custom

  The executable reads requirements.custom and prints any evidence objects it
  owns. Required key: status. All other keys are copied into the report.

    {"check":"version-sync","status":"pass"}
    {"check":"version-sync","status":"fail","message":"README and Cargo.toml differ"}

  Script exit code signals health only. Nonzero exit, invalid JSON, missing item,
  duplicate item, unknown item, zero custom lines, or a 60 second timeout is a
  runtime error with exit 2. Pass/fail verdicts travel only in evidence lines.

COMPLETE TYPED VERIFIER
  This Python example accepts the protocol and returns pass evidence for every
  item in one typed block.

  #!/usr/bin/env python3
  import json
  import sys
  from pathlib import Path

  spec_path = Path(sys.argv[1])
  category = sys.argv[2]
  block_index = int(sys.argv[3])

  spec = json.loads(spec_path.read_text())
  block = spec["requirements"][category][block_index]

  for item in block.get("required", []):
      print(json.dumps({"item": item, "status": "pass"}))
  for item in block.get("exists", []):
      print(json.dumps({"item": item, "status": "pass"}))
  for item in block.get("forbidden", []):
      print(json.dumps({"item": item, "status": "pass"}))

COMPLETE CUSTOM VERIFIER
  This Python example reads opaque custom entries and returns one pass evidence
  line per entry.

  #!/usr/bin/env python3
  import json
  import sys
  from pathlib import Path

  spec_path = Path(sys.argv[1])
  spec = json.loads(spec_path.read_text())

  for entry in spec["requirements"].get("custom", []):
      print(json.dumps({
          "check": entry.get("check", "custom"),
          "status": "pass"
      }))

LINT ERRORS
  JSON_SCHEMA               wrong shape, unknown field, wrong type, or bad version
  VACUOUS_SPEC              no required, exists, or values item anywhere
  CATEGORY_HAS_NO_VERIFIER  non-builtin category has requirements and no verifier
  UNKNOWN_CATEGORY          verifiers key is not a category
  PATH_RULE / GLOB          bad path, or a glob that will not compile
  DUPLICATE_TARGET          two blocks in one category have the same target
  DUPLICATE_ITEM            same item appears twice in one block
  CONTRADICTION             same item is required and forbidden in one block
  REDUNDANT                 same item is both required and exists in one block
  ITEM_FORMAT               required/exists item is empty, untrimmed, or a glob
  EXISTS_SINGLE_PLACE       exists used on tree or exports
  DEAD_VERIFIER             verifier declared for an empty category
  VERIFIER_COMMAND_EMPTY    verifier command array is empty
  CUSTOM_SHAPE              custom entry is not an object

WORKFLOW
  1. Write the plan in prose.
  2. Extract a JSON spec for checkable items.
  3. Write verifier scripts for categories without a builtin.
  4. Run specular verify before coding and confirm it fails where expected.
  5. Build until specular verify exits 0.
  6. Spot-check by hand; the spec is not exhaustive.

REPORT
  lint:

    {"result":"pass"}

    {"result":"fail","violations":[{"code":"VACUOUS_SPEC","message":"..."}]}

  verify:

    {
      "specular_version": "0.2.0",
      "spec": {"path": "spec.json", "sha256": "..."},
      "verifier_files": [{"path": "scripts/verify_deps.py", "sha256": "..."}],
      "git": [{"path": "spec.json", "state": "clean"}],
      "evidence": [
        {
          "category": "tree",
          "polarity": "required",
          "item": "src/main.rs",
          "source": "builtin",
          "status": "fail",
          "message": "missing required path: src/main.rs"
        },
        {
          "category": "dependencies",
          "target": ["Cargo.toml"],
          "item": "guardrail*",
          "source": "custom",
          "status": "pass"
        },
        {
          "category": "custom",
          "check": "version-sync",
          "source": "custom",
          "status": "fail",
          "message": "README and Cargo.toml differ"
        }
      ],
      "conforms": false
    }
