Chapter 10 · Packaging and the Beginner Capstone
Documentation, Release, and Portfolio Quality
- Page 20 of 22
- 28 min read
Aiko in Osaka sends a recruiter the link to her project. The repository has a README that works, a demo that also shows what happens when things go wrong, honest limits, and one number for quality, so the recruiter keeps reading. Another candidate's code may be just as good, but nothing in it says so. Code decides whether a feature works; documentation decides whether anyone can tell.
Everything here is local and offline. The model is a small stand-in, not a language model; real models are less predictable, which is exactly why you test. This page adds three small modules and a docs/ folder and edits nothing that exists.
What you will learn
- How to record a demo from synthetic data (one success, one missing-evidence case, one handled error) and document the provider-neutral interface with an adapter that leaves the core untouched.
- How to document evaluation results and limitations honestly, with numbers produced by a run.
- How to check for secrets and sensitive fixtures, set a dependency update policy, and turn a release checklist into a script.
- How to write a short case study, plan the next step from evidence, and review a release candidate in a clean environment.
What goes into docs/
One rule governs the folder: a number in a document is generated by a run or it is not there. A hand-typed number drifts the day the code changes, as the README test on the last page showed. So a script writes the documents that hold results, and a test fails when one is stale.
| File | Covers | Made by |
|---|---|---|
DEMO.md | the recorded demo | generated from a run |
INTERFACE.md | adding an adapter | written, backed by adapter_example.py and a test |
EVALUATION.md | dataset, metrics, failures | generated from a run |
LIMITATIONS.md | what is not supported or not tested | generated counts around written text |
UPDATE-POLICY.md, RELEASE-CHECKLIST.md | upgrades and releases | written; the checklist is partly run by release.py |
CASE-STUDY.md, NEXT-STEP.md | portfolio story, next step | generated numbers around written reasoning |
Record a demo using synthetic data
A demo is a recording of real behaviour that anyone can regenerate. Ours is a script, not a screen capture: it calls the app from Packaging, Configuration, and Reproducibility in-process and writes what came back. It shows three cases, because a demo that only succeeds says nothing about trust: an answer with its citation, a question no document answers, and a model that crashes. The data is the fictional Kestrel Cycles help centre; the user id mateo is made up.
# excerpt of kit/evidencedesk/docgen.py
def demo_md() -> str:
class Crash: # a model that fails with text that must never reach the caller
def complete(self, request):
raise RuntimeError("password=hunter2 at /srv/app/db.py")
steps = [("1. Success", create_app(), Q_OK, "demo-1", "The answer cites the passage it used."),
("2. Missing evidence", create_app(), Q_NONE, "demo-2", "No document answers this, so the assistant refuses instead of guessing."),
("3. Handled error", create_app(client=Crash()), Q_OK, "demo-3",
"The model crashed with an internal message. The caller gets a plain failure: no path, no password, no stack trace.")]
from fastapi.testclient import TestClient
out = ["# Demo", "", "Synthetic data only (the fictional Kestrel Cycles help centre). Offline: a fake model, no key, no network.",
"Generated by `python -m evidencedesk.docgen` through the in-process test client; do not edit by hand.", ""]
for title, app, question, rid, note in steps:
with TestClient(app) as http:
r = http.post("/ask", json={"question": question}, headers={"x-user-id": "mateo", "x-request-id": rid})
out += [f"## {title}", "", note, "", "```text", f"POST /ask {_json({'question': question})}", f"-> {r.status_code} {_json(r.json())}", "```", ""]
return "\n".join(out)Run it, then read the demo.
python -m evidencedesk.docgen
sed -n '/^## 1/,$p' docs/DEMO.mdwrote docs/DEMO.md
wrote docs/EVALUATION.md
wrote docs/LIMITATIONS.md
wrote docs/NEXT-STEP.md
wrote docs/CASE-STUDY.md
## 1. Success
The answer cites the passage it used.
```text
POST /ask {"question": "How long do refunds take after inspection?"}
-> 200 {"message": "Refunds go back to the original payment method within 5 business days after the warehouse receives the item. [kc-returns-v3:c2]", "request_id": "demo-1", "status": "answered"}
```
## 2. Missing evidence
No document answers this, so the assistant refuses instead of guessing.
```text
POST /ask {"question": "Can I pay in instalments?"}
-> 200 {"message": "Not enough evidence in the help centre to answer this. Please contact Kestrel Cycles support.", "request_id": "demo-2", "status": "refused"}
```
## 3. Handled error
The model crashed with an internal message. The caller gets a plain failure: no path, no password, no stack
trace.
```text
POST /ask {"question": "How long do refunds take after inspection?"}
-> 500 {"message": "Something went wrong on our side. Please try again later.", "request_id": "demo-3", "status": "failed"}
```Notice three things. The refusal is a normal 200 answer: no evidence is correct behaviour, not an error. The crash becomes a plain 500 with the generic message; the invented crash text (a password and a path) never reaches the caller. And the request ids are fixed by a header, so the file is the same on every run.
Document the provider-neutral interface
The contract is one method, complete(request) -> response, with our own request, response and error types (see Provider-Neutral LLM Clients). An adapter does four jobs: map our request to the provider's, call it, map the answer back, and map failures to our error classes (see Adapters, Configuration, and Error Handling). docs/INTERFACE.md lists those steps. Words are not proof, so the kit holds a complete adapter for an imaginary provider: system text and input are separate fields, usage and the finish reason have other names, and a failure is a status. pretend_server stands in for the network. It speaks that wire format but answers from a fake client, so the adapter's mapping code really runs.
# file: kit/evidencedesk/adapter_example.py
"""A third-party adapter written against evidencedesk.llm only. No core module imports this file."""
from __future__ import annotations
from collections.abc import Callable
from typing import Any
from evidencedesk.errors import from_status
from evidencedesk.llm import LLMClient, LLMRequest, LLMResponse, Message, Usage
Transport = Callable[[dict[str, Any]], dict[str, Any]] # one JSON object in, {"status", "headers", "body"} out
STOP = {"stop": "end", "max_tokens": "length"} # their words -> ours; anything else is "other"
class JsonAdapter:
"""For an imaginary provider: system text and input are separate fields, usage and finish reason have their own names."""
def __init__(self, transport: Transport, model: str = "example-model") -> None:
self.transport, self.model = transport, model
def complete(self, request: LLMRequest) -> LLMResponse:
text = lambda role: "\n".join(m.content for m in request.messages if m.role == role)
reply = self.transport({"model": self.model, "system": text("system"), "input": text("user"), "max_output": request.max_tokens})
if reply["status"] != 200: # job 4: failures become our error classes, so retries and the safe layer work unchanged
raise from_status(reply["status"], reply.get("headers"), str(reply.get("body", "")))
body = reply["body"]
return LLMResponse(body["text"], self.model, Usage(body["tokens"]["in"], body["tokens"]["out"]), STOP.get(body["finish"], "other"))
def pretend_server(inner: LLMClient) -> Transport:
"""Stands in for the network: it speaks the imaginary wire format but answers from any LLMClient (here, a fake)."""
def transport(payload: dict[str, Any]) -> dict[str, Any]:
sent = LLMRequest((Message("system", payload["system"]), Message("user", payload["input"])), payload["model"], payload["max_output"])
got = inner.complete(sent)
finish = {v: k for k, v in STOP.items()}.get(got.stop_reason, "other")
return {"status": 200, "body": {"text": got.text, "tokens": {"in": got.usage.input_tokens, "out": got.usage.output_tokens}, "finish": finish}}
return transportNow send the same two questions through the whole service twice, once with the default client and once through the adapter, and add a provider that fails.
from fastapi.testclient import TestClient
from evidencedesk.adapter_example import JsonAdapter, pretend_server
from evidencedesk.api import OfflineClient, create_app
def ask(client, question):
with TestClient(create_app(client=client)) as http:
return http.post("/ask", json={"question": question}, headers={"x-user-id": "mateo"}).json()
adapted = JsonAdapter(pretend_server(OfflineClient()))
for q in ("How long do refunds take after inspection?", "Can I pay in instalments?"):
print(ask(adapted, q) == ask(None, q), ask(adapted, q)["status"])
down = JsonAdapter(lambda payload: {"status": 500, "body": "stack trace at /srv/app.py"})
print(ask(down, "How long do refunds take after inspection?"))True answered
True refused
{'status': 'failed', 'message': 'Something went wrong on our side. Please try again later.', 'request_id': 'req-1'}Same answers through both clients, so rag, safefail and api did not change. The failing provider ends as the standard failure shape and its text never appears. These tests pin it down, including one that fails if a core module ever imports an adapter.
# excerpt of kit/tests/test_release.py
def test_core_logic_gives_the_same_answer_through_the_adapter():
question = "How long do I have to return a new bike?"
assert answer_question(JsonAdapter(pretend_server(FakeClient())), question) == answer_question(FakeClient(), question)
@pytest.mark.parametrize("question", [Q, "Can I pay in instalments?"])
def test_the_whole_service_answers_identically_through_the_adapter(question):
def ask(client):
with TestClient(create_app(client=client)) as http:
return http.post("/ask", json={"question": question}, **ASK).json()
assert ask(JsonAdapter(pretend_server(OfflineClient()))) == ask(None)
def test_no_core_module_imports_the_adapter():
for path in (KIT / "evidencedesk").glob("*.py"):
if path.name != "adapter_example.py":
imported = {n.module for n in ast.walk(ast.parse(path.read_text(encoding="utf-8"))) if isinstance(n, ast.ImportFrom)}
assert "evidencedesk.adapter_example" not in imported, path.namepython -m pytest tests/test_release.py -k "adapter or core or whole_service or stop_reasons or failing_provider" | sed 's/ in [0-9.]*s//'....... [100%]
7 passed, 7 deselectedThis shows that an adapter fits the contract and the core does not know it exists. It does not show how a real provider behaves: real adapters need an account, so they stay sketches marked # not run here, as on the first two pages.
Document evaluation results
An evaluation section needs four things: the dataset (name, version, hash, size), the metrics with their meaning, the failures listed rather than hidden, and what the metrics cannot prove. EVALUATION.md is written from a live run of the fixed suite in Rubrics, Regression, and Release Gates. First the gate's metrics, then the failures part of the document.
python -m evidencedesk.gate | sed -n '4,9p;$p'
sed -n '/^## Failures/,$p' docs/EVALUATION.md recall_at_k 0.882
citation_validity 1.0
abstention_correct 0.773
tool_pass 1.0
format_pass 1.0
pass_rate 0.848
GATE: GO
## Failures
| Case | Question | Expected | What happened |
|---|---|---|---|
| lg03 | কিস্তিতে বাইক কেনা যায় কি? | abstain | a Bangla question no document answers; it was answered, not refused (the unsafe direction) |
| rc03 | RD2-24 | answer | an exact identifier on its own; the toy embedder matches letters and the match was too weak to answer |
| rc06 | How do I get my money back? | answer | a paraphrase ('money back' for 'refund'); the right document was not in the top results |
| rc08 | How long before a voucher lapses? | answer | a paraphrase ('voucher lapses' for 'gift card expires'); the top two results were too close to choose |
| rc13 | How long are my support chats stored? | answer | a paraphrase ('chats stored' for 'retention'); the top two results were too close to choose |
## What these numbers cannot show
- The answers come from recorded, hand-written replies, not from a language model. The suite checks retrieval,
thresholds, citation checks and the gate, not answer quality of any real model.
- 33 cases is a small sample: one case moves the pass rate by about 3.0 points, so a one-case difference is
noise.
- There is no held-out set: the same cases are used to judge and to tune, so the scores are optimistic for
unseen questions.
- A passing citation check shows the cited passage was shown to the model, not that it supports the claim.
- No latency, cost or load was measured here.Read the failures as a reviewer would. The four English misses refused although a document held the answer; they show what the toy embedder cannot do (an identifier on its own, three paraphrases). The Bangla miss, lg03, is different: the assistant answered a question that no document answers, the unsafe direction. The Bangla slice has only five cases, so one case moves it by twenty points. The closing list is the part reviewers trust most, because it shows that the author knows the limits of the score.
Publish an honest limitations section
A limitation is specific, tied to evidence, and says whether something is unsupported (known not to work) or untested (nobody looked). "May have limitations" is not one. Cover languages, data, models, the service and conditions you never ran. The counts are generated, so they cannot drift.
cat docs/LIMITATIONS.md# Limitations
Generated by `python -m evidencedesk.docgen`; the counts come from the kit as it is now.
## Languages
- Supported by tests: English and Bangla only. The corpus has 11 English and 2 Bangla documents; the
evaluation has 5 Bangla and 3 mixed-script questions against 14 English ones.
- Bangla pass rate is 0.8 on 5 cases; failing: lg03. Any other language is untested.
## Models and retrieval
- The model is a fake with recorded replies. Real providers were never called for this release; their adapters
are untested sketches.
- The embedder is a toy (character trigrams). It matches letters, not meaning: 4 English questions fail (3
paraphrases, 1 exact identifier) and one test is a documented expected failure.
## Data
- Synthetic documents only (13 documents, 36 chunks), searched in memory. Large corpora, document updates and
real customer text are untested.
- 33 evaluation cases and 15 abuse cases: regression sets, not proof of quality or safety.
## Service and security
- `x-user-id` is a stand-in for authentication. Do not expose the service before real authentication exists.
- Prompt injection is limited by design (allowlists, owner filters, screening), not solved; the abuse set
shows known attacks are blocked, not that new ones are.
- Tested in-process with `TestClient` on Linux with Python 3.12.3. A real server, concurrency, load, macOS and
Windows were not tested.
## Packaging
- Data folders sit beside the package; a built wheel would not contain them. `gate.hand_written_client`
imports a test module (a known wart).Notice the claim: Linux and Python 3.12.3 only, although the README names three operating systems as targets. A README may state a goal; a limitations file states what was run.
Check for secrets and sensitive fixtures
A script catches known shapes; a person must do the rest. The manual checklist is in RELEASE-CHECKLIST.md: search the git history and not only the working tree (a deleted secret stays in history, so rotate it), check that .gitignore covers environments, .env files and logs, look in fixtures, logs and screenshots for real names or data, and check the licence of every file you did not write.
The automatic part reuses what you built. safefail.personal_data finds email, card and phone shapes (Safe Failure and Security Regression Tests), and the key shape and the one made-up key come from security. The new function adds provider-style keys, private-key blocks, hard-coded assignments and secret-like file names, and it skips environments and caches (scan_data walks every folder, so it would read a .venv inside the project).
# excerpt of kit/evidencedesk/release.py
ALLOWED = { # (file, kind) -> why a person decided this finding is fine; review it again whenever the file changes
("data/abuse/abuse_set.jsonl", "key-shaped"): "made-up bait that the key-shape control must block",
("tests/test_security_p17.py", "key-shaped"): "the same made-up bait, asserted by a test",
("tests/test_context_quality.py", "hard-coded secret"): "a reserved example address used as a fake secret",
}
def scan_secrets(root: Path = KIT, allowed: dict[tuple[str, str], str] | None = None) -> list[str]:
"""'path:line kind' for every secret-shaped string, secret-like file name or personal-data shape. Values are never returned.
Findings listed in `allowed` (default ALLOWED) are dropped; pass {} to see everything."""
allowed = ALLOWED if allowed is None else allowed
found = []
for path in project_files(root):
rel = path.relative_to(root)
found += [f"{rel} file name" for pattern in BAD_NAMES if path.match(pattern)]
if path.suffix not in TEXT:
continue
for number, line in enumerate(path.read_text(encoding="utf-8", errors="replace").splitlines(), 1):
found += [f"{rel}:{number} {kind}" for kind, shape in SHAPES.items() if shape.search(line)]
found += [f"{rel}:{number} key-shaped" for m in KEY_SHAPE.finditer(line) if m[0] != SIMULATED_SECRET] # the one made-up key is allowed
found += [f"{rel}:{number} {kind}" for kind in personal_data(line)] if path.suffix in PERSONAL else []
return [f for f in found if (f.split(":")[0].split(" ")[0], f.split(" ", 1)[1]) not in allowed]Run it three ways: everything it sees, after review, and on a folder where we plant a key, a password and an email (built inside the block, so this page holds no key-like text).
import tempfile
from pathlib import Path
from evidencedesk import release
print("everything the scan sees:", *release.scan_secrets(allowed={}), sep="\n ")
print("after review:", release.scan_secrets())
with tempfile.TemporaryDirectory() as folder: # a planted copy; the values are built here so this page holds no key-like text
(Path(folder) / "notes.md").write_text("key: sk-" + "ab" * 15 + "\npassword = \"" + "q" * 14 + "\"\nask ana@corp.test\n", encoding="utf-8")
(Path(folder) / ".env").write_text("MODE=demo\n", encoding="utf-8")
print("planted:", *release.scan_secrets(Path(folder)), sep="\n ")everything the scan sees:
data/abuse/abuse_set.jsonl:6 key-shaped
tests/test_context_quality.py:123 hard-coded secret
tests/test_security_p17.py:55 key-shaped
after review: []
planted:
.env file name
notes.md:1 provider-style key
notes.md:2 hard-coded secret
notes.md:3 emailThe three raw findings are deliberate: a made-up key that the key-shape control must block (in the abuse set and in one test) and a reserved example address used as a fake secret. A person read each and wrote the reason into ALLOWED; an allowance names the file and the kind, not the line, so adding a line does not break it. The scan reports where and what, never the value. Its limits: it finds known shapes only (a password in plain prose passes) and it does not read git history. A dedicated scanner is a good addition, not a replacement for the checklist.
Set a dependency update policy
Pins stop surprise changes; a policy decides when to accept one on purpose. UPDATE-POLICY.md says: test in a scratch copy and a new environment, compare pip freeze with the lock file, run the suite and the evaluation gate, compare the metrics with the baseline, read release notes and advisories, and adopt as one reviewed change. Upgrade at once for a published security fix; otherwise on a schedule and when a Python version leaves support. Here is the drill. Nothing was newer on the day we wrote this, so the candidate steps numpy back one release; the steps for a real upgrade are identical.
rm -rf ../try && cp -r . ../try && cd ../try
python -m venv .venv && source .venv/bin/activate
sed -i 's/^numpy==.*/numpy==2.5.3/' requirements.txt
python -m pip install -q --disable-pip-version-check -r requirements.txt
python -m pip freeze | diff - <(grep -v '^#' requirements.lock.txt)
python -m pytest | tail -1 | sed 's/ in [0-9.]*s//'
python -m evidencedesk.gate | tail -1
{ grep '^#' requirements.lock.txt; python -m pip freeze; } > lock.new && mv lock.new requirements.lock.txt
python -m pytest | tail -1 | sed 's/ in [0-9.]*s//'
python -m evidencedesk.release --tests | tail -110c10
< numpy==2.5.3
---
> numpy==2.5.4
1 failed, 468 passed, 1 xfailed
GATE: GO
469 passed, 1 xfailed
RESULT: READY (0 failed, 0 skipped)The diff is the real change: one line. The suite then reports one failure, the right one: the test that requires the lock file to contain the pins. The lock is part of the release, so the last step regenerates it. After that the suite passes, the gate says GO and the release check is ready. The evaluation numbers did not move, which is the evidence you want from a patch-level change.
What the drill does not catch: behaviour the tests do not exercise, real provider behaviour (nothing here calls one), problems with real data volumes, concurrency or other systems, and a compromised release, because a version pin is not a content hash.
Tool note (as of 2026-10-12): Python 3.12 is listed as supported until 2028-10, and 3.10 reached end of life on 2026-10-01, so a Python version leaving support is a reason to run this policy. On that date an advisory database listed one medium pytest advisory (fixed in 9.0.3), and the package index listed no known vulnerabilities for the latest releases of pydantic, fastapi, numpy and pytest. pip's hash-checking mode needs
==pins and a hash for every package;uv.lockbelongs to the uv tool, andpylock.toml(PEP 751) is a standard alternative. Advisories change daily: check the current documentation.
Define a basic release checklist
A checklist works when it is short, binary and run by a script wherever possible. Ours requires: the supported Python, exact pins, configured and passing tests, documented configuration (README and docs/), required data, offline mode as the default, generated documents that match a fresh run, a clean secret scan, an evaluation gate that does not block, and an abuse set with no failing case. release.py runs them all; the suite runs only with --tests because it is slow. Each check is a name, a status and a detail.
# excerpt of kit/evidencedesk/release.py
@dataclass(frozen=True)
class Check:
name: str
status: str # "ok", "FAIL" or "skip"
detail: str
def run_checks(root: Path = KIT, tests: bool = False) -> list[Check]:
checks: list[Check] = []
def add(name: str, problem: str | None, ok_detail: str) -> None:
checks.append(Check(name, "FAIL" if problem else "ok", problem or ok_detail))
...
add("offline by default", None if (s.offline, s.provider, s.api_key) == (True, "fake", None) else "defaults are not offline", "offline, fake provider, no key")
add("secrets and sensitive fixtures", f"{len(hits)} finding(s): {hits[:3]}" if hits else None, "no findings")
add("evaluation gate", None if decision.verdict != "block" else "; ".join(decision.blockers), f"{decision.verdict.upper()} against the stored baseline")
...
if tests:
... # run pytest in a subprocess and keep its last line
else:
checks.append(Check("test suite", "skip", "run with --tests"))
return checkspython -m evidencedesk.releaseRELEASE CHECK evidencedesk
[ok ] python version 3.12 (needs >=3.12)
[ok ] exact pins every line of requirements.txt uses ==
[ok ] tests configured testpaths ['tests'], 25 test files
[ok ] required data 5 of 5 paths present
[ok ] offline by default offline, fake provider, no key
[ok ] documentation 9 documents present
[ok ] generated docs current match a fresh run
[ok ] secrets and sensitive fixtures no findings
[ok ] evaluation gate GO against the stored baseline
[ok ] abuse set no failing case
[skip] test suite run with --tests
RESULT: READY (0 failed, 1 skipped)The suite is skipped without --tests, and the report says so instead of pretending. READY means the automatic part passed, not that the release is good: the manual part of the checklist (a clean-environment run by a person, the history search, the licence check) still belongs to a human.
Write a short portfolio case study
A case study answers a reviewer's questions on one page: the user's problem, what you built, what you traded away, what you measured, and what you would do next. Use measured numbers only, and drop any sentence you cannot back. Ours is generated, so its numbers are the ones above.
cat docs/CASE-STUDY.md# Case study: EvidenceDesk
Numbers below are measured by `python -m evidencedesk.docgen`; nothing is estimated.
## The user problem
Customers of a fictional bike shop ask policy questions. A wrong answer about refunds or recalls is worse than
no answer, so the assistant must cite its source, say when it has none, and never leak private data or obey
text hidden in a document.
## Architecture
A request enters one FastAPI route, passes identity, task and size checks, retrieves evidence from a local
corpus, builds a context in which document text is marked untrusted, asks a provider-neutral client for a
reply, validates the reply, and returns one response shape. The model is replaceable behind a one-method
interface; the safety checks live outside the model.
## Trade-offs
- Offline fake model and toy embedder: every test is repeatable and free, but real quality is unmeasured.
- Refuse when evidence is weak: fewer wrong answers, more refusals. The threshold decides the balance.
- One public response shape with no internal codes: callers learn nothing from errors, and developers rely on
logs.
- Exact version pins and a lock file: repeatable installs, with the cost of deliberate upgrades.
## Measured results
- Evaluation (33 cases): recall 0.882, citation validity 1.0, abstention 0.773, pass rate 0.848.
- Failures: lg03, rc03, rc06, rc08, rc13; the release gate says GO against the stored baseline.
- Abuse set: 15 cases, checked by the release script.
## What I would do next
See `NEXT-STEP.md`: more Bangla refusal cases before any change to the threshold.Plan the next engineering step
After an evaluation the tempting next step is a bigger component. The disciplined one starts from the failures: list them, group them by cause, ask which hurts users most, pick the cheapest experiment, and write the success test before the change. Here the failures are rc03, rc06, rc08, rc13 and lg03. Refusing an answerable question costs a support contact; answering an unanswerable one costs trust, so lg03 comes first. The cheapest lever is the confidence threshold, so the document sweeps it over the same cases.
sed -n '/^| min_score/,/^$/p' docs/NEXT-STEP.md| min_score | Failing cases | Pass rate | Unanswerable answered |
|---|---|---|---|
| 0.25 | lg03, rc06, rc08, rc13 | 0.879 | 1 of 5 |
| 0.3 | lg03, rc03, rc06, rc08, rc13 | 0.848 | 1 of 5 |
| 0.35 | rc03, rc06, rc08, rc13 | 0.879 | 0 of 5 |
| 0.4 | rc02, rc03, rc06, rc08, rc13 | 0.848 | 0 of 5 |At 0.35 lg03 passes and nothing new breaks; at 0.4 rc02 breaks. It is tempting to set 0.35 and move on. Do not: the value was found on the cases it is scored on, and one Bangla case is the whole evidence. The decision in NEXT-STEP.md is to add at least five Bangla questions that no document answers, run the sweep again, and only then choose. A reranker, real embeddings or a second search method wait until a failure shows what is missing; rc03 and the paraphrases are parked with that reason written down.
Mini-Lab: Release Candidate Review
Review the kit as a stranger would. Copy the kit folder, call it project, and save the block as lab.sh beside it. We ran env -i PATH=/usr/bin:/bin HOME=$PWD bash lab.sh, an empty environment, so no variable could help and no API credential existed. It creates a fresh environment, installs from the pinned file, compares the install with the lock file and the demo with the README, makes any network connection fail loudly while the demo and the gate run, and runs the release check with the full suite.
cd project
python3.12 -m venv .venv
source .venv/bin/activate
env | grep -ci api_key
python -m pip install -q --disable-pip-version-check -r requirements.txt
python -m pip freeze | diff - <(grep -v '^#' requirements.lock.txt) && echo "same as the lock file"
python -m evidencedesk.api | diff - <(sed -n '/^GET \/health/,/^POST \/ask -> 401/p' README.md) && echo "demo matches the README"
python - <<'EOF'
import socket
def blocked(*args, **kwargs):
raise RuntimeError("network call attempted")
socket.socket.connect = blocked # any outgoing connection now fails loudly
socket.getaddrinfo = blocked
from evidencedesk.api import demo
from evidencedesk.gate import run_gate_suite
print("offline demo lines:", len(demo().splitlines()), "| pass rate:", run_gate_suite()["metrics"]["pass_rate"])
EOF
python -m evidencedesk.release --tests0
same as the lock file
demo matches the README
offline demo lines: 5 | pass rate: 0.848
RELEASE CHECK evidencedesk
[ok ] python version 3.12 (needs >=3.12)
[ok ] exact pins every line of requirements.txt uses ==
[ok ] tests configured testpaths ['tests'], 25 test files
[ok ] required data 5 of 5 paths present
[ok ] offline by default offline, fake provider, no key
[ok ] documentation 9 documents present
[ok ] generated docs current match a fresh run
[ok ] secrets and sensitive fixtures no findings
[ok ] evaluation gate GO against the stored baseline
[ok ] abuse set no failing case
[ok ] test suite 469 passed, 1 xfailed
RESULT: READY (0 failed, 0 skipped)Check each line. The credential count is 0, the install matches the lock, and the demo matches the README. With connections blocked, the demo still produced its five lines and the evaluation still scored 0.848, so offline operation is shown, not assumed. The release check passes with the suite inside it: 469 passed and the one documented expected failure. Documented limitations are a required item, and they exist. What this cannot show: an in-process run on one Linux machine says nothing about other systems, real providers or load, which is exactly what LIMITATIONS.md says.
Where this shows up at work
Omar in Cairo joins a team that wants to adopt a prototype. Before anyone reads the code they ask for the demo, the evaluation numbers, the known limits and how it is released. A prototype with those four things gets a decision in one meeting; one without them gets a week of questions. The same documents serve an audit, a hand-over and the portfolio you show for the next job.
Common mistakes
- Hand-typed numbers. They are right on the day and wrong a month later. Generate them from a run and test that the file is current.
- A demo that only succeeds. It hides what trust depends on. Show a refusal and a handled error.
- Vague limitations. "Not production ready" helps nobody. Name the language, the data size, the fake model and the conditions you did not run.
- Trusting the secret scan. A scan finds shapes, not history or plain-text passwords. Keep the manual checklist and rotate anything that ever leaked.
- Upgrading in the shipping environment. You cannot compare or roll back. Test in a scratch copy and update the lock file in the same change.
Try it yourself
- Easy. Revisit Packaging, Configuration, and Reproducibility. In a copy of the project delete
docs/LIMITATIONS.mdand run the release check. Predict which checks fail, then run it. - Medium. Revisit Rubrics, Regression, and Release Gates. Run the gate with
--min-score 0.35. What failures remain, what is the verdict, and why is that not enough to change the default? - Hard. Revisit Provider-Neutral LLM Clients. Write an adapter for a provider whose reply is a list of parts plus a done reason and has no usage field. Show that truncation reaches the user as a cut-off marker and that an empty finished reply is caught by the contract check.
Answers
Easy. Two checks fail for one cause: the file is required (documentation) and it is generated (a missing generated file counts as out of date). The result is NOT READY and the exit code is 1.
rm -rf ../copy && cp -r . ../copy && cd ../copy && rm docs/LIMITATIONS.md
python -m evidencedesk.release | grep -E 'FAIL|RESULT'[FAIL] documentation docs/LIMITATIONS.md missing or empty
[FAIL] generated docs current LIMITATIONS.md out of date
RESULT: NOT READY (2 failed, 1 skipped)Medium. The gate prints GO, and lg03 is fixed; rc03, rc06, rc08 and rc13 remain. GO means no metric fell below the baseline and no critical case failed. It does not mean 0.35 is right: it was chosen by looking at these cases, and a single Bangla case supports it. Collect more Bangla refusal cases first.
python -m evidencedesk.gate --min-score 0.35 | sed -n '/failures/p;$p' failures: rc03, rc06, rc08, rc13
GATE: GOHard. Usage defaults to zeros, which means unknown, and the adapter says so in a comment instead of inventing numbers. The length stop reason becomes our length, so answer_question appends the marker.
from evidencedesk.gate import contract_problems
from evidencedesk.llm import LLMRequest, LLMResponse, Message, Usage, answer_question
class PartsAdapter:
"""An imaginary provider: the reply is a list of parts and a done reason; it reports no usage at all."""
def __init__(self, transport):
self.transport = transport
def complete(self, request: LLMRequest) -> LLMResponse:
raw = self.transport({"text": request.messages[-1].content})
stop = {"complete": "end", "length": "length"}.get(raw["done_reason"], "other")
return LLMResponse("".join(raw["parts"]), "parts-model", Usage(), stop) # Usage() = zeros: unknown, and we say so
cut = PartsAdapter(lambda p: {"parts": ["Standard delivery ", "takes 4 to 7 days"], "done_reason": "length"})
print(contract_problems(cut.complete(LLMRequest((Message("user", "How long?"),)))), "|", answer_question(cut, "How long?"))
silent = PartsAdapter(lambda p: {"parts": [""], "done_reason": "complete"})
print(contract_problems(silent.complete(LLMRequest((Message("user", "How long?"),)))))[] | Standard delivery takes 4 to 7 days [cut off]
['a finished reply needs text']Pass checklist:
- Easy: you predicted two FAIL lines (documentation and generated docs) and NOT READY, and you can say why there are two.
- Medium:
GATE: GOwith four failures left, and a reason that mentions the same cases and too little Bangla data. - Hard: no application code changed; a contract check returns
[]for the truncated reply and one problem for the empty one; the cut-off marker appears. - Mini-lab: 0 credentials, same as the lock file, demo matches, offline demo and gate run, release check READY with the suite included.
Summary
- Documentation is evidence: a regenerable demo, an interface guide proved by a test, evaluation results with failures, and limitations that name what was run.
- A number in a document is generated by a run, and a test fails when the document is stale.
- Scan for secrets and sensitive fixtures, review every finding, and keep the manual checklist, including the git history.
- Upgrade in a scratch copy, compare the freeze, the suite and the baseline, and then update the lock file on purpose.
- Choose the next step from the failures, test the cheapest idea on more data first, and add no component without evidence.
Next: Practice Lab puts the whole project to work in five graded challenges with offline fixtures and clear pass criteria.