IntentGuard

Implementation status

Current measured stateimplementation statusml

Evidence

Status vocabulary comes from AGENTS.md: Measured is an evidence qualifier and never substitutes for implementation.

Status uses the repository vocabulary in AGENTS.md. Measured is an evidence qualifier and never substitutes for implementation.

UmbrellaCapabilityStatusEvidence
U01Project foundation and reproducibilityImplementedGate A local checks pass; CPU validation passed for main commit 1495776 (run 30853223606)
U02BANKING77 data contractImplementedmake data validated PolyAI/banking77@1fb62b1bb4635df59a8e1b2f2bc5e0643b2856c8: source 10,003/3,080, derived 8,502/1,501/3,080, 77 labels, deterministic fingerprints, and local provenance.
U03TF-IDF logistic-regression baselineImplementedmake baseline produced run intentguard-baseline-1fb62b1bb463-059ee4b12214 on PolyAI/banking77@1fb62b1bb4635df59a8e1b2f2bc5e0643b2856c8, measured from the reloaded artifact: test accuracy 0.8653, macro-F1 0.8654, 3,080 examples across 77 classes, seed 42. The test read here is documented divergence D1: REQUIREMENTS.md AC-002 requires make baseline to write test macro-F1 and outranks ARCHITECTURE.md.
U04DistilBERT training and immutable artifactImplementedmake train sealed run intentguard-distilbert-1fb62b1bb463-88e538757339 from distilbert/distilbert-base-uncased@12040accade4e8a0f71eabdb258fecc2e7e948be: measured validation accuracy 0.7162, macro-F1 0.6541, and threshold 0.1684 selected from validation confidences only at coverage 0.7015, accepted accuracy 0.8395, selective risk 0.1605. No test split was read, so this umbrella has no test metric. CPU-only by decision D10; NFR-001's GPU clause stays unevidenced.
U05Comparative evaluation and selective predictionImplementedmake evaluate compares both sealed artifacts on the untouched test split as run intentguard-evaluation-1fb62b1bb463-55796a53ca3e, applying the persisted validation threshold 0.16841767053420467 without reselecting it. Measured on 3,080 test examples: baseline accuracy 0.8653 / macro-F1 0.8654, transformer accuracy 0.6955 / macro-F1 0.6620, macro-F1 delta -0.2034, verdict baseline_better. Selective prediction at that threshold: baseline coverage 0.7500, accepted accuracy 0.9333, selective risk 0.0667; transformer coverage 0.6799, accepted accuracy 0.8161, selective risk 0.1839. Calibration Measured over 15 fixed equal-width bins: baseline ECE 0.4883 (mean confidence 0.3770 against accuracy 0.8653, 15/15 bins occupied), transformer ECE 0.4697 (mean confidence 0.2258 against accuracy 0.6955, 9/15 bins occupied); both are underconfident. Full risk/coverage curves are written for both models (3,074 and 3,081 points). The curated unsupported-request fixture is Measured in the same run: 12 hand-written requests spanning all six declared categories, decided once each at that same persisted threshold, all 12 abstaining, written separately to unsupported_fixture.json and unsupported_fixture.md. That rate is reported only beside its in-distribution contrast — mean confidence 0.0503 on the fixture against 0.2258 on test, with the same model abstaining on 0.3201 of test — because a total rate alone is indistinguishable from a model that abstains on everything. No accuracy is reported for the fixture: no BANKING77 intent is correct for any row, so accuracy is undefined there. Single-request latency Measured in this run on the recorded CPU over 200 real test texts after 20 discarded warm-up requests: baseline p50 0.55 ms / p95 0.59 ms, transformer p50 8.64 ms / p95 11.08 ms — descriptive for that machine and that run, not a service-level claim, and the only non-reproducing section of the report. Repeated runs of this identical configuration have produced transformer p50 values between roughly 8.6 ms and 10.7 ms on this machine, so read these two figures as one sample from that spread; the run ID covers the sampling protocol and never the durations, which is why re-running rewrites this directory in place with different timings and identical metrics.
U06FastAPI inference and real-artifact demoImplementedAll three subtasks are complete: S06.1 typed schemas, stable errors, request IDs, and text-free logging; S06.2 the sealed-artifact predictor behind /health and /v1/predict; S06.3 make serve and make demo wired to that path. make demo Measured over a real socket against intentguard-distilbert-1fb62b1bb463-88e538757339: /health returned ready, device cpu, label_count 77; the in-domain request "How do I activate my new card?" was accepted as activate_my_card at confidence 0.5300218231428692; the curated unsupported row unsupported-001 abstained at 0.04127836201977225 with intent null. Both decisions used the persisted validation threshold 0.16841767053420467, unchanged from the sealed bundle and reselected nowhere. The abstained confidence agrees with the E05 evaluation's 0.04127837051435484 for the same row to seven decimal places, so serving and evaluation are reading the same weights and the same threshold. Startup logged service_start then artifact_loaded; the artifact was verified before the port was bound. make lint clean (Ruff, mypy strict over 50 files, 10/10 foundation checks) and make test passed in both environment states; the suite counts are recorded once, for the current tree, in the reconciliation section below rather than per row, because a count quoted against an older tree reads as a regression when the only thing that changed was the number of tests.
U07Full validation and acceptance gateImplementedBoth subtasks are complete and have executed on CPU. S07.1: scripts/validate_acceptance.py audits all 42 primary identifiers, make acceptance and one CI step invoke it, and its unit tests pass in both environment states. S07.2: the audit, this document's corrections, and the enumerated verdict below. CPU execution is Measured by run 31104632401 on commit 4cfc8ec: the workflow and its foundation job concluded success, make lint passed, make test measured 598 passed / 39 skipped in 20.74s, and the acceptance step executed the audit on a clean runner, classifying 44 rows over 42 identifiers and reaching the same causes a fully-evidenced local run reaches. That step exited non-zero, which was the audit's correct verdict while U08 was Planned — the gate reporting an incomplete repository as incomplete. That exit was masked by continue-on-error: true at the time, and the masking was never read as a passing strict-MVP verdict; it has since been removed, as the enforced gate below records. U07's completion is its orchestration and its honest audit, not the value of the verdict those produce. The reconciled audit has itself since executed on a clean runner: run 31109003496 on 17c56d9 measured 611 passed / 39 skipped and printed the three-cause FAIL, so the code that removed the circular rule is CPU-evidenced rather than only the version that preceded it.
U08Documentation and recruiter-ready deliveryImplementedAll three subtasks are complete. S08.1: README.md restructured as a reviewer entry point — intended use, a five-command "Review in five minutes" path, the lifecycle in order, a measured-results table, the confidence and fixture caveats, and links out to operations, status, limitations, and the authoritative specification instead of restating them. S08.2: docs/OPERATIONS.md documents make acceptance, both evidence-root variables and the --print-only/--output audit safety rule, the status vocabulary, a fallback-consequence table, and five troubleshooting stop points. S08.3: the timed walkthrough below executed against the real sealed artifact. Every comparison.json-sourced metric published in README.md was diffed against run intentguard-evaluation-1fb62b1bb463-55796a53ca3e at the precision shown — 19 figures, 0 mismatches — rather than copied from a prior document; the fixture metrics in the same section (12/12 abstained, mean confidence 0.0503, and the 0.3201 in-distribution abstention contrast) were checked separately against unsupported_fixture.json. No new document was added: a separate model card would have duplicated this file and docs/LIMITATIONS.md, which is an explicit non-goal of E08.

