You are building Sentinel: a durable vulnerability-response decisioning system for open-source software dependencies, operating under incomplete, evolving, and externally-modelled evidence.
The full rationale is in docs/DECISIONS.md. Read it before Phase 0. It is frozen.
Do not redesign the use case. Architecture changes only because a measurement says so.
The engineering bar is in docs/ENGINEERING.md. Read it before writing any code. It
is not frozen — it tightens, never loosens.
Never invent a number.
This project exists because a previous draft filled its architecture with plausible, confident, fabricated figures. Every number you write must come from code that ran, or carry a label saying where it came from. A missing measurement is reported as missing. An estimate is labelled as an estimate. A fabricated number is a project-ending defect, not a rough draft.
If you find yourself about to write a figure you did not compute, stop and write
TODO(measure): <what needs measuring> instead.
Corollary: never write a findings document before the code that produces it has run. Do not scaffold a report with expected values to be filled in later. That is how invented numbers survive into a final draft.
Every quantitative claim carries exactly one label:
| Label | Meaning |
|---|---|
MEASURED |
Computed by our pipeline from our frozen corpus |
DERIVED |
Mathematically derived from MEASURED or declared inputs |
SOURCE_FACT |
Official external fact (a policy date, a model-version date, a licence term) |
LITERATURE |
A number from an external study — not ours until we reproduce it |
ASSUMED |
An explicit business assumption; must live in config.py |
SIMULATED |
Generated enterprise/process behaviour |
COUNTERFACTUAL |
Replay under an alternative policy |
The headline rule: no ASSUMED or SIMULATED quantity may support a headline
result, empirical finding, benchmark claim or ROI claim. Anywhere else it must be
visibly labelled. tools/check_claims.py enforces this mechanically — it is not advisory.
| ID | Measurement | Phase |
|---|---|---|
| M0 | Temporal reconstructability audit — per source | 0 |
| M1 | Evidence maturity curve | 0 |
| M2 | Source-completeness shift across 2024-12-10 and 2026-04-15 | 0 |
| M3A | EPSS version-boundary decision discontinuity | 0 |
| M3B | Early-warning utility vs later-KEV positives (positive-unlabeled) | 1 |
| M4 | Action-set frontier at matched effort | 1 |
| M5 | Change-failure rate on real upgrades (sandboxed) | 2 |
| M6 | Verification sufficiency | 2 |
| M7 | Agent topology value | 6 |
M0 runs before M1. If a source turns out not to be historically reconstructable, that changes the plan and you must stop and report it, not work around it silently.
- KEV is a positive-unlabeled proxy, not exploitation ground truth.
KEV=1is strong evidence of exploitation.KEV=0proves nothing. Never compute a false-negative rate against KEV without stating this bound in the same paragraph. - We cannot compute a consumer-side calibration or reliability curve for EPSS. EPSS targets ~2.5–3% observed-exploitation-activity per 30 days; KEV covers ~0.5% of CVEs cumulatively. Scoring one against the other makes EPSS look ~5× overconfident by arithmetic. Use early-warning utility (M3B), never the word calibration.
No component enters the system without a measurement that earns it. Before adding a queue, a workflow engine, a cache, a second agent, a database, or a service, write one sentence naming the measurement that forces it. If you cannot, do not add it.
Specifically banned unless a measurement demands them: Kubernetes, Kafka, multi-region, graph databases, agent frameworks adopted for their own sake.
Two boundaries that are architectural, not stylistic:
- The model proposes; deterministic policy authorises. Whatever decides "may this action execute" is ordinary code with a decision record. Never an LLM.
- Security boundaries are non-collapsible; agent boundaries are collapsible. Credentials live in tool adapters, never in agents. A single reasoning agent with a read-only evidence adapter and an isolated repo-write adapter has the same isolation as three agents. Never justify a second agent on credential grounds.
A phase is not complete when its measurement runs. It is complete when its code would survive being paged at 3am.
Every phase has four gates, not one: measurement, engineering, operability,
adversarial. All four are in docs/ENGINEERING.md Part II. A phase may not be tagged
until all four pass. Producing a valid number through code that fails the engineering gate
is the same class of defect as producing an invented number: it makes the project look
finished when it is not.
The non-negotiable subset, enforced by tools/check_engineering.py:
- Every declared dependency is imported by shipping code. No decorative dependencies.
- Config is
imported.exec(compile(...))is banned. - Every threshold a decision depends on lives in
config.py— once, not twice. - Every network, disk and subprocess call has timeouts, bounded retry with backoff, rate-limit handling, corruption detection and resume.
- Every dropped record is counted and reported. Silent skips are fabricated numbers under a different name.
- Every
results/*.jsonrecords run ID, git SHA, corpus manifest digest and config used. mypy --strictclean onsrc/andtools/. Coverage floor 90% onsrc/.- Failure-injection tests for every I/O path. No network in tests.
This does not license gold-plating. Architecture discipline below still binds: the code that exists must be excellent; more code must still be earned by a measurement.
Phases 0 and 1 were tagged under the measurement gate alone. Their outstanding engineering
debt is itemised in docs/ENGINEERING.md Part IV (R1–R11) and must close before the
phase-2 tag.
- No exploit code. No proof-of-concept development. No weaponisation research.
- No scanning of systems not owned by the operator.
- The system reasons about response to public advisories. It does not research offence.
- Phase 2 executes untrusted third-party code (test suites, package lifecycle hooks).
It runs only inside the sandbox defined in
docs/SANDBOX.md: no host socket, no credentials, ephemeral FS, CPU/memory/wall limits, default-deny network with explicit registry egress. If the sandbox is not ready, the experiment does not run.
pyproject.toml Dependencies. uv.lock is committed alongside it.
config.py ALL assumptions. The only place an ASSUMED number may be defined.
corpus/manifest.json Per-source retrieval URL, timestamp, schema version, digest, licence
scripts/fetch/ One fetcher per source. Idempotent. Digest-verified.
scripts/measure/ One script per measurement. Writes results/<id>.json. Never prose.
results/ Machine-generated JSON. The only legitimate source of a number.
docs/findings/ Prose reports. Every number here must trace to results/ or a label.
src/ The system itself. Built in later phases.
tools/ check_claims.py and other gates.
data/ Raw downloads. GITIGNORED. Never committed.
Never commit raw data. Commit the manifest and the digests so the corpus is
reconstructable. data/ is in .gitignore and must stay there.
make setup uv sync - resolves and locks the toolchain
make fetch Download all sources, verify digests, write corpus/manifest.json
make m0 Run the reconstructability audit
make m1 m2 m3a Run individual measurements
make status Which phase is next, and what blocks it (reads git tags)
make build Run every remaining phase headlessly (prompts/AUTONOMOUS_BUILD.md)
make verify lint + format + typecheck + tests(+coverage) + claims + engineering <-- the gate
make test Tests only
make lint ruff check + ruff format --check
make typecheck mypy --strict on src/ and tools/
This project uses uv. Run everything through uv run rather than activating a venv,
and add dependencies with uv add <pkg> (or uv add --dev <pkg>) rather than editing
pyproject.toml by hand. uv.lock is committed - the same reconstructability
discipline we apply to the corpus applies to the toolchain.
make verify must pass before you end any turn that touched docs/findings/ or
results/. A Stop hook enforces this.
- Work in phase order. Phases are in
docs/DECISIONS.mdPart VI (9 phases, 0–8). Do not start a phase whose predecessor's four gates have not all passed (docs/ENGINEERING.mdPart II). - Stop and report — do not work around — when:
- M0 finds a source is not historically reconstructable
- a kill criterion fires (they are in
docs/DECISION.md) - a measurement contradicts the plan
- you need a credential, a paid API, or network access to something not in the manifest
- a fix would require inventing a number
- Commit per measurement, with the results JSON in the same commit as the code that
produced it. Message format:
M2: measure CPE provenance by publication month. - Write
docs/findings/<id>.mdonly afterresults/<id>.jsonexists. Report what the data says, including when it says the hypothesis was wrong. A negative result is a publishable finding, not a failure. - Update
docs/findings/STATUS.mdafter each measurement: what ran, what it showed, what it changed, what it killed.