Metadata-Version: 2.4
Name: csd-intent
Version: 1.0.0
Summary: Cross-runtime audit of CSD intent specifications, test markers and claim review freshness.
Author: Rafael Pires
License: MIT
Project-URL: Homepage, https://github.com/PSA-Department-of-Engineering/csd-library
Project-URL: Documentation, https://github.com/PSA-Department-of-Engineering/csd-library#readme
Project-URL: CSD methodology, https://github.com/PSA-Department-of-Engineering/cognitive-software-delivery
Keywords: csd,intent,testing,specification,audit,cross-language
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Testing
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: PyYAML>=6.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: ruff>=0.1.0; extra == "dev"
Requires-Dist: mypy>=1.0; extra == "dev"
Dynamic: license-file

# csd-intent

Cross-runtime audit tool for CSD intent specifications. Validates `intent.yaml`
against [CSD-INTENT-01](https://github.com/PSA-Department-of-Engineering/cognitive-software-delivery) and confirms every
runner-bound claim is attested by at least one test marker across **any** test runner -
pytest, vitest, Playwright, Jest, or anything that uses the standard `intent()`
/ `@intent()` marker shape.

This tool is **standalone**. It does not run inside your project's test suite; it
is invoked separately (CLI, CI step, or pre-commit hook).

## Why it exists

`pytest-intent`, `vitest-intent`, and `playwright-intent` provide the test-side
marker helpers - they let a test declare which intent claim it attests. They
deliberately know nothing about other runtimes, schema validation, or whether a
claim is unattested.

`csd-intent` owns the cross-runtime auditing: it walks every test file in the
project (Python via AST, TS/JS via regex), reads `intent.yaml`, and answers:

1. **Schema** - does every claim match CSD-INTENT-01?
2. **Orphan** - does every test marker reference a real claim?
3. **Coverage** - does every active claim have an attesting test or a current `llm` review?

## Install

```bash
# From PyPI:
pip install csd-intent

# Local dev against a csd-library checkout:
pip install -e path/to/csd-library/csd-intent
```

## Use

```bash
# Audit the current directory (expects intent.yaml at the root):
csd-intent

# Audit a specific project:
csd-intent /path/to/project

# Audit but tolerate unattested claims (the "intent before test" workflow):
csd-intent --fail-on schema

# Constrain the scan to specific directories:
csd-intent --tests-dir backend/tests --tests-dir frontend/src --tests-dir e2e

# Quiet summary only:
csd-intent --quiet

# The same audit as one JSON object on stdout, for a script or a dashboard:
csd-intent --json
```

Exit code is `0` on a clean audit, `1` when any violation falls into the
configured `--fail-on` set (`any` by default).

## Output

```
intent.yaml (/path/to/project/intent.yaml): 24 claims, 2 violation(s).

UNATTESTED (2):
  [unattested] INT-SB-018: no @intent / intent() marker references this claim
  [unattested] INT-SB-029: no @intent / intent() marker references this claim
```

### JSON output

`--json` prints the same audit as one JSON object on stdout and nothing else. The
exit code still follows `--fail-on`, so one run both records the summary and gates
on it.

```json
{
  "generated_at": "2026-09-22T09:41:07Z",
  "claims": 24,
  "attested": 20,
  "unattested": 2,
  "draft": 1,
  "active": 22,
  "deprecated": 1,
  "violations": 2,
  "requirements_traced": 17,
  "clean": false,
  "projects": [
    {
      "intent_path": "/path/to/project/intent.yaml",
      "claims": 24,
      "attested": 20,
      "unattested": 2,
      "draft": 1,
      "active": 22,
      "deprecated": 1,
      "violations": 2,
      "requirements_traced": 17,
      "clean": false,
      "claims_by_id": {
        "INT-SB-001": {
          "status": "active",
          "attested": true,
          "scope": "unit",
          "derived_from": ["REQ-003"]
        }
      }
    }
  ]
}
```

| Key | Meaning |
| --- | ------- |
| `claims` | claims declared in `intent.yaml` |
| `attested` | claims with a valid marker or a current PASS review |
| `unattested` | claims the report lists under `UNATTESTED`: active and lacking a valid attestation |
| `draft`, `active`, `deprecated` | claims by status |
| `violations` | violations of every kind: the report's "N violation(s)" |
| `requirements_traced` | distinct ids across every claim's `derived_from` annotation |
| `clean` | `true` when there is no violation of any kind |
| `generated_at` | when the audit ran, ISO-8601 UTC |
| `projects` | one entry per audited project, root first, with its `intent_path`, its own counts, and its `claims_by_id` |
| `claims_by_id` | in each `projects[]` entry: that project's claims by id, each with `status`, `attested`, `scope`, and `derived_from` |

Unattested draft and deprecated claims are excluded from the coverage gate, so
those two counts do not sum to `claims`. A tree of nested projects sums its counts into
the top-level keys and keeps `requirements_traced` distinct across projects; a claim id
is scoped to the project that declares it, so an id two projects both declare is
reported under each.

## What it scans

- **Python**: any file matching `test_*.py` or `*_test.py`, walked via AST for
  `@intent("INT-...")` decorators on functions starting with `test_`.
- **JS/TS**: any file matching `*.test.{ts,tsx,js,jsx,mts,cts}` or
  `*.spec.{ts,...}`, scanned by regex for `intent('INT-...', 'name', fn)` calls
  (single-ID and array-of-IDs forms both supported).
- **Excluded directories**: `node_modules`, `.venv`, `venv`, `dist`, `build`,
  `.git`, `__pycache__`, `.pytest_cache`, `.mypy_cache`, `.ruff_cache`, `.tox`.

## CSD-INTENT-01 conformance

The schema check validates each claim against the canonical fields per
[CSD-INTENT-01 §4.1](https://github.com/PSA-Department-of-Engineering/cognitive-software-delivery):

```yaml
INT-NNN:
  version: 1.0.0
  status: active                # draft | active | deprecated
  statement: "..."              # >= 10 chars, RFC 2119 language
  rationale: "..."              # optional but recommended
  test:
    scope: integration          # unit | integration | e2e | llm
    component: SwitchFlow
    type: behavior              # invariant | behavior | contract
  criticality: critical         # critical | high | medium | low
```

The scope can also be declared as a top-level `scope` field.

### Claim review

An `llm` claim carries only its latest verdict and the digest of the claim it judged:

```yaml
review:
  claim: 0312c5160e2b65b48a48c7a42f6af83d3d2382a8bc6cc919db79fd6e37993401
  verdict: PASS
```

`claim` is the lowercase SHA-256 of the claim's meaning: every field except
`review` and `status`, as canonical JSON over the parsed values (sorted keys, no
spaces, dates in ISO 8601), so YAML layout never changes it. A claim holding a
value with no canonical form (a non-string mapping key such as an unquoted `on:`,
a set, or a cycle) has no digest, and its review is unresolvable.
A new review replaces this mapping; Git preserves its history. The commit the
reviewer read, the reasons and the findings belong to the reviewer's PR or
delivery record.

An active `llm` claim requires a current PASS: the recorded digest equals the
claim's digest as it reads. Editing the claim makes the review stale; nothing
outside the claim does, so recording a review and activating a claim keep it
current, and so does any change to other files or other claims. The audit reads
no Git history, so a review stays current through rebase, squash and merge
commits and in a shallow clone. Missing, failed, stale and malformed reviews are
unattested; markers on `llm` claims are mismarked. CI checks the record without
invoking a model.

The audit's report prints the digest a current record names on every active
claim whose review is unreviewed or stale, and `--json` carries it for every `llm`
claim, drafts included. The public `read_review(claim)` and `claim_digest(claim)` helpers read the
same values, and JSON summaries expose `state`, a derived `reason`, `verdict` and
`claim` (that digest) for each `llm` claim.

### Duplicate keys are refused

`intent.yaml` is parsed with a loader that raises on a repeated key rather than
resolving it last-wins. A claim id reused by accident would otherwise delete the
earlier claim before any check could see it - and every marker written for that
claim would silently start attesting the survivor, with the audit still printing
`CLEAN`. The duplicate is reported as a schema violation naming both lines:

```
SCHEMA (1):
  [schema] intent.yaml: duplicate key `INT-GATE-006` (first declared on line 42,
  declared again on line 187); YAML would silently keep only the last one
```

Merge keys still work: `<<: *base` plus an explicit override is not a duplicate.

## License

MIT.