Strict MVP is complete: every MUST capability U01–U08 is Implemented and no applicable claim is left unmeasured. The pinned dataset contract, both models' test metrics, the transformer's validation metrics and selected threshold, the test-split comparison and abstention behaviour, the curated unsupported-request check, both models' calibration and single-request CPU latency, and the served accept/abstain behaviour of the sealed transformer artifact over HTTP are Measured from executed local runs. No GPU claim is evidenced.

The five-minute walkthrough is Measured#

Executed on this tree against the E05 artifact root, following README.md's "Review in five minutes" section in order. Elapsed command time, measured with a wall clock:

StageCommandResultElapsed
1make setupexit 0under 1 s on a warm environment
2make lintexit 0; Ruff clean, mypy strict 52 files, foundation 10/103 s
3make testexit 0; 650 passed26 s
4make demoexit 0; accept and abstain over HTTP9 s
5validate_acceptance.py --print-onlyexit 1 at that moment, correctly, because U08 was still Planned when the walkthrough ranunder 1 s

Total command time 38 s, well inside the five-minute boundary, which excludes prerequisite installation. make setup is quoted against a warm environment: a first-time clone additionally pays the locked-environment install, measured separately at roughly 25 s on this machine including interpreter provisioning. Stage 5's non-zero exit is recorded rather than smoothed over — the walkthrough deliberately preceded U08's promotion, so the gate was still reporting the truth about an unfinished capability. The re-audit after promotion is below.

