Monitoring system for Filecoin ProPGF kernel funding. Teams commit (function, SLA,
source) manifests under registry/; the pipeline independently fetches, evaluates,
and lands verdicts. Full picture: README.md. Role-specific playbooks:
docs/guide-projects.md (teams) and docs/guide-reviewers.md (committee).
uv sync # install (Python 3.11+, uv only — never pip)
uv run pytest -q # full offline suite; MUST be green before any commit
uv run python scripts/validate_draft.py --all # drafts: schema+kernel+SQL+allowlist
uv run python -m scripts.validate_pr registry/<team>.yaml # the PR gate, locally
uv run python scripts/promote_draft.py registry/drafts/<team>.yaml --add-allowlist
uv run fpm review <team> [--store DIR] [--dev-auto-approve] [--live --live-oso --oso-org UUID]
uv run fpm observe [teams...] [--as-of DATE] [--dry-run] [--live-oso --oso-org UUID] # readings only -> data/observations.csv
uv run fpm report <team> --link URL [--intent "..."] [--out FILE] # draft an entry from a URL
uv run fpm land --store DIR --oso-org UUID [--public-name N --private-name N]
uv run fpm contract <team> [--facts FILE] [--out FILE] # render a grant contract from manifest + contracts/<team>.facts.yaml
uv run python -m scripts.exports write # regenerate data/kernel_{functions,metrics}.csv from registry/
uv run python -m scripts.exports upload --oso-org UUID # regenerate, then republish both public static models
scripts/demo_project_flow.sh / scripts/demo_reviewer_flow.sh # offline end-to-end demosSkills (if your harness loads .claude/skills/), in program order:
draft-application (Karma form, pre-award) · reconcile-metrics (decide what a team is measured
on; keep agreement §3, registry, facts and dashboard in agreement) · author-manifest (a team
encodes its agreed set) · review-and-land (run the pipeline, adjudicate readings, land verdicts).
- Never hand-edit another team's
registry/<team>.yamloutside a PR; CODEOWNERS and the static gate exist precisely to catch that. - Every
functions[]entry's (tier, category, sub_category) must matchregistry/_kernel.yamlcharacter-for-character —validate_draftchecks this. When that slot is shared by several kernel functions, also setkernel_functionto the exact inventory name (the gate rejects a shared slot without it and lists the choices); it's optional when the slot is unique. - Exactly one of
source.extract/transformper http-json function. Transform SQL: single SELECT, single scalar, only therawtable (structural exfiltration guard). - New source hosts require a
registry/_allowlist.txtaddition, and it must land in an EARLIER PR than the manifest that uses it -- not the same one.validate.ymlanddry-run.ymlboth read the allowlist from the BASE branch, never the PR head, so a host added in the same PR is not yet trusted when its metric is checked: the live dry-run reportsegress not allowed (host ... is not on the provisioning allowlist)and the host cannot be proven at all. Land the host first, then the metric. - Any field added to
Manifest/FunctionSpecMUST be classified insrc/fpm/governance/fields.py::FIELD_BUCKETS—tests/test_governance_fields.pyfails otherwise. That one map is what the goalpost diff compares AND what selects functions for the live dry-run, so an unclassified field would silently escape both.trivialmeans a change provably cannot alter what is measured, how the number is read, or who is accountable; the default ismaterial. - Thresholds are human commitments: the report drafter deliberately omits them; drafts
mark ours
PLACEHOLDER. Don't invent tight thresholds without probe evidence. - Secrets never enter the repo; live smokes read
OSO_API_KEYfrom the environment. - The registry is PUBLIC. Do not put quoted agreement text, money figures, DocuSign or other
contract identifiers, or characterisations of a recipient's paperwork into
registry/. Where a bar is absent, say why withsla.unscored_reason(an enum) and keep the reasoning incontracts/<team>.facts.yaml, which is gitignored.contracts/being absent is the normal state for a collaborator, so nothing in tests or CI may require it. - An outside consumer reads
filecoin.filpgf_public.*, NOT this repo's static models, anddocs/public-datasets.mdis that contract. The mart is TWO tables, kept deliberately minimal —kernel_timeseries_metrics_by_project(every fact about a reading, including the bar that day) andkernel_functions(the inventory, incl. the 14 functions nothing measures, so coverage has an honest denominator) — both public-read, and it is built by UDMs ininsights-private/projects/filecoin/models/and deployed with that repo'sscripts/deploy_models.py; every model runs@daily, so a registry change reaches the mart within a day. This repo owns only the LANDING tables it feeds:filpgf_kernel_functions(the kernel inventory, including functions nothing measures) andfilpgf_kernel_metrics(one row per SLA entry with its join keys:kernel_id,grant_ref,oso_project_slug,team,state), beside the two series below. Both are DERIVED fromregistry/byfpm.exports— regenerate withscripts/exports.py write, never hand-editdata/kernel_functions.csvordata/kernel_metrics.csv;tests/test_exports.pyfails when the committed copies disagree with the registry. Keep money, agreement terms and contract identifiers out of them:grant_refis safe (Karma issues it publicly), what a grant is worth is not. data/observations.csv(values) anddata/thresholds.csv(the bar as it stood that day) are the system of record for the time series — OSO'sfilpgf_sla_observationsandfilpgf_sla_thresholdsare full-table republishes of them. Never hand-edit either, and never write them except throughfpm.observations/fpm.thresholds, which normalize the date and dedupe the day. Compliance is NOT stored: the dashboard joins the two on (observed_at, team, function_id, metric) and derives pass/fail/unscored/indeterminate at render, so a corrected threshold fixes history instead of leaving it judged against a superseded bar.- Nothing unattended may write a verdict, and this is now ENFORCED, not just stated:
fpm.land.assert_adjudicatedrefuses any batch carryingapprover="dev-auto"(whatfpm review --dev-auto-approvestamps), whole and before publishing anything..github/workflows/observe.ymlrunsfpm observe(fetch + evaluate, no model);scripts/run_full_review.shrefreshes the observation/threshold series and lands NOTHING; adjudication staysfpm reviewwith a human. - Tests are offline-deterministic; anything live goes in
scripts/live_*_smoke.py(quarantined, never imported by tests).
- dlt auto-parses ISO strings to tz-aware timestamps → in transforms use the column
directly and compare against
:now_tz(NOTfrom_iso8601_timestamp, NOT:now). - Unix-epoch integer columns pair with the naive binds (
:now+from_unixtime). - Nested JSON arrays land in unreachable child tables; nested objects flatten to
parent__fieldcolumns. Never setmax_table_nesting: 0with nested arrays. - Trino integer division truncates — CAST to DOUBLE.
- dlt AUTO-DETECTS a paginator when the config omits one, and GitHub sends Link headers, so a
commits?per_page=30fetch walks the whole history and 403s the unauthenticated 60 req/hour budget for every later GitHub metric.build_ingestion_confignow always sendsendpoint.paginator; never drop it. A403 rate limit exceededon a metric that passes in isolation is this, and the real dlt error is in the run log'sextra.error, NOTevent(which only ever says "Data ingestion failed"). source.auth.secret_refnames an ENV VAR, not an OSO secret: OSO wants the real VALUE in the config, lifts it into its own store, and keeps a path-derived marker ({"name": "client.auth.token"}). Passing a reference name makes it authenticate as that literal string -> 401. So the provisioning host (laptop, nightly runner) must hold the credential; the repo still holds only the name. Actions secret isGH_API_TOKENbecause Actions reserves theGITHUB_prefix.- Rotating a credential does NOT change the config shape (fingerprints strip secrets, and
OSO's stored config has no value to compare), so datasets keep the OLD token and 401
silently. After any rotation run
fpm observe --reprovision. - A REST ingestion config CANNOT reference an OSO environment secret by name — tested and
rejected in all four syntaxes (
{$type:secret,name},secretName,{{ secrets.X }},{$secret}). Only a Python UDM can (context.secret("NAME")withsecrets=[...] + environment_name=...), verified live: core_limit 5000. That path is the documented ESCAPE HATCH, deliberately not adopted — arbitrary Python discards the structural exfiltration guard (single SELECT, single scalar, one boundrawtable) that makes accepting community-PR sources safe, and UDM code deploys over the API, outside CODEOWNERS and the static gate. If it is ever adopted, the UDM source must live in this repo and deploy from CI, orregistry/<team>.yamlstops being the complete answer to how a metric is computed. fpm reviewteam name =registry/<name>.yamlfilename stem.- Trino timestamp literals:
'YYYY-MM-DD HH:MM:SS'(space, no T, no offset). - A manifest may omit
sla.thresholdentirely — that is "measured, not scored", and it is the honest state for a team whose agreement is missing or whose signed §3 still says "(to confirm)". Do NOT invent a number to fill the slot. Setsla.threshold.sourcetosigned-appendixonly when you have read it in the signed appendix; it defaults toprovisionaland the dashboard labels provisional bars as such.
src/fpm/ pipeline (manifest, provision, adapters, evaluate, observe, observations, thresholds,
detectors, synthesize, pipeline, store, land, report/, governance/, transform/, kernel, drafts) ·
tests/ mirrors it · registry/ the trust anchor · fixtures/ offline responses ·
dashboards/propgf-kernel-health.py (marimo, uv sync --extra dashboards) ·
docs/ guides (public-datasets.md is the consumer-facing contract; the grant-commitments
appendix + per-plan design docs are gitignored, local only).