The call that doesn't happen.
Preflight checks the call flow your server is about to serve through the Vonage Voice API. Its create-call gateway refuses requests that fail an encoded structural check, before forwarding them to the carrier. The checks cite selected federal and Georgia telemarketing provisions; they do not determine whether a call is lawful. No language model decides. Three-valued monitors report true, false or inconclusive, and strict policy holds an inconclusive request.
Live: the site at preflight-web-nine.vercel.app (the sandbox runs the engine in your browser; every number on the page is read from the host on load), the cockpit at /app/, the API at preflight-api-rc34.onrender.com, the CLI on npm, and a verification itinerary, with browser-only checks and terminal prerequisites listed.
Demo: Preflight: See the broken path before dialing. The gateway run, silent iPhone footage and separately labelled browser-call audio are distinct evidence.
Built for the DIALED IN Builder Challenge (CreateHER Fest x Vonage, Atlanta cohort). This README describes this source revision; deployments and the published CLI may lag it. Dated live evidence and remaining limits are listed under Honest status.
Inspect the failing path, correct the flow, and retry. A request refused by the gateway is not sent to the carrier.
An untraced call-flow branch can omit an identification or opt-out step. Checking a destination number alone does not inspect those branches. Preflight gives the developer a graph of observed actions and a failing path to inspect. Its reference application contains a deliberate timeout defect so the checker and the corrected flow can be tested side by side. Consent, exemptions and legal applicability still require separate assessment.
Three fields change in your Vonage application: answer_url, event_url and fallback_answer_url
point at Preflight instead of at your server, and Preflight is told where your real server lives.
Outbound creation requests must also go through POST /v/calls for pre-dial enforcement. Changing
the webhooks alone cannot prevent dialing. On the answer and branch paths:
- Vonage calls Preflight with a signed JWT. Preflight verifies the signature (HS256 against the
account signature secret, selected by the
api_keyclaim, with the payload hash checked) and rejects anything unsigned with 403 before touching any state. - Preflight forwards the request to your real answer URL, removing its private placement correlation parameter when present. The origin latency measures time to response headers, not the complete body download; the gateway's verification metric can include body-read time. Outbound calls go through the create-call gateway instead, because the platform fires the answer webhook only once a call is answered.
- Your server responds with its NCCO. Preflight parses it into typed actions and reads the atom vocabulary off each one (speaks, synthetic, identifies, offers_optout, connects_human).
- Preflight resolves facts about the person on the line: state, rate center and a line-type prior from the NANPA central office code file, the timezone from the number prefix, and whether the call falls inside 8am to 9pm at their location.
- Every armed monitor runs over the path. Verdicts are true, false or inconclusive.
- All true: the origin's bytes pass through untouched, except that an input or notify callback is rewritten to route through Preflight so the object it returns is observed too. Any false: the call is blocked and a safe object is returned that names the rule and the citation. Any inconclusive under strict policy: the call is held, because a monitor that cannot decide does not guess. A branch nobody has observed yet is inconclusive until it has been.
- Every decision is appended to a hash-chained evidence log whose head is sealed to Sigstore Rekor once a day, so a third party who does not trust the operator can verify it.
A create-call request and a running application go in. The gateway blocks a request when the checked flow violates an armed property, holds uncertain requests under strict policy, and forwards passing requests to Vonage. The evidence log records the decision and placement result. The developer reads the failing action path, fixes the flow, and tries again. Calls that bypass the gateway cannot be stopped before dial; answer and branch hooks check them after placement.
Tier 1 is mechanically verified from the call-control object plus number facts, armed by default, and
a false verdict blocks the call. The formulas are LTL over the atom vocabulary and live in
packages/engine/src/properties.ts.
| ID | Property | What Preflight checks, structurally | Citation |
|---|---|---|---|
| P1 | Calling hours | The call is initiated inside 8am to 9pm at the destination, resolved from the number prefix to a timezone against the call timestamp. Every call, not only its spoken actions: a flow that goes straight to a live agent at 6am is still initiated at 6am. within_hours |
47 CFR 64.1200(c)(1) |
| P2 | Identification present | No synthetic speech with no live human leg occurs strictly before the declared identification beat. (!(speaks & synthetic & !connects_human)) W identifies |
47 CFR 64.1200(b)(1) |
| P3 | Interactive opt-out present | From the identification beat, an input declared as the opt-out handler or a connection to a live endpoint is reachable later on the path. G( identifies -> F (offers_optout | connects_human) ) |
47 CFR 64.1200(b)(3) |
| P4 | Caller ID integrity | A valid, non-suppressed caller id is set on the call. A fact about the call, decided at its first action. caller_id_present |
O.C.G.A. 46-5-27(g)(2); Ga. Comp. R. & Regs. 515-14-1-.03(c) |
| P5 | Georgia identification first | Nothing is spoken strictly before the declared identification beat. Position, not presence. (!speaks) W identifies |
O.C.G.A. 46-5-27(g)(1); Ga. Comp. R. & Regs. 515-14-1-.03(b) |
Two of these atoms come from what the developer declares about their own flow (which spoken beat identifies the caller, which input collects a do-not-call request), matched structurally by phrase, stream URL or event URL. Preflight never judges whether the words spoken are truthful. Without a declaration nothing identifies and nothing offers opt-out, which is the fail-closed default.
The sentence that appears in the interface. Preflight verifies the structure of your call flow against a published set of rules. It does not verify consent, business relationships, or the content of what is spoken. It is a compliance tool, not legal advice, and it does not create an attorney-client relationship. Coverage is reported as endpoints observed, and a branch never exercised has never been checked.
Every row names the file where the behavior lives. Nothing in this table is a scaffold.
| Component | Shipped behavior | Where |
|---|---|---|
| Signed-webhook ingress | HS256 verification with @vonage/jwt, secret selected by api_key, payload hash checked, 403 before any state is touched |
apps/api/src/vonage/verifyWebhook.ts |
| Origin forwarder | Byte-exact pass-through, origin latency timed separately, fail-closed safe object on timeout | apps/api/src/proxy/forward.ts, apps/api/src/server.ts |
| NCCO parser | Typed actions for talk, stream, input, connect, notify, record, conversation, pay; never throws; every defect is an issue with a path; an untypable action stays in position as unknown |
packages/engine/src/ncco/parse.ts |
| Atom extraction | The five action atoms and four call atoms, declaration-driven, unresolved facts stay null | packages/engine/src/ncco/atoms.ts |
| LTL3 monitor construction | Hand-built from Bauer, Leucker and Schallhart (2011): LTL to Büchi by the Gerth, Peled, Vardi, Wolper tableau, per-state emptiness, subset construction of the property and its negation, product, three-valued labelling, Moore minimisation. One table lookup per step. Zero dependencies. | packages/engine/src/ltl/ |
| Properties and evaluator | P1 to P5 compiled once per process; a path evaluates to verdicts plus the exact witness path on any false; open branches hold; terminal paths get the definite end-of-flow verdict | packages/engine/src/properties.ts, packages/engine/src/evaluate.ts |
| Number facts | 204,776 NPA-NXX rows from the NANPA central office code file with state, rate center, operating company and a line-type prior; timezone by longest prefix from libphonenumber's map (2,046 entries); calling hours are three-valued when a prefix spans zones | packages/numfacts/ |
| Identity Insights lookup | A hold the free tables could not resolve (timezone unknown or split at that instant) schedules one paid Identity Insights request after the response has gone out; the answer is cached per line, the next decision for that line reads the platform's time zones and carrier network type through the resolver (hours_basis: <zone> by Identity Insights, line type at high confidence), and the held row shows where the lookup stands. Bounded per day, one in flight per line, a failed lookup not retried for six hours, off unless IDENTITY_INSIGHTS=on and the application key is present. Never inside a decision |
packages/numfacts/src/identityInsights.ts, apps/api/src/insights/lookups.ts, apps/api/src/store/insightStore.ts |
| Rate properties P6 to P8 | GET /api/campaign folds the stored event webhooks for a window (default the last thirty days) into per-call telemetry and computes the Tier 2 figures with the denominators the rules name: abandonment over calls answered by a person (machine-answered calls excluded; a call without machine detection counts as a person, and the basis says so), ring duration over unanswered calls with a ringing and an end time, and the platform's twelve-second line over answered calls. Each carries a verdict, the figure with its unit (a fraction, or seconds for the P8 median), the count it stands on and a one-sentence basis; an empty window is inconclusive, never guessed. Only ended outbound dials count, the far end of a connect is not a dial, and a person counts as connected only when another leg of the same conversation was answered, never from the planned connect alone |
packages/engine/src/rates.ts, apps/api/src/campaign.ts |
| Decision layer | Pass, block or hold per call over every observed path from here, prefixed by what the call already executed; the person on the line is the callee of an outbound call or the caller of an inbound one; strict policy holds on inconclusive, advisory passes with a warning; an object that is not an NCCO is blocked under either | apps/api/src/decide/flow.ts |
| Passive graph discovery | Every served object merges into a transition system (nodes per action, sequential, branch and continue edges, observation counts); paths from a node end terminal, open or cyclic; coverage reports declared endpoints observed, states, edges, branch points and open branches | packages/engine/src/graph/, apps/api/src/store/graphStore.ts |
| Declared-versus-actual diff | The developer declares, per endpoint, the action sequences they believe it serves (Setup: GET /api/setup, PUT /api/setup/declaration, both behind the dashboard token; every change names who made it and is an evidence-log entry carrying the declaration's hash, and the next decision uses it without a restart). GET /api/flow colours every discovered node declared or undeclared, names the undeclared ones that speak synthetically, and lists the declared endpoints and actions discovery has never seen |
packages/engine/src/graph/diff.ts, apps/api/src/store/declarationStore.ts, apps/api/src/server.ts |
| One-click install and rollback | From Setup, POST /api/setup/install (dashboard token) reads the application through the Application API with the account credentials the person enters, records its current answer, event and fallback hooks, points all three at this host with signed callbacks on, reads the application back and reports success only when the read-back matches what was written; POST /api/setup/rollback writes the recorded hooks back the same way. Both are evidence-log entries carrying the hooks before and after; the credentials are kept nowhere |
apps/api/src/setup/application.ts, apps/api/src/server.ts |
| Held-queue push notifications | With VAPID keys configured, a hold under strict policy is pushed to every subscribed phone after the response has gone out (the decision never waits on a push service): the number masked, the first inconclusive property and its reason, a link to the row. GET /api/push/vapid serves the public key; subscribing, unsubscribing and a test push (/api/push/test, the pipe proven end to end on a real phone) need the dashboard token; a push service answering 404 or 410 retires that subscription; the table is bounded (PUSH_SUBSCRIPTIONS_MAX) and no send waits more than ten seconds. The page that subscribes a phone is /phone/ on the web app |
apps/api/src/push/notify.ts, apps/api/src/push/routes.ts, apps/api/src/store/pushStore.ts |
| Softphone tokens | POST /api/softphone/token mints a Client SDK user token from the application's private key: a judge token (public, capped per day by a durable slot taken under a database lock before the platform is asked and released if it refuses, a fresh judge- user created through the Users API) so a person with no phone at hand places the demonstration call from the page, or the scheduler's token (dashboard token) so the fixed flow's live leg is answered in the browser. Each is the application token plus a subject and the ACL Vonage's own backend guide gives a voice user, with a short life. The application carries the RTC capability (scripts/vonage/enable-rtc.mjs); RTC events land on /v/rtc and are not stored. The page that runs the softphone is /phone/ on the web app |
apps/api/src/softphone/routes.ts, scripts/vonage/enable-rtc.mjs |
| Web app | One Vite app, three entries, deployed to the dedicated Vercel project preflight-web: the public site (the hero draws the reference flow from the engine in the browser and lights the branch that breaks 47 CFR 64.1200(b)(3); live counters, the ledger head, the last reconciliation and seal and the rate properties are read from the host on every load; the sandbox runs the same engine on a pasted object; the consent gate in three steps), the cockpit (/app/: live monitor, block detail, flow graph, held queue, evidence log, setup; the token stays in the tab's session), and the phone page (/phone/: push subscription with a service worker, the browser softphone). Motion follows the design checkpoint: a data-attribute library with a reduced-motion branch in every module |
apps/web/ |
| Branch hook | On pass, input and notify callbacks are rewritten to route through Preflight, so the replacement object (or its absence) is observed, evaluated as a continuation, and can be stopped mid-call with the safe object | apps/api/src/hooks/branch.ts |
| Create-call gateway | POST /v/calls requires an application JWT, not a Client SDK user token, verified before fetching. It checks inline NCCO or pre-fetches the configured origin; a supplied answer_url must name exactly this host's /v/answer so the live answer cannot bypass enforcement. Passing requests are forwarded to the platform; block and hold return 409 without placement. A released hold reserves at most one platform request, including when that request's result is ambiguous |
apps/api/src/gateway/calls.ts |
| Reference application | The deliberately small notification flow behind the public number: a broken mode whose menu timeout branch speaks with no opt-out, and a fixed mode with the keypress routed to the declared opt-out handler; mounted under /reference on the same host and switchable at runtime with a token, so the film's fix is one request |
apps/reference/src/index.ts |
| Held queue | A call the interlock could not decide under strict policy waits for a person; deciding it needs the dashboard token and a name, writes an override entry to the ledger, and a re-submission carrying the hold id places the call only for that destination | apps/api/src/store/holdStore.ts, apps/api/src/gateway/calls.ts |
| Consent gate | POST /api/consent/start calls the visitor's phone with a four-digit code over Verify v2's voice channel; /api/consent/check grants a single-use, fifteen-minute consent, written to the ledger with a keyed hash of the number (HMAC under the application's private key, so the public log cannot be walked back to a number) and never its digits; /api/demo/call places one call to that number through the create-call gateway with a token the process mints from its own application key, so the interlock decides it like any other call. A block does not spend the consent; a placed call does, once. Daily allowances bound what a public page can spend |
apps/api/src/consent/ |
| Decision stream | /api/stream serves decisions as server-sent events with a replay of recent ones on connect, behind the dashboard token because it carries phone numbers; the dashboard's transport |
apps/api/src/stream.ts |
| CLI | npx preflight-interlock (binary preflight): check one object, replay the labelled corpus, verify a ledger from a host or a file; one bundled file, no data tables, no account |
packages/cli/ |
| Public recompute endpoints | /api/summary (decision counts, ledger head, coverage, verify and origin latency p50 and p95, the last reconciliation), /api/coverage, /api/flow, /api/campaign, /api/ledger/head, /api/ledger/entries, /api/ledger/verify, all unauthenticated and answered cross-origin to the web app |
apps/api/src/server.ts |
| Statute text and citations | 47 CFR 64.1200 at the 2026-09-02 eCFR vintage, O.C.G.A. 46-5-27 as amended by SB 73, and PSC rule 515-14-1-.03, committed with hashes; every quoted clause is a byte-for-byte substring of its source and is either used by a property or excused with a written reason, both directions tested | packages/rules/ |
| Evidence log | Canonical JSON, sha256 hash chain from genesis, a Postgres table that refuses UPDATE and DELETE twice over (revoked grants plus a trigger), advisory-locked appends, public head, entries and verify endpoints |
packages/ledger/, apps/api/src/store/ledgerStore.ts, apps/api/src/db/migrations/0003_ledger.sql |
| Transparency-log seal | Daily workflow signs the chain head with a P-256 key, uploads a hashedrekord to Sigstore Rekor, verifies it back from the public log, and records the seal in the ledger |
.github/workflows/seal.yml, packages/ledger/keys/preflight-ledger-public.pem |
| Carrier-side reconciliation | Nightly, Reports API records for the last 26 hours are matched by UUID. Outbound records require a recorded gateway placement with platform status 201; an answer-time observation alone cannot hide a bypass. Inbound records match observed inbound calls. Unmatched calls, possible leaks near a gateway refusal and known placements missing from the pull are reported. Historical rows with unknown provenance are not assumed to be gateway placements. A mismatch fails the job; the report and last result remain in the evidence log and summary | apps/api/src/reconcile.ts, scripts/vonage/reconcile.mjs, .github/workflows/reconcile.yml |
| Event store | Every signed event webhook body persisted with its received-at timestamp, the raw material for the rate properties | apps/api/src/store/pgEventStore.ts |
flowchart TD
V["Vonage Voice platform<br/>answer_url · event_url · fallback"] -->|signed JWT| IN["1. Ingress<br/>verify HS256, 403 before state"]
IN --> FW["2. Origin forwarder<br/>timed separately"]
FW <-->|"your NCCO"| ORIGIN["Your real server"]
FW --> PARSE["3. Parser + atoms<br/>typed actions, 9 atoms"]
PARSE --> NF["4. Number facts<br/>NANPA prior · prefix timezone · calling hours"]
NF --> MON["5. Monitor bank (LTL3)<br/>P1..P5 · true / false / inconclusive"]
MON --> DEC{"6. Decision"}
DEC -->|all true| PASS["origin bytes, untouched"]
DEC -->|any false| BLOCK["safe object naming rule + citation"]
DEC -->|inconclusive, strict| HOLD["hold object; queue for a human"]
PASS --> V
BLOCK --> V
HOLD --> V
DEC -.->|every decision| LEDGER[("hash-chained evidence log<br/>Postgres, append-only")]
LEDGER -.->|daily| REKOR["Sigstore Rekor seal"]
Two decisions shape everything: the answer webhook has five seconds, and Preflight is in series with
your server inside that budget, so verification is a bounded traversal plus one local table lookup,
never a network call. And the graph of a real call flow is distributed across your webhook handlers
(an input or notify callback can return a replacement object), so it cannot be known from any
one document; an open branch is held until it has been observed.
The product specification was written before the code. Building it found ten defects, each
recorded with the check that found it in docs/fact-sheet.md:
- Answer-webhook timing. Vonage fires the answer webhook when a call is answered, so a webhook-only interlock cannot keep an outbound phone silent. Outbound calls need a create-call gateway that pre-fetches and verifies the flow before the request reaches the platform.
- The P2 and P5 formulas. The spec wrote them as
!( !identifies U speaks ), which is false on every compliant flow because the identification beat itself speaks. Weak until is the correct encoding, and the monitor test suite pins both the defect and the fix. - The Georgia subsection letters. The spec cited O.C.G.A. 46-5-27(b) for identification and (c) for caller id. Those are the definitions and the no-call prohibition. The duties are (g)(1) and (g)(2), which the citation-enforcement test now asserts against the codified text.
- The P3 formula. As printed it obliged every synthetic utterance to be followed by an opt-out,
which flagged a closing sentence after the opt-out and the spec's own declared agent path. The
rule anchors the opt-out to the identification, so P3 is
G( identifies -> F (offers_optout | connects_human) ), and the graph test replays the spec's example with the agent path passing and the untraced branch failing. - The Georgia penalty figures. The spec called them omitted and conflicting. The codified post-SB 73 text settles them: up to 2,000 USD per violation in Attorney General proceedings, actual loss or up to 1,000 USD per violation privately, no cap in a class action; the citation test asserts the wording.
- P1's scope.
G( speaks -> within_hours )checked only spoken actions, so a flow that goes straight to a live agent at 6am passed. Calling hours are a fact about the call, decided at its first action; corpus objects pin it. - The pay action. The spec left it silent to the atoms, but the platform reads a pay action's prompts aloud, so a pay with prompts speaks synthetically and, before the identification beat, breaks P2 and P5.
- P4's scope. Caller id is set on the call request, so it is a fact about the call like P1, not an always over the actions; an open path with a caller id no longer holds on P4.
- The P8 citation. "Vonage AUP item 18" names nothing: the policy page renders its prohibitions as unnumbered bullets. P8 cites the section and the page date instead.
- Inbound and in-app calls. The spec judges every call as if the application placed it. A call
the person on the line initiated (an inbound call, or a Client SDK user's leg, which arrives with
endpoint_type: "app"andfrom_userand nofromordirection) is inside calling hours by construction, and its caller id is the platform number they dialled. Found when the browser softphone's first call blocked on P4 for the wrong reason.
apps/api/ Fastify service: ingress, forwarder, decision layer, stores, ledger endpoints, migrations
apps/web/ Vite front end: the public site, the cockpit (/app/) and the phone page (/phone/); live at https://preflight-web-nine.vercel.app
apps/reference/ the deliberately non-compliant reference application (a Fastify plugin, mounted by the api)
packages/engine/ NCCO parser, atoms, LTL parser, LTL3 monitor construction, properties, evaluator
packages/numfacts/ NANPA table, prefix timezone map, calling-hours resolver, committed data + manifest
packages/ledger/ canonical JSON, hash chain, verification, the public seal key
packages/rules/ committed statute texts at a pinned vintage, verbatim quoted clauses, two-direction citation enforcement
packages/cli/ npx preflight-interlock: check, replay, verify-ledger (bundled, no dependencies)
corpus/ncco/ labelled call-control objects with expected atoms, verdicts and witness paths
scripts/ fact-sheet.ts, ai-tone-gate.sh, mutation/ (the harness), ops/ (itinerary, Render env, the web deploy), vonage/ (account and call helpers), design/ (fonts, tokens)
spike/gate1/ the measurement rig that killed the previous candidate (kept as evidence)
docs/ fact-sheet.md (the only source for any number), api.md, judges.md, adr/, design/, film/, submission/
.github/workflows/ ci.yml, seal.yml, keepalive.yml, deadman.yml, daily-call.yml, reconcile.yml, itinerary.yml, web-e2e.yml
Prerequisites: Node 22 and pnpm 10. A Postgres database is optional; without DATABASE_URL the
service runs on in-memory stores and says so on /health. Memory data is lost on restart.
git clone https://github.com/StephenSook/preflight.git
cd preflight
pnpm install --frozen-lockfileFor a credential-free local inspection, create a new .env at the repository root with only:
VONAGE_API_KEY=local-placeholder
VONAGE_SIGNATURE_SECRET=local-placeholder
ORIGIN_ANSWER_URL=http://127.0.0.1:3131/reference/answer
SELF_PING=offThese are dummy values for local inspection, not Vonage credentials. Do not set DATABASE_URL
or copy the template's placeholder database URL for this mode. No application key is present,
so the create-call gateway refuses callers and demonstration calls are disabled. The reference
route is not enabled; this setup tests the public reads and browser sandbox, not telephony.
pnpm dev:api # terminal 1; reads the root .env and resolves key paths from the root
VITE_API_URL=http://127.0.0.1:3131 pnpm dev:web # terminal 2, from the repository root
curl http://localhost:3131/health # must report store: memory
curl http://localhost:3131/api/summary # initially empty countsOpen http://localhost:5173 for the browser sandbox. The cockpit requires a dashboard token.
Without VITE_API_URL, the web development server proxies API requests to the deployed host,
not your local API. Environment variables already exported in the shell override .env values.
For real Vonage use, start from .env.example instead and fill in the account values, application
key paths and real ORIGIN_ANSWER_URL. Remove DATABASE_URL unless using Postgres. Point the
application's answer, event and fallback URLs at /v/answer, /v/event and /v/fallback on your
public host. The local placeholder setup above does not establish a working platform integration.
For Postgres integration tests, use a dedicated disposable test database owned by a login role
created with NOSUPERUSER. Set DATABASE_URL to that role's connection URL. Never use production
or a shared development database: the tests mutate data, and a superuser bypasses the ledger's
permission restrictions, invalidating its rejection tests. Without a database, integration tests
are not run (they skip locally and fail under CI); a local unit-only run is not a full-suite pass.
pnpm test # every suite, 468 tests; requires the disposable database described above
pnpm verify:engine # the engine suites alone, verbose
pnpm replay corpus/ncco # every labelled object reproduces its label, offline
pnpm ledger:verify https://preflight-api-rc34.onrender.com # recompute the live chain from genesis
pnpm --filter @preflight/numfacts fetch # refresh the number-facts tables from their sourcesCI runs on every push to main: lint, typecheck, the web app's build, the full vitest suite, an
AI-tone gate over every prose surface, the fact-sheet check (the README's counts and the recorded
mutation run must match the tree), offline call-proof guards, Chromium and WebKit UI regressions
against isolated test fixtures, and gitleaks over the full history.
The Postgres integration suites (event store, decision store, ledger, softphone slots) run against
a real database in CI and are written to fail, never skip, when DATABASE_URL is missing. Beyond
CI, scheduled jobs exercise the deployed system rather than assert it: the seal daily, the carrier
reconciliation nightly, the judge itinerary daily from a clean runner, a real call through the
gateway daily, and the web app visited daily at desktop and phone widths with axe.
The engine's own guarantees are tests, not claims:
- textbook LTL3 verdicts for G, F, U, X, R, GF, response and
G (a -> F b); - verdicts are final: once true or false, every extension keeps the verdict, over random traces;
- a formula and its negation are complementary on every prefix and at every end of flow;
- the 48-object corpus carries expected atoms, verdicts, decision and witness path per file, every label derived by hand before any run, so a reviewer checks a label by reading the object;
- a mutation harness (
pnpm mutate) applies 56 hand-written mutations one at a time (weak until turned strict, the live-human clause dropped, calling-hour boundaries moved, inconclusive collapsed to true, Büchi acceptance negated, chain links unchecked, canonical key order removed) and requires a failing test for every one; the last run killed 56 of 56; - the HTTP suite replays the spec's own example end to end: the untraced timeout branch that speaks synthetically is caught at the hook on the first call and at answer time on the next;
- the "runs in the browser" claim is a test: the engine is bundled for the browser, the bundle is
refused if it contains any Node built-in, and it parses, evaluates and diffs a flow in a bare
context with no
require,processorBuffer(the engine carries its own SHA-256, checked against node:crypto on every length across block boundaries).
The evidence log is verifiable by anyone with the URL: GET /api/ledger/verify recomputes every
hash from genesis and reports the first broken entry, if any. The Rekor seal is verified with
rekor-cli; docs/judges.md carries the recipe that ties a Rekor entry to
this ledger's head, and the host refuses a seal whose head is not an entry of its own log.
No account and no key:
curl https://preflight-api-rc34.onrender.com/health # store, decision counts, ledger head
curl https://preflight-api-rc34.onrender.com/api/summary # decisions, coverage, latency p50/p95
curl https://preflight-api-rc34.onrender.com/api/ledger/verify # recomputes every hash from genesis
curl https://preflight-api-rc34.onrender.com/api/coverage # declared versus observed endpointsThe production ledger's first seal is Rekor log index 2707993586 (entry
108e9186e8c5677a6f2d22e6fd33a4eaf81d0c5466921791cb6ead82c59202bc78ffe2d7e1e533f0), recorded back
into the ledger as entry 2 after rekor-cli verify passed in the workflow. Entry 1 is the first real
block: the reference flow refused by the gateway. Two earlier seals (log indexes 2707849371 and
2707971831) belong to the shared development database that was retired when CI got its own Neon
branch; they stay in the public log, as anything sealed there does.
rekor-cli get --log-index 2707993586 --format jsonRelease 0.2.1 is published with the September 8 malformed-input, graph-version and ledger-page fixes. Node and npm are required; its ledger and object checks run from an empty directory:
npx -y [email protected] verify-ledger https://preflight-api-rc34.onrender.com
npx -y [email protected] check my-flow.json # exit 0 pass, 2 block, 3 hold
npx -y [email protected] replay corpus/ncco # from the clone: 48 labels match, exit 0The published artifact reproduces all 48 current corpus labels, verified with an empty npm cache.
The daily itinerary checks this parity and the live ledger. pnpm replay corpus/ncco runs the same
corpus against the source checkout. Older releases do not match every current label.
| Source | Used for | Terms |
|---|---|---|
NANPA central office code assignments (reports.nanpa.com) |
state, rate center, operating company, line-type prior | public, no account; derived table committed with sha256 and file date in packages/numfacts/data/SOURCES.json |
libphonenumber resources/timezones/map_data.txt |
prefix to timezone | Apache-2.0, The Libphonenumber Authors |
eCFR (ecfr.gov) |
47 CFR 64.1200 text at a pinned date (2026-09-02), committed under packages/rules/data |
U.S. Government work |
O.C.G.A. 46-5-27, 2025 Code (via Justia) and Senate Bill 73 (2024) as signed (gov.georgia.gov) |
the Georgia identification, caller-id and liability text, post-SB 73 | statutory text; the signed act is the primary source |
| Ga. Comp. R. & Regs. 515-14-1-.03 (via Cornell LII) | the Public Service Commission's identification and caller-id rules | regulatory text |
| Vonage Voice API reference | NCCO field names, webhook shapes, signed callbacks | documentation |
docs/judges.md: a three-minute itinerary for a stranger with a terminal, executed daily.docs/api.md: every route the host serves, who may call it, what it answers.docs/adr/: the decisions with their alternatives and consequences (the create-call gateway, the free host kept warm, the hand-built three-valued engine, the declared flow).docs/fact-sheet.md: the only source for any number quoted anywhere.docs/design/: the design checkpoint with its measured inventory and the seven decisions, the token file the site's custom properties are generated from, the section copy.
What is live, what stands on little data, and what is built but not yet proven, so nobody has to guess:
- The API is live at https://preflight-api-rc34.onrender.com (Render free tier, kept warm by the keepalive workflow with a dead-man check behind it and a daily real-call job through the gateway). Signed webhooks are verified live on both legs of a real call, and the answer-webhook timing that shaped the design is measured, not assumed (docs/fact-sheet.md).
- The web app is deployed at https://preflight-web-nine.vercel.app: the public site (the sandbox
runs the engine in the browser; every number on the page is read from the host on load), the
cockpit's six screens over server-sent events, and the phone page. Both of its lines are proven
end to end in a real browser by
apps/web/tests/phone-proof.mjs: a test push delivered and shown as a notification, and a Client SDK call placed to the reference flow and decided by the interlock (docs/fact-sheet.md). On September 8, a physical iPhone also displayed the test push and placed an audible softphone call matching a P3 block in the ledger. Exact spoken wording and delivery of a newly generated hold notification on that handset were not verified. - The rate properties P6 to P8 stand on the calls this host has seen; until the scripted batch of human-answered calls runs, the figures rest on a handful of calls and the basis line says how many.
- Coverage is bounded by observed traffic. A branch never exercised has never been checked, and the interface says so in its header rather than in a footnote.
- An open path holds under strict policy. The always-properties are never true on a finite prefix while a branch is unobserved; that is the honest LTL3 answer, and the held queue is where a human decides.
- P3 can never return true on a prefix, only false or inconclusive, because no finite observation proves that an opt-out will eventually be offered. It becomes definite only when the flow ends.
- The line-type prior cannot see porting. A number ported from a landline carrier to a wireless one still shows the original code holder; every NANPA-derived fact carries confidence "low".
- A timezone from a prefix is a proxy for where the person is. Mobile numbers travel. When a prefix spans zones and they disagree at that instant, calling hours are undecided and the call holds.
- Georgia figures come from the codified text, not from commentary. After Senate Bill 73 (effective
July 1, 2024) the "knowing" requirement is gone, liability reaches the party the call is made on
behalf of, the Attorney General's civil penalty is up to 2,000 USD per violation (46-5-27(h)), and
private damages are actual loss or up to 1,000 USD per violation with no cap in a class action
(46-5-27(i)(2)). The quoted clauses live in
packages/rules/data/citations.json. - Not legal advice. Structure and position are checked; consent, business relationships and the truth of what is spoken are declared by the developer and marked unverified.
Apache-2.0. See LICENSE.