The demo's observed output, against bundle intentguard-distilbert-1fb62b1bb463-88e538757339: /health returned ready on device cpu with label_count 77; "How do I activate my new card?" was accepted as activate_my_card at confidence 0.5300218231428692; the curated row unsupported-001 abstained at 0.04127836201977225 with intent null. Both used the persisted threshold 0.16841767053420467. That abstained confidence agrees with the E05 evaluation's 0.04127837051435484 for the same row to seven decimal places, which is what shows serving and evaluation read the same weights and the same threshold — a matching model_version string alone would not.

U06 is Implemented, which closes the serving half of the MUST hierarchy: the typed FastAPI boundary loads one sealed artifact, applies the persisted validation threshold, and make demo demonstrates one accept and one abstain over a real socket rather than through a test client or a deterministic stand-in.

Suite counts reconcile across three environments#

An earlier revision of this document recorded make test as 585 passed with the artifact present and 563 passed / 22 skipped without it. Those figures do not reproduce on any tree and are corrected here. The collected total is what changed: this row's counts were carried forward from the tree that measured them and were never re-measured as tests were added.

Each row names the tree it was measured on, because a count carried across trees is exactly how the 585/563 figures went stale. The current tree collects 650, adding 13 audit tests to the 637 collected at 4cfc8ec:

EnvironmentTreeResultSkip gate
Local, INTENTGUARD_ARTIFACT_ROOT set to the E05 artifact rootcurrent (650 collected)650 passednone
Local, variable unsetcurrent (650 collected)628 passed, 22 skippedall 22 in tests/integration/test_api.py, gated on a sealed intentguard-distilbert bundle
GitHub Actions, clean runner4cfc8ec (637 collected)598 passed, 39 skippedthe same 22, plus 17 in tests/integration/test_training_smoke.py gated on the distilbert-base-uncased Hugging Face cache
GitHub Actions, clean runner17c56d9 (650 collected)611 passed, 39 skippedthe same two gates, unchanged
GitHub Actions, clean runnerd31c333 (650 collected)611 passed, 39 skippedthe same two gates, unchanged

Within one tree the numbers differ only by which gate is unmet, and each gate's skip message names the command that satisfies it. The CI row was once an arithmetic expectation of 22 + 17 = 39 skips; run 31104632401 measured exactly that against 4cfc8ec, so it is a measurement and must not be re-labelled an expectation.

The 17c56d9 row was likewise recorded as an expectation first: the two gates are unchanged, so 650 collected minus 39 skips predicted 611 passed. Run 31109003496 measured exactly 611 passed, 39 skipped, 1 warning in 21.10s, so it is now a measurement.

The d31c333 row is a measurement from the outset: run 31117901793 attempt 2 measured 611 passed, 39 skipped, 1 warning in 19.14s. It matches 17c56d9's counts because E08 changed no test count — the two documentation-only commits between those trees added no test and removed none, and the one assertion narrowed in tests/unit/test_validate_acceptance.py left the collected total at 650. Every CI row is pinned to its tree, because a count carried across trees is exactly how the 585/563 figures went stale.

Strict-MVP verdict: PASS#

Issued by make acceptance, which exits zero only when no cause remains. Measured on this tree after the walkthrough above: the audit classified 44 rows over 42 primary identifiers — NFR-001 contributes three rows, one per clause — and reported 43 passed, 1 not evidenced, 0 blocked, with an empty cause list.

The order matters and is recorded deliberately. The same audit on the same tree reported FAIL — 3 cause(s) before U08 was promoted, and PASS after. The three causes were U08 Planned, T-008 blocked, and NFR-010 blocked — the latter two solely because their owning capability was Planned, since README.md and docs/OPERATIONS.md already existed. That is precisely why the promotion came last. U08's declared status is the audit's only lever over those two rows, so editing it first would have produced a passing verdict by declaration; the walkthrough, the demo against the real sealed artifact, and the 19-figure metric diff are the evidence the promotion rests on.

Causes 2 and 3 were the audit's own correction to an earlier defect in it. Its first version passed any identifier whose named file was present on disk, consulting the owning capability's status only when the file was missing. That reported NFR-010 as passed while U08 was Planned — a Planned capability rendering as satisfied, which is precisely the substitution this gate exists to prevent. The status decides, and presence alone never passes.

The verdict is environment-independent, and CPU CI has now confirmed it#

Three audit runs on this tree exit 0 and report PASS — two local, one on a clean CPU runner:

EnvironmentRowsVerdict
Fully evidenced (INTENTGUARD_ARTIFACT_ROOT and INTENTGUARD_REPORT_ROOT at the E05 roots)43 passed, 1 not evidenced, 0 blockedPASS, exit 0
Degraded (neither variable set)23 passed, 21 not evidenced, 0 blockedPASS, exit 0, with 14 environmental gaps reported separately
CPU CI, degraded by construction — run 31117901793 attempt 2 on commit d31c33323 passed, 21 not evidenced, 0 blockedPASS, exit 0, with the same 14 environmental gaps

The degraded run reaches the same verdict while routing 21 artifact-backed rows to environmental gaps, which is the property the audit was built to have. CI's counts are identical to the local degraded run's, row for row, which demonstrates that property in a second environment rather than asserting it.

That CI run is the first clean-runner audit of a tree where U08 is Implemented. Its foundation job acquired a hosted runner, executed all seven steps, and measured make lint clean — Ruff, mypy strict over 52 files, 10/10 foundation checks — make test at 611 passed / 39 skipped in 19.14s, and the acceptance audit at the verdict above. The audit recipe exited 0 on its own merits: the step logs contain no ##[error], no make: *** [Makefile:38: acceptance] Error 1, and no ##[error]Process completed with exit code 2. For the first time in this repository the step's green conclusion and its true exit status agree.

Attempt 1 of that same run is recorded here rather than discarded, because omitting it would overstate the evidence. It concluded failure at the workflow level without ever executing: no runner was acquired, the job was cancelled with zero steps and zero billable runtime, and it produced no lint, test, or audit output. That is an infrastructure non-result — not a failed check, and not evidence about this tree in either direction. It is the mirror image of the masking problem below, and the same lesson: a conclusion is never read in place of logs.

The CI acceptance step is now enforced. It carried continue-on-error: true while the truthful verdict was FAIL — a masked step whose real exit status stayed visible in the logs, rather than a || true that would have discarded it. That masking was removed once the run above measured PASS on a clean runner, because a flag that once prevented a red check from restating what the documents already said would now hide a regression behind a green conclusion. A failing audit fails the job, and .github/workflows/ci.yml also declares workflow_dispatch so an exact ref can be revalidated after an infrastructure non-result without manufacturing a commit. Enforcement does not retire the reading rule: this verdict is trustworthy because the step logs were read, as docs/LIMITATIONS.md records, and not because a badge is green.

Four causes were removed as circular, not as fixed#

An earlier revision recorded seven causes. The other four — U07 Partial, plus T-007, NFR-002, and NFR-009 blocked on that status — were an artifact of the gate measuring itself, and removing them changed no evidence:

  • T-007, NFR-002, NFR-009, and AC-014 are E07's own deliverables, so the audit is the thing they name. Deciding them from the owning status closed a loop: a FAIL verdict was the stated reason U07 was Partial, and a Partial U07 then blocked the three rows, so one declared status became four causes and a truthful FAIL described itself four extra times. AC-014 already had this carve-out; it now covers the three identifiers that share the problem. They are classified on observable orchestration — the script exists, the workflow runs make test and make acceptance, the run covered all 42 identifiers — and never on the verdict's value.
  • U07 became Implemented on executed evidence, not on a document edit. Run 31104632401 is a measured CPU execution of the whole orchestration. Requiring a PASS verdict to promote U07 was unsatisfiable by construction: a green step needs continue-on-error: true removed, which needs the verdict to already be PASS, which needs U08 Implemented — so U07 could never close on its own work. A capability is complete when its own behaviour exists and has run, which is what the vocabulary in AGENTS.md says and what the CI run shows.

The verdict stayed FAIL throughout that reconciliation. The three causes that remained were all genuinely downstream of U08, and each would still have been reported if the audit's own rows had been removed entirely. U08's delivery, recorded above, is what cleared them.

The reconciliation has since executed on a clean CPU runner. Run 31109003496 on commit 17c56d9 printed Strict MVP: FAIL — 3 cause(s) naming exactly U08, T-008, and NFR-010, with 14 claims separated as unverifiable in that environment and not counted against the verdict. So the three-cause verdict is reproduced by a second, independent environment: the runner has no artifact root, routes 21 rows to not_evidenced where a fully-evidenced local run passes them, and still reaches an identical cause list. That is the environment-independence property the audit is built to have — the causes are substantive, and the environmental gaps are reported separately rather than inflating them. That property holds after U08's delivery too: both environment states now reach PASS, as the verdict section records.

Three further candidate causes were assessed and did not hold:

  • NFR-001's RAM clause passes as Measured. The sealed bundle's provenance.json records peak_memory_bytes 2,877,472,768 — 2.88 GB against a 24 GB clause.
  • NFR-001's GPU clause is not_applicable, not an unmeasured MUST. The same record shows cuda_available false on device cpu, so no GPU path was exercised. NFR-002 requires CPU execution; the GPU clause describes local convenience hardware, not a deliverable capability. CUDA compatibility is unverified and no GPU claim is made anywhere. This is the single not_evidenced row.
  • The 585/563 contradiction is resolved above rather than left standing as a verdict input.

No applicable MUST clause is left unmeasured: across all 44 rows, 25 are measured with an evidence path and 19 are not_applicable with a recorded reason. make lint enforces that no row can be excused without one.

NFR-006's latency figures remain Measured but explicitly non-reproducing: the run ID covers the sampling protocol and never the durations. The audit records that qualifier rather than a bare pass.

The serving evidence above was produced on this machine from the E05 artifact root. It is not a CI claim: the demo has not been executed in GitHub Actions and will not be, by decision D39 — the sealed bundle is untracked and roughly 257 MB, and a CI-trained model would carry a different threshold and so be a different artifact. make demo on a clean CPU runner is therefore out of scope rather than pending, and it is not an unmet part of U07. The latency figures in the demo transcript are single observations and no service-level claim is made from them. Those figures are also narrower than their published definition, which is documented divergence D2: INTERFACE_CONTRACT.md:72 defines latency_ms as request validation plus inference plus response assembly, while the served value times inference only. Validation has already completed when the handler is entered, and assembly cannot be inside a number the response being assembled must carry. The reported value is therefore a lower bound on the contract's quantity, recorded rather than relabelled.

The transformer does not beat the baseline#

The fine-tuned DistilBERT scored 0.2034 lower on test macro-F1 than the TF-IDF baseline. REQUIREMENTS.md AC-004 requires the comparison to be reported whichever way it falls, so this is a satisfied acceptance criterion and not a defect. At the frozen configuration — two CPU epochs, learning rate 2e-5, max sequence length 96 — the lexical baseline is the stronger model on this split.

No configuration, epoch count, seed, or threshold was changed in response to observing this result. Doing so would let a test label influence a modelling decision, which AC-005 forbids. The result stands as measured, and any future attempt to improve the transformer must be justified and validated without reading the test split.

Neither model's confidence is a probability of correctness#

Both models are substantially underconfident on the test split. The baseline reports a mean confidence of 0.3770 against an accuracy of 0.8653, and the transformer 0.2258 against 0.6955. Every occupied bin errs in the same direction for both models, which is why each ECE happens to equal its aggregate confidence-accuracy gap here; that is a property of this data, not of the metric, and the imbalanced fixture in tests/fixtures/metric_regression.json separates the two quantities (1/12 against 1/20).

The transformer's confidence never exceeded 0.6000 on any of the 3,080 test examples — six of the fifteen bins are empty. The persisted threshold of 0.1684 therefore has to be read relative to that range, not as a claimed probability of correctness. Reported because a reader who takes 0.1684 as a probability would conclude the system accepts almost anything, which is not what the coverage of 0.6799 shows.

No recalibration was applied. Temperature scaling or an equivalent fit would need its own validation-only evidence, and it is out of scope for the weekend MVP. Abstention here is a ranking decision over confidences, which underconfidence does not invalidate; the cost it imposes is coverage, since answers the model would have got right are discarded.

Related in repository

Open in full graph →
DOCUMENTED_BYDOCUMENTED_BY — source: docs/IMPLEMENTATION_STATUS.md § Implementation statusDOCUMENTED_BYDOCUMENTED_BY — source: docs/IMPLEMENTATION_STATUS.md § Implementation statusDOCUMENTED_BYDOCUMENTED_BY — source: docs/IMPLEMENTATION_STATUS.md § Implementation statusDOCUMENTED_BYDOCUMENTED_BY — source: docs/IMPLEMENTATION_STATUS.md § Implementation statusDOCUMENTED_BYDOCUMENTED_BY — source: docs/IMPLEMENTATION_STATUS.md § Implementation statusDOCUMENTED_BYDOCUMENTED_BY — source: docs/IMPLEMENTATION_STATUS.md § Implementation statusDOCUMENTED_BYDOCUMENTED_BY — source: docs/IMPLEMENTATION_STATUS.md § Implementation statusDOCUMENTED_BYDOCUMENTED_BY — source: docs/IMPLEMENTATION_STATUS.md § Implementation statusDOCUMENTED_BYDOCUMENTED_BY — source: docs/IMPLEMENTATION_STATUS.md § Implementation statusIMPLEMENTS — source: docs/IMPLEMENTATION_STATUS.md § Implementation statusCURRENTdocument: Implementation statusU01 — Project foundation…capability: U01 — Project foundation and reproducibilityU02 — BANKING77 data con…capability: U02 — BANKING77 data contractU03 — TF-IDF logistic-re…capability: U03 — TF-IDF logistic-regression baselineU04 — DistilBERT trainin…capability: U04 — DistilBERT training and immutable artifactU05 — Comparative evalua…capability: U05 — Comparative evaluation and selective predictionU06 — FastAPI inference …capability: U06 — FastAPI inference and real-artifact demoU07 — Full validation an…capability: U07 — Full validation and acceptance gateU08 — Documentation and …capability: U08 — Documentation and recruiter-ready deliveryU01 — Project foundation…claim: U01 — Project foundation and reproducibility: Implemented
Hover an edge for its provenance · click a node to open it10 nodes · 10 edges