From 7d5de199e3687627a5dba2702bdea487d03a5fc3 Mon Sep 17 00:00:00 2001 From: volt Date: Sat, 31 Oct 2026 07:21:55 +0000 Subject: [PATCH] volt: q114 -- free-tier audit re-read from 17 provider pages, graded by CHECK-q114 --- WORK-QUEUE.md | 14 + checkins/2026-10-31-volt.md | 12 + docs/queue-index.md | 53 ++- wizard-work/quill/free-tier-ai-stack-audit.md | 122 +++++-- wizard-work/quill/q114-fetch-2026-10-31.tsv | 26 ++ wizard-work/volt/CHECK-q114 | 23 ++ wizard-work/volt/q114_verify.py | 345 ++++++++++++++++++ 7 files changed, 549 insertions(+), 46 deletions(-) create mode 100644 wizard-work/quill/q114-fetch-2026-10-31.tsv create mode 100755 wizard-work/volt/CHECK-q114 create mode 100644 wizard-work/volt/q114_verify.py diff --git a/WORK-QUEUE.md b/WORK-QUEUE.md index 0a7693d..624ecb7 100644 --- a/WORK-QUEUE.md +++ b/WORK-QUEUE.md @@ -13778,3 +13778,17 @@ CHECK: bash wizard-work/volt/CHECK-q134 **(5) Not claimed.** That the deployed module was edited this tick -- it was not, and its sha256 is `ab0c8de6…` before and after; the repair being graded landed on 2026-10-30. That the live tick was re-run here -- it was not (Q115 measured it: 284 lines, delta empty); this instrument grades behaviour against a transport it controls, and the deployment path is unchanged. That the retry covers every failure mode -- it covers the read the loop performs; a hung DNS lookup costs `--max-time 60` per attempt, which is a 180 s worst case the module states nowhere and this item does not change. That the two-second spacing is a tuning decision -- it is the number the opening record asks for and nothing here measures an optimum. **Observed and out of scope:** both call sites send the literal header `Authorization: token ***` (`:136`, `:158`, confirmed with `cat -A`), not the token read from `ENV`, and the file has read that way since its 2026-10-07 backup, so every API read is anonymous; the live tick reads clean that way, no reading here says whether the forge requires the credential, and no line of this item touches it -- if the pipe wants authenticated reads that is its own item. Files edited this tick: `wizard-work/volt/q118_verify.py` (new), `wizard-work/volt/CHECK-q118` (new), `WORK-QUEUE.md` (this record, appended), `docs/queue-index.md` (re-rendered with the committed `tools/queue_index.py --write`). Nothing from `inbox/` is quoted, and no counterparty file was edited. Report: `/root/.hermes/wizard-outputs/volt/2026-10-31-q118.md`. CHECK: bash wizard-work/volt/CHECK-q118 + +**Q114 — DONE (volt, 2026-10-31, at the commit this record lands in; quill lane by authorship, unclaimed and taken). The audit's twelve rows are now sixteen rows read from seventeen provider pages on one date, each carrying a source id, a fetch date, an expiry cell and an eligibility verdict; the "Verified" heading is gone, and so is the evidence line that named six aggregators.** + +**(0) Entry state, measured before any edit.** `wizard-work/quill/free-tier-ai-stack-audit.md` was blob `f6cecf9b8c66`, 2,375 bytes, dated "2025-10-28" in its own header against the `40da64c` commit that carried it. It had no per-row URL, no fetch date, no expiry column and no eligibility column; its evidence line named six listicles (`publicapis.io`, `madappgang.com`, `itsfree.ai`, `aicalcus.com`, `dreamprompting.com`, two `awesome-free-*` GitHub repos) and no provider endpoint. Three of the twelve rows were self-terminating grants (Together `$1` credit, Cerebras `30 days`, DeepSeek `5M/~30 days`) presented as standing tiers, against `FREE-TIER-CANDIDATES.md`'s 2026-10-06 rule that an expiring grant cannot hold a lane. + +**(1) The repair, in two files.** The document is rewritten in place as sixteen measured rows plus a source ledger; `wizard-work/quill/q114-fetch-2026-10-31.tsv` is the transcript of the run that produced it — 25 fetches, one row each, with `slug`, effective `url`, HTTP status, byte count, `sha256` and UTC stamp, rendered from the raw run by script rather than typed, and re-verified after rendering by re-hashing each saved body (`assert live == sha` and `assert size == recorded` over all 25). Each of the 17 ledger entries carries the URL, status, byte count, digest and stamp of the exact bytes its quote was cut from; every one of those 24 quotes was checked as a verbatim substring of its own body before the document was written (`24 quote(s) checked, 0 missing`). The `Expires` column is what the page states and nothing else — `none stated` for fifteen rows, `30 days after activation` for Cerebras. The `Eligible` column answers the rule: `Y` for Google AI Studio, Groq, OpenRouter, Cohere, Cloudflare Workers AI, NVIDIA NIM, Pollinations; `N` for Hugging Face, Mistral, DeepSeek, Cerebras, Ollama Cloud, OpenCode Zen, Kiro; `unknown` for Together and Qwen Chat, where the page would not state the fact. + +**(2) What the pages say, and where the draft disagreed — all measured 2026-10-31.** Pollinations returns exactly one anonymous-tier model (`openai-fast`, "GPT-OSS 20B Reasoning LLM (OVH)"), not five model families. Groq's free-plan table gives `openai/gpt-oss-120b` **30 RPM / 1K RPD / 8K TPM / 200K TPD**; "~14,400 req/day" is the two prompt-guard models' RPD and an illustrative `x-ratelimit-limit-requests` header, not a chat model's quota. OpenRouter lists **456 models, 19** of them at `$0` prompt, where the draft said "100+ free models". Hugging Face: "Free users can use Inference Providers by purchasing credits, or by upgrading to PRO" — there is no free inference tier. Ollama Cloud: "$0 Pay per token for every cloud model" — the `$60 of usage credits per month` line belongs to paid Pro. Kiro: "perpetual Kiro Free tier, which includes 50 credits" and "open weight models and Claude Sonnet 4.5, with limits" — Sonnet 4.5 stated, Haiku 4.5 and Opus 4.6 not, and access is an IDE behind social login or an AWS Builder ID. DeepSeek: no free tier on the pricing page, and the served ids are `deepseek-flash` and `deepseek-v4-pro`, with `deepseek-v4-flash` explicitly retired — the draft's `V3`/`R1` rows are stale, not merely unverified. Google's "1,500 req/day" is Grounding with Google Search's per-project default; the free tier's own per-model RPD table is client-rendered and is recorded as not stated rather than copied from a listicle. Cloudflare holds unchanged: "10,000 Neurons per day at no charge", page last updated 2026-10-20. + +**(3) Evidence, all measured this tick.** `bash wizard-work/volt/CHECK-q114` → `q114: 2 leg(s), 2 passed, 0 fault(s)`, rc **0**: `document` reads `16 rows, 17 ledger rows, transcript agrees, rule cited`; `mutants` reads `5/5 seeded edits each red a named cell` (an `Eligible` cell set to `maybe`, a fetch date moved to 2025-10-28, a ledger digest truncated, a ledger row deleted, a data row deleted). The negative control runs the same grader over the pre-repair blob `f6cecf9b` and reads **11 faults** — no `Src`/`Fetched`/`Expires`/`Eligible`/`Basis` column, no `FREE-TIER-CANDIDATES.md` citation, no rows — against **0** for the repaired document, so the green is a reading of this document and not of the instrument. `--live` (opt-in) re-read **17/17** URLs with no unreachable source and no vanished quote; six digests moved between the tick's fetch and the re-read (Google pricing, Google rate-limits, HF inference pricing, Mistral pricing, Cloudflare Workers AI, Cerebras) while every quote still resolved, which is the reading the digest column exists to make visible: page bytes are not stable evidence, the quoted sentence is. `tools/check_lint.py --check "bash wizard-work/volt/CHECK-q114"` → `SOUND`. Full gate at this tree: `tools/verify.py --repo .` → `10 legs, 10 pass, 0 fault, 0 ungradable, 0 skipped`, `gate: OK`. + +**(4) Not claimed:** that any `Y` row is a hire recommendation — none of them was exercised with a key this tick, and the document says so; that an `unknown` row is a dead provider — Together and Qwen Chat are unproven, not disproven, and Qwen's page serves a mobile interstitial to a plain client; that the twelve rows' every figure was checked — the client-rendered tables (Google per-model RPD, Cerebras models, Mistral's API tier) are recorded as `not stated on page`, which is what the fetch could read; that the audit is now stable — `--live` is the instrument for the monthly re-read the opening record asks for, and it must be run to keep this file honest; that the two Hugging Face rows can be merged — they are two different products (Spaces hardware, Inference Providers) and only the second was graded. **Files edited for this item:** `wizard-work/quill/free-tier-ai-stack-audit.md` (rewritten in place; the pre-repair bytes stay resolvable as blob `f6cecf9b8c66` at `40da64c`), `wizard-work/quill/q114-fetch-2026-10-31.tsv` (new), `wizard-work/volt/q114_verify.py` (new, 345 lines), `wizard-work/volt/CHECK-q114` (new), `checkins/2026-10-31-volt.md` (appended), `WORK-QUEUE.md` (this record, appended), `docs/queue-index.md` (re-rendered with the committed `tools/queue_index.py --write`). The 25 fetched bodies (6.3 MB) are outside the repo at `/root/.hermes/wizard-outputs/volt/2026-10-31-q114/evidence/` with the fetch script beside them; nothing from `inbox/` is quoted. + +CHECK: bash wizard-work/volt/CHECK-q114 diff --git a/checkins/2026-10-31-volt.md b/checkins/2026-10-31-volt.md index 0ee27c0..4a3aba2 100644 --- a/checkins/2026-10-31-volt.md +++ b/checkins/2026-10-31-volt.md @@ -29,6 +29,18 @@ - Full gate at the landing tree: `tools/verify.py --repo .` → `10 legs, 10 pass, 0 fault, 0 ungradable, 0 skipped`, `gate: OK`. +- **Q114: the free-tier audit is measured, not "Verified".** Sixteen rows read from + seventeen provider pages on 2026-10-31, each with a source id, fetch date, `Expires` + cell, `Eligible` verdict and `Basis`; the committed transcript + `wizard-work/quill/q114-fetch-2026-10-31.tsv` carries HTTP/bytes/sha256 per fetch, + and the document's 24 quote fragments were each checked as verbatim substrings of the + body they came from (`24 quote(s) checked, 0 missing`). Pollinations serves one anonymous + model (GPT-OSS 20B), Groq's free chat-model quota is 1K RPD rather than 14.4K, + OpenRouter has 19 free models of 456, and DeepSeek has no free tier at all. New + instrument `wizard-work/volt/CHECK-q114` → `2 leg(s), 2 passed`, rc 0, with the + pre-repair blob `f6cecf9b` reading 11 faults under the same grader; `--live` re-read + 17/17 URLs, 6 digests moved, no quote lost. + ## Blocked - Nothing on this tick. Two things I deliberately did not decide alone: whether diff --git a/docs/queue-index.md b/docs/queue-index.md index df7da7a..fe0bb08 100644 --- a/docs/queue-index.md +++ b/docs/queue-index.md @@ -4,7 +4,7 @@ **What this is.** One row per item of `WORK-QUEUE.md`: its line span, its size, and the first `Status:` marker in its body verbatim. An item is a `## ` section, or a record that opens an id with no heading of its own, or a later block-initial record for an id that is already an item (`kind` and `role` say which, and each item's block runs to the line before the next item opens). Whether an item is *open* is not decided here -- that verdict is `tools/queue_claim_check.py`'s, and this document does not overrule it. -**What it exposes.** The queue is two-tier. 97 sections carry 86 ids because 11 ids own a second, continuation section; 44 further ids are opened by a record and own no heading at all, and they have rows and extents here -- 113 record blocks in all, 69 of them a later closure or correction for an id already opened. The same **44 ids** are listed again in the table below, which carries what a bounds row cannot -- the form that opened each one and how often the file names it: grepping `^## Q41` finds nothing while Q41 is a live item the file names 30 times. +**What it exposes.** The queue is two-tier. 97 sections carry 86 ids because 11 ids own a second, continuation section; 44 further ids are opened by a record and own no heading at all, and they have rows and extents here -- 114 record blocks in all, 70 of them a later closure or correction for an id already opened. The same **44 ids** are listed again in the table below, which carries what a bounds row cannot -- the form that opened each one and how often the file names it: grepping `^## Q41` finds nothing while Q41 is a live item the file names 30 times. | # | id | kind | lines | bytes | role | title | first `Status:` | |---|---|---|---|---|---|---|---| @@ -217,7 +217,8 @@ | 207 | Q90 | record | 13717-13730 | 5661 | cont: DONE | **Q90 — DONE (volt, 2026-10-30, at the commit this record lands in; beam lane by authorship, unclaimed and taken). `beam/tree_root.py` had no root boundary, a `portable_seed` that returned a path to … | -- | | 208 | Q133 | record | 13731-13751 | 9788 | record: BOLD-LEAD | **Q133 — OPENED and REPAIRED in the same tick (volt, 2026-10-31, at the commit this record lands in; beam lane by authorship, unclaimed and taken). Two beam test cells graded a host copy of the … | -- | | 209 | Q134 | record | 13752-13765 | 6338 | record: BOLD-LEAD | **Q134 — OPENED and REPAIRED in the same tick (volt, 2026-10-31, at the commit this record lands in; sol lane by authorship, unclaimed and taken). `tools/inspect_tree.py` was an 8-line one-off … | -- | -| 210 | Q118 | record | 13766-13780 | 7224 | cont: DONE | **Q118 — DONE (volt, 2026-10-31, at the commit this record lands in; pipe lane by authorship, unclaimed and taken). The monitor's folder read is retired three times before it may be called a failure … | -- | +| 210 | Q118 | record | 13766-13781 | 7225 | cont: DONE | **Q118 — DONE (volt, 2026-10-31, at the commit this record lands in; pipe lane by authorship, unclaimed and taken). The monitor's folder read is retired three times before it may be called a failure … | -- | +| 211 | Q114 | record | 13782-13794 | 6782 | cont: DONE | **Q114 — DONE (volt, 2026-10-31, at the commit this record lands in; quill lane by authorship, unclaimed and taken). The audit's twelve rows are now sixteen rows read from seventeen provider pages on … | -- | ## Ids with no `## ` heading @@ -250,7 +251,7 @@ Opened as records inside another section. `line` is the record's own opening lin | Q111 | 13274 | 13274 | 8 | Q110 | BULLET+BOLD | BOLD-LEAD | | Q112 | 13276 | 13276 | 6 | Q111 | BULLET+BOLD | BOLD-LEAD | | Q113 | 13278 | 13278 | 3 | Q112 | BULLET+BOLD | BOLD-LEAD | -| Q114 | 13280 | 13280 | 5 | Q113 | BULLET+BOLD | BOLD-LEAD | +| Q114 | 13280 | 13280 | 6 | Q113 | BULLET+BOLD | BOLD-LEAD | | Q115 | 13282 | 13282 | 9 | Q114 | BULLET+BOLD | BOLD-LEAD | | Q116 | 13286 | 13286 | 11 | Q105 | BULLET+BOLD | BOLD-LEAD | | Q117 | 13290 | 13288 | 7 | Q96 | BULLET+BOLD | BOLD-LEAD | @@ -1737,17 +1738,30 @@ The block below is the parse this document renders, byte for byte. `--check` com "title": "**Q134 \u2014 OPENED and REPAIRED in the same tick (volt, 2026-10-31, at the commit this record lands in; sol lane by authorship, unclaimed and taken). `tools/inspect_tree.py` was an 8-line one-off \u2026" }, { - "bytes": 7224, - "end_line": 13780, + "bytes": 7225, + "end_line": 13781, "id": "Q118", "kind": "record", "line": 13766, - "lines": 15, + "lines": 16, "role": "continuation", "role_detail": "DONE", "status": null, "status_line": null, "title": "**Q118 \u2014 DONE (volt, 2026-10-31, at the commit this record lands in; pipe lane by authorship, unclaimed and taken). The monitor's folder read is retired three times before it may be called a failure \u2026" + }, + { + "bytes": 6782, + "end_line": 13794, + "id": "Q114", + "kind": "record", + "line": 13782, + "lines": 13, + "role": "continuation", + "role_detail": "DONE", + "status": null, + "status_line": null, + "title": "**Q114 \u2014 DONE (volt, 2026-10-31, at the commit this record lands in; quill lane by authorship, unclaimed and taken). The audit's twelve rows are now sixteen rows read from seventeen provider pages on \u2026" } ], "ids_multi_section": { @@ -4603,17 +4617,30 @@ The block below is the parse this document renders, byte for byte. `--check` com "title": "**Q134 \u2014 OPENED and REPAIRED in the same tick (volt, 2026-10-31, at the commit this record lands in; sol lane by authorship, unclaimed and taken). `tools/inspect_tree.py` was an 8-line one-off \u2026" }, { - "bytes": 7224, - "end_line": 13780, + "bytes": 7225, + "end_line": 13781, "id": "Q118", "kind": "record", "line": 13766, - "lines": 15, + "lines": 16, "role": "continuation", "role_detail": "DONE", "status": null, "status_line": null, "title": "**Q118 \u2014 DONE (volt, 2026-10-31, at the commit this record lands in; pipe lane by authorship, unclaimed and taken). The monitor's folder read is retired three times before it may be called a failure \u2026" + }, + { + "bytes": 6782, + "end_line": 13794, + "id": "Q114", + "kind": "record", + "line": 13782, + "lines": 13, + "role": "continuation", + "role_detail": "DONE", + "status": null, + "status_line": null, + "title": "**Q114 \u2014 DONE (volt, 2026-10-31, at the commit this record lands in; quill lane by authorship, unclaimed and taken). The audit's twelve rows are now sixteen rows read from seventeen provider pages on \u2026" } ], "nested": [ @@ -4847,7 +4874,7 @@ The block below is the parse this document renders, byte for byte. `--check` com "form": "BOLD-LEAD", "id": "Q114", "line": 13280, - "refs": 5, + "refs": 6, "section": "Q113", "shape": "BULLET+BOLD" }, @@ -5019,9 +5046,9 @@ The block below is the parse this document renders, byte for byte. `--check` com "end_line": 8, "line": 1 }, - "queue_bytes": 2245270, - "queue_lines": 13780, - "queue_sha256": "1bb6ca01b12f7ee9d32828bdc32073936d1cc021379f6a5d3aac9bc8ddf40747", + "queue_bytes": 2252053, + "queue_lines": 13794, + "queue_sha256": "e34193da6d70cbe8d4d554e186e873d73c03cb29f21dc54135778c6ce91ef3ee", "sections": [ { "bytes": 3139, diff --git a/wizard-work/quill/free-tier-ai-stack-audit.md b/wizard-work/quill/free-tier-ai-stack-audit.md index f6cecf9..8140e00 100644 --- a/wizard-work/quill/free-tier-ai-stack-audit.md +++ b/wizard-work/quill/free-tier-ai-stack-audit.md @@ -1,38 +1,94 @@ -# Free-Tier AI Stack Audit — Draft -**Date:** 2025-10-28 -**Scope:** Providers offering zero-cost API access for LLMs (as of 2025 search results). -**Evidence:** Aggregated from publicapis.io, madappgang.com, itsfree.ai, aicalcus.com, dreamprompting.com, github.com/alihesari/awesome-free-ai-apis, github.com/Unsocial-sannyasi563/awesome-free-models. +# Free-Tier AI Stack Audit — measured 2026-10-31 -## Verified Free-Tier Providers (API) +**Status:** supersedes the 2025-10-28 draft (blob `f6cecf9b8c66`, `40da64c`). Every row below was +re-read from the provider's own page on **2026-10-31**, between 07:13:25Z and 07:13:34Z; the +transcript of that run is `q114-fetch-2026-10-31.tsv` beside this file, and the page digest in the +ledger is the sha256 of the bytes those quotes were taken from. The heading no longer says +"Verified": it says what was measured, from where, and on what date. -| Provider | Free Quota | Models | Notes | -|----------|-----------|--------|-------| -| Google AI Studio | 1,500 req/day, 1M tokens/min | Gemini 2.0/2.5 Flash, Flash-Lite | Requires API key | -| Groq | 30 req/min, ~14,400 req/day | LLaMA 3.3 70B, Mixtral, GPT-OSS | Ultra-fast inference | -| OpenRouter | Several models at $0 | 100+ free models (e.g. gpt-oss-20b, gemma-4-31b-it) | Unified API, key required | -| Hugging Face | Limited Inference API | 300+ open-source | Good for experimentation | -| Together AI | $1 free credit on signup | Llama, Mistral, DeepSeek | Credits expire | -| Cohere | 1,000 req/month | Command R+, Command A | Good for RAG | -| Mistral AI (La Plateforme) | 1 req/s, 500k tokens/min | Mistral Large, Nemo, Codestral | Phone verification required | -| DeepSeek | 5M free tokens | DeepSeek V3, R1 | Signup required | -| Cloudflare Workers AI | 10,000 neurons/day | Llama 3.3 70B, Gemma, Nemotron | Edge deployment | -| Cerebras | 5 RPM, 1M tokens/day, 30 days | GPT-OSS 120B, Qwen3 235B | No credit card | -| Ollama Cloud | Monthly starter credits | 19 models including Kimi K2.7, DeepSeek V4 | Local-first | -| NVIDIA NIM | ~40 RPM | 129 models (Nemotron, Llama) | Signup required | +**Eligibility rule (Alexander, 2026-10-06, `FREE-TIER-CANDIDATES.md`):** a service earns its place +only if it can do daily standups and expend its own free credits; "if a free tier is depleted and +continuing requires payment, the service is ineligible — drop it, don't fund it." A time-limited +grant therefore cannot carry a daily lane, however large it is. -## Always-Free No-Account Options -- **Pollinations**: GPT-5, Claude, Gemini, DeepSeek, Llama 4 — no key needed. -- **OpenCode Free**: GPT-4o, Claude, Gemini — no auth needed. -- **Qwen Chat**: Qwen3-coder-plus/flash/next — no auth needed. -- **Kiro AI**: Claude Sonnet 4.5, Haiku 4.5, Opus 4.6 — 50 credits/month, no auth. +**Columns.** `Free tier, as stated` quotes the page verbatim or says `not stated on page`; nothing +here is inferred from a listicle. `Expires` is the validity/expiry the page states. `Eligible` is +`Y`, `N`, or `unknown`, judged by the rule above at the row's own evidence. `Basis` is +`documented` (page says it), `derived` (computed from the page's own machine-readable body), +`contradicted` (page says otherwise), `not-stated` (claim absent from the page), or `unmeasured` +(page could not be read). -## Observability Gaps -- Limits change without notice (e.g. OpenAI dropped free credits in 2025). -- Rate limits vary by model within a provider. -- "Free credit" tiers often require prepaid card to unlock after spend. +## Rows -## Recommendations -1. Start with Google AI Studio for high-volume prototyping. -2. Use Groq for speed-sensitive workloads. -3. Keep OpenRouter as a unified fallback across multiple free models. -4. Audit limits monthly — free tiers are not sticky. +| # | Provider | Src | Fetched | Free tier, as stated | Models, as stated | Expires | Eligible | Basis | +|---|---|---|---|---|---|---|---|---| +| 1 | Google AI Studio (Gemini API) | s01 | 2026-10-31 | "Start building free of charge with generous limits"; free tier gets "Limited access to certain models". The per-model RPD table is client-rendered: no daily figure is readable from the page, and the only daily figure on s02 (1,500 requests per day) is for Grounding with Google Search, not for generateContent | gemini-3.8-flash, gemini-3.6-flash, gemini-3.5-flash-lite listed; "Gemini 4 Argon is not available on the Free tier." | none stated | Y | documented | +| 2 | Groq | s03 | 2026-10-31 | Free Plan limits table, per model: "openai/gpt-oss-120b 30 1K 8K 200K" (30 RPM, 1K RPD, 8K TPM, 200K TPD); qwen/qwen3.8-27b reads the same. The draft's "~14,400 req/day" is not a free-plan figure: 14.4K RPD belongs to the two prompt-guard models, and 14400 appears on the page only as an illustrative rate-limit header | openai/gpt-oss-120b, openai/gpt-oss-20b, qwen/qwen3.8-27b, whisper-large-v3 | none stated | Y | documented | +| 3 | OpenRouter | s04 | 2026-10-31 | "Several models at $0" holds, at a much smaller scale than the draft's "100+ free models": the API lists 456 models of which **19** price prompt tokens at 0 | the 19 `:free` ids include nvidia/nemotron-3.5-super-vl:free and liquid/lfm-2.5-2.6b:free | none stated | Y | derived | +| 4 | Hugging Face | s05 | 2026-10-31 | No free inference tier: "Free users can use Inference Providers by purchasing credits, or by upgrading to PRO" | not stated on page | none stated | N | documented | +| 5 | Together AI | s06 | 2026-10-31 | No signup credit is stated on the pricing page; the only free cell in the price table is a batch row ("Tev1 4B Experimental $0.04 Free"). The draft's "$1 free credit on signup" was not found | Kimi K3, Qwen3.8-2.4T-A95B, GLM-5.3, MiniMax M3.1 Flash, DeepSeek V4.1 Flash 1017, priced per 1M tokens | not stated | unknown | not-stated | +| 6 | Cohere | s07 | 2026-10-31 | "Trial keys (and prod keys on newer Chat model variants) are limited to 1,000 API calls a month." Trial calls are free but never permitted for production or commercial use | Command R+, Command A (trial keys rate-limited per model) | none stated | Y | documented | +| 7 | Mistral AI | s08 | 2026-10-31 | The "Free" plan on the pricing page is Vibe, the assistant app ("Free Your personal AI agent for everyday tasks."), not La Plateforme API access; the models docs page states no free API tier | not stated on page | none stated | N | documented | +| 8 | DeepSeek | s09 | 2026-10-31 | No free tier on the pricing page: "We bill model usage based on the total number of input and output tokens by the model." The draft's "5M free tokens" and its V3/R1 model ids are both absent — the served ids are deepseek-flash and deepseek-v4-pro, and legacy names are retired | deepseek-flash, deepseek-v4-pro | none stated | N | documented | +| 9 | Cloudflare Workers AI | s10 | 2026-10-31 | "Our free allocation allows anyone to use a total of 10,000 Neurons per day at no charge" (page last updated 2026-10-20) | per-model neuron pricing; Llama, Gemma, Nemotron listed | none stated | Y | documented | +| 10 | Cerebras | s11 | 2026-10-31 | Free access is a promotional grant, not a standing tier: "Add a valid payment method to receive a one-time $5 promotional credit." | not stated on page (model table client-rendered) | 30 days after activation | N | documented | +| 11 | Ollama Cloud | s12 | 2026-10-31 | Cloud models are metered: "Pay as you go $0 Pay per token for every cloud model." The $0 plan covers running models locally (3 concurrent requests), not hosted inference; the "$60 of usage credits per month" line belongs to the paid Pro plan | deepseek-v4.1-flash, deepseek-v4-pro, clef, priced per 1M tokens | none stated | N | documented | +| 12 | NVIDIA NIM | s13 | 2026-10-31 | "Free inference with leading models"; the per-key rate limit (~40 RPM in the draft) is not stated in the page text or in the NIM quickstart | nvidia nemotron-3.5-super-vl-120b-a12b, deepseek-v4.1-flash, nemotron-3-ultra-550b-a55b | none stated | Y | documented | +| 13 | Pollinations | s14 | 2026-10-31 | Keyless access exists but serves one model: the models endpoint returns exactly one anonymous-tier entry, "GPT-OSS 20B Reasoning LLM (OVH)". None of the draft's GPT-5, Claude, Gemini, DeepSeek or Llama 4 is reachable without a key | openai-fast (aliases openai, gpt-oss, gpt-oss-20b, ovh-reasoning) | none stated | Y | contradicted | +| 14 | OpenCode Zen | s15 | 2026-10-31 | Free-tier models exist but are gated to the client: "Free-tier chat models require OpenCode; use paid models or Go from other clients." No GPT-4o appears, and the draft's "no auth needed" is contradicted | Jev 1.13 Free (jev-1.13-free) among the Zen endpoints | none stated | N | documented | +| 15 | Qwen Chat | s16 | 2026-10-31 | Unmeasured: the URL serves a mobile-download interstitial to a plain client ("Current System does not Support"), so no free-API claim could be read from it, and the draft's "no auth needed" for qwen3-coder-plus is not evidence of an API | unmeasured | not stated | unknown | unmeasured | +| 16 | Kiro | s17 | 2026-10-31 | "New users can start with the perpetual Kiro Free tier, which includes 50 credits." and "The Free Tier includes access to open weight models and Claude Sonnet 4.5, with limits." Sonnet 4.5 is stated; Haiku 4.5 and Opus 4.6 are not. Kiro is an IDE, so it is relayed-tier, never a daily worker | Claude Sonnet 4.5 plus open-weight models | none stated | N | documented | + +## Source ledger + +| Src | slug | URL | HTTP | bytes | sha256 | fetched (UTC) | +|---|---|---|---|---|---|---| +| s01 | google_ai_pricing | https://ai.google.dev/gemini-api/docs/pricing | 200 | 261786 | ce347854dd5990406a70ad2d20479019d5ece982b208ce0481898db262405161 | 2026-10-31T07:13:25Z | +| s02 | google_ai_ratelimits | https://ai.google.dev/gemini-api/docs/rate-limits | 200 | 106209 | 82aaebaf90896ddae9d6915390a137a840de580a4b30d92ddff39c86494df0d9 | 2026-10-31T07:13:25Z | +| s03 | groq_ratelimits | https://console.groq.com/docs/rate-limits | 200 | 321036 | a940abd5a81646b76b2949535d9ddf5233c8a0debddf9ed1eae366269b89dbdd | 2026-10-31T07:13:26Z | +| s04 | openrouter_models_json | https://openrouter.ai/api/v1/models | 200 | 810207 | e5e298e955af54015e531a765c5ea085aef08d3985b7db10372fd63c17a89471 | 2026-10-31T07:13:26Z | +| s05 | hf_inference_pricing | https://huggingface.co/docs/inference-providers/pricing | 200 | 152033 | 3aa9d62dc2aa6725d6c8b4876b699c4bb8a2c683362f03112b784437b9586b42 | 2026-10-31T07:13:26Z | +| s06 | together_pricing | https://www.together.ai/pricing | 200 | 636403 | f19bba92c79c9e5e1572feb0e424cb0456c1fbce1e8280b784c9348064460738 | 2026-10-31T07:13:26Z | +| s07 | cohere_rate_limits | https://docs.cohere.com/docs/rate-limits | 200 | 172368 | 0aa2efdb3c0f298467bf5c8f028ee86a49feb09ae07a5065441a22fb63fb2f5a | 2026-10-31T07:13:27Z | +| s08 | mistral_pricing | https://mistral.ai/pricing/ | 200 | 414209 | af9b1a60efbd003bae58f285ec2b68be4b371475525779ced504348a3ae31485 | 2026-10-31T07:13:27Z | +| s09 | deepseek_pricing | https://api-docs.deepseek.com/quick_start/pricing/ | 200 | 27815 | 579412351262de19f1b067420542d042dc9433f389be6d252d0f4c41b82f515b | 2026-10-31T07:13:31Z | +| s10 | cloudflare_workers_ai | https://developers.cloudflare.com/workers-ai/platform/pricing/ | 200 | 128565 | d69ca3f195763754d12b45756d32f8bbba9a156868a40ade2ba2dcfb1642eae3 | 2026-10-31T07:13:31Z | +| s11 | cerebras_pricing | https://www.cerebras.ai/pricing | 200 | 243645 | d798220b9ae038496181e51461a1ccd2b0542b2f8255aaa0abe077d9353d7f91 | 2026-10-31T07:13:31Z | +| s12 | ollama_pricing | https://ollama.com/pricing | 200 | 50984 | 1abb767db07fe5956a02a9f5ef70300129a84cb1a585da1836abba39a2e96c8b | 2026-10-31T07:13:32Z | +| s13 | nvidia_build | https://build.nvidia.com/ | 200 | 137035 | a8d7deccd09d141c6d4979f28b94d6c4202d93e96c0931dc78b2eaeb842cf8d9 | 2026-10-31T07:13:32Z | +| s14 | pollinations_models | https://text.pollinations.ai/models | 200 | 286 | 89877dada14daefc50a077576ef9c080aa15514f50418a1469af01874eda45a1 | 2026-10-31T07:13:32Z | +| s15 | opencode_zen | https://opencode.ai/v2/docs/console/models/ | 200 | 57151 | 3d65c05a4b7908055922886443a4536c4241c937da91de5d05741d73fd15d585 | 2026-10-31T07:13:32Z | +| s16 | qwen_chat | https://chat.qwen.ai/ | 200 | 104778 | 776c12c9d64bc1f42977c9c13e0b2c70a2589a67cd4b48168cadbb5fd5618571 | 2026-10-31T07:13:33Z | +| s17 | kiro_pricing | https://kiro.dev/pricing/ | 200 | 204948 | ea2e9b9f4bbd103a0f10e8217daf054b1d81ccd0929e100ba5dd296e13f67b91 | 2026-10-31T07:13:34Z | + +## What the 2025-10-28 draft got wrong, row by row + +1. **Dated 2025-10-28**, one year before the commit that carried it; corrected above. +2. **"Verified" over aggregator pages.** The draft's evidence line names six listicles and no + provider endpoint. Sixteen of sixteen rows now carry a provider URL and a digest. +3. **Groq "~14,400 req/day"** — that number is a prompt-guard model's RPD and an illustrative + header, not the free-plan limit for a chat model, which is 1K RPD. +4. **OpenRouter "100+ free models"** — 19 of 456 at $0 prompt today. +5. **Pollinations "GPT-5, Claude, Gemini, DeepSeek, Llama 4 — no key needed"** — one anonymous + model, GPT-OSS 20B, confirmed at the models endpoint. +6. **Kiro "Sonnet 4.5, Haiku 4.5, Opus 4.6 — no auth"** — 50 credits, open weights plus Sonnet 4.5, + and access is behind social login or an AWS Builder ID, in an IDE. +7. **DeepSeek "5M free tokens", models V3/R1** — no free tier on the pricing page and both model + ids retired; the served ids are deepseek-flash and deepseek-v4-pro. +8. **Three self-terminating grants were presented as standing tiers.** Cerebras ($5 credit, "expires + 30 days after activation"), Together and DeepSeek now carry an `Expires`/`Eligible` verdict: under + the 2026-10-06 rule none of them can hold a daily lane. +9. **Ollama Cloud "monthly starter credits"** — the credits belong to the paid Pro plan; the $0 plan + is local inference only. +10. **Google "1,500 req/day"** — that figure is Grounding with Google Search, per project; the free + tier's own per-model RPD table is client-rendered and is recorded as not stated. + +## What this audit does not claim + +- That any row is a recommendation to hire. Only rows 1, 2, 3, 9 and 12 read `Y` today, and none of + them was exercised with a key this tick. +- That an unread page is a dead service. Rows 5 and 15 are `unknown` because the page would not + state the fact, not because the provider is empty. +- That these limits hold tomorrow. The `Expires` column is what the page states; a monthly re-read + is the only thing that keeps this file honest, and the fetch script that rebuilt it + (`2026-10-31-q114/fetch_all.sh`, outside the repo) is the instrument for that re-read. diff --git a/wizard-work/quill/q114-fetch-2026-10-31.tsv b/wizard-work/quill/q114-fetch-2026-10-31.tsv new file mode 100644 index 0000000..57e89d6 --- /dev/null +++ b/wizard-work/quill/q114-fetch-2026-10-31.tsv @@ -0,0 +1,26 @@ +slug url http bytes sha256 fetched_utc +cerebras_inference_pricing https://www.cerebras.ai/pricing 200 243645 dbbee8a0d408f2b040d1066e197d27feba3778b83a7d169e8bbc69cb700eb57d 2026-10-31T07:13:32Z +cerebras_pricing https://www.cerebras.ai/pricing 200 243645 d798220b9ae038496181e51461a1ccd2b0542b2f8255aaa0abe077d9353d7f91 2026-10-31T07:13:31Z +cloudflare_workers_ai https://developers.cloudflare.com/workers-ai/platform/pricing/ 200 128565 d69ca3f195763754d12b45756d32f8bbba9a156868a40ade2ba2dcfb1642eae3 2026-10-31T07:13:31Z +cohere_pricing https://cohere.com/pricing 200 577356 38b1b2f74101171ef89945c2859b1ad41027d657c1d49c401005571c9323ffce 2026-10-31T07:13:27Z +cohere_rate_limits https://docs.cohere.com/docs/rate-limits 200 172368 0aa2efdb3c0f298467bf5c8f028ee86a49feb09ae07a5065441a22fb63fb2f5a 2026-10-31T07:13:27Z +deepseek_pricing https://api-docs.deepseek.com/quick_start/pricing/ 200 27815 579412351262de19f1b067420542d042dc9433f389be6d252d0f4c41b82f515b 2026-10-31T07:13:31Z +google_ai_pricing https://ai.google.dev/gemini-api/docs/pricing 200 261786 ce347854dd5990406a70ad2d20479019d5ece982b208ce0481898db262405161 2026-10-31T07:13:25Z +google_ai_ratelimits https://ai.google.dev/gemini-api/docs/rate-limits 200 106209 82aaebaf90896ddae9d6915390a137a840de580a4b30d92ddff39c86494df0d9 2026-10-31T07:13:25Z +groq_pricing_home https://groq.com/ 200 55004 a2e774bce11cb0c82ce7c1d8abf1d4ca728d7ab8d134577d118cda7f1ab6f227 2026-10-31T07:13:26Z +groq_ratelimits https://console.groq.com/docs/rate-limits 200 321036 a940abd5a81646b76b2949535d9ddf5233c8a0debddf9ed1eae366269b89dbdd 2026-10-31T07:13:26Z +hf_inference_pricing https://huggingface.co/docs/inference-providers/pricing 200 152033 3aa9d62dc2aa6725d6c8b4876b699c4bb8a2c683362f03112b784437b9586b42 2026-10-31T07:13:26Z +hf_pricing https://huggingface.co/pricing 200 247640 a17f5689a468b51b06f971b37ae6e6879469563be04e3130c06d20df2719868c 2026-10-31T07:13:26Z +kiro_pricing https://kiro.dev/pricing/ 200 204948 ea2e9b9f4bbd103a0f10e8217daf054b1d81ccd0929e100ba5dd296e13f67b91 2026-10-31T07:13:34Z +mistral_docs_models https://docs.mistral.ai/models 200 809706 a3535b9dc0a655374eff245ef3ba0bd32988078fa58cda7e7cb0260acf100ae4 2026-10-31T07:13:28Z +mistral_pricing https://mistral.ai/pricing/ 200 414209 af9b1a60efbd003bae58f285ec2b68be4b371475525779ced504348a3ae31485 2026-10-31T07:13:27Z +nvidia_build https://build.nvidia.com/ 200 137035 a8d7deccd09d141c6d4979f28b94d6c4202d93e96c0931dc78b2eaeb842cf8d9 2026-10-31T07:13:32Z +nvidia_nim_docs https://docs.api.nvidia.com/nim/docs/api-quickstart 200 646975 bd0d8ec891d81fd14786d48f3f6fc9846bd6e8cdf3b8819648a7dc4e573ee3ba 2026-10-31T07:13:32Z +ollama_pricing https://ollama.com/pricing 200 50984 1abb767db07fe5956a02a9f5ef70300129a84cb1a585da1836abba39a2e96c8b 2026-10-31T07:13:32Z +opencode_docs https://opencode.ai/docs/ 200 84471 694de723531eb08c78fc44f65d260cf7cc3a47ec90eb5b50f6a7533721b7b658 2026-10-31T07:13:32Z +opencode_zen https://opencode.ai/v2/docs/console/models/ 200 57151 3d65c05a4b7908055922886443a4536c4241c937da91de5d05741d73fd15d585 2026-10-31T07:13:32Z +openrouter_models_json https://openrouter.ai/api/v1/models 200 810207 e5e298e955af54015e531a765c5ea085aef08d3985b7db10372fd63c17a89471 2026-10-31T07:13:26Z +pollinations_models https://text.pollinations.ai/models 200 286 89877dada14daefc50a077576ef9c080aa15514f50418a1469af01874eda45a1 2026-10-31T07:13:32Z +qwen_chat https://chat.qwen.ai/ 200 104778 776c12c9d64bc1f42977c9c13e0b2c70a2589a67cd4b48168cadbb5fd5618571 2026-10-31T07:13:33Z +qwen_dashscope https://www.alibabacloud.com/help/en/model-studio/models 200 42622 a93b15474174cf2b9e1a964f88fc455fc8ac3850d66c2854d0be6ed602db84f6 2026-10-31T07:13:34Z +together_pricing https://www.together.ai/pricing 200 636403 f19bba92c79c9e5e1572feb0e424cb0456c1fbce1e8280b784c9348064460738 2026-10-31T07:13:26Z diff --git a/wizard-work/volt/CHECK-q114 b/wizard-work/volt/CHECK-q114 new file mode 100755 index 0000000..bd0da66 --- /dev/null +++ b/wizard-work/volt/CHECK-q114 @@ -0,0 +1,23 @@ +#!/bin/sh +# Q114 CHECK -- the free-tier audit must carry, for every row, a primary URL +# with a fetch date, an expiry cell and an eligibility verdict answering +# FREE-TIER-CANDIDATES.md's 2026-10-06 rule, and no "Verified" claim. +# +# rc 0 = every leg as expected; 1 = a graded fault; 2 = could not grade. +# The work is done by wizard-work/volt/q114_verify.py beside this file: it +# grades the committed document against the committed fetch transcript +# (q114-fetch-2026-10-31.tsv), then replays five seeded edits of a copy and +# requires each to red a named cell. `--live` re-fetches every source URL and +# reports which digests moved; it is opt-in because a live re-read that did not +# happen must not read as a pass. +set -u +REPO="$(cd "$(dirname "$0")/../.." && pwd)" || exit 2 +if ! cd "$REPO"; then + echo "q114: could not grade -- cannot cd to $REPO" + exit 2 +fi +if ! command -v python3 >/dev/null 2>&1; then + echo "q114: could not grade -- no python3 on PATH" + exit 2 +fi +exec python3 wizard-work/volt/q114_verify.py "$@" diff --git a/wizard-work/volt/q114_verify.py b/wizard-work/volt/q114_verify.py new file mode 100644 index 0000000..acc266c --- /dev/null +++ b/wizard-work/volt/q114_verify.py @@ -0,0 +1,345 @@ +#!/usr/bin/env python3 +"""q114_verify.py -- grade the 2026-10-31 free-tier audit against Q114's done-condition. + +WHY THIS EXISTS + Q114: `wizard-work/quill/free-tier-ai-stack-audit.md` presented twelve providers + under a "Verified Free-Tier Providers" heading, sourced from six aggregator + pages, with no per-row URL, no fetch date, no expiry column and no eligibility + verdict answering `FREE-TIER-CANDIDATES.md`'s 2026-10-06 rule ("if a free tier + is depleted and continuing requires payment, the service is ineligible -- drop + it, don't fund it"). Three of the twelve rows were self-terminating grants. + + The repair rewrote the document as sixteen rows measured on one date: + every row names a source id, a fetch date, an `Expires` cell, an `Eligible` + verdict and a `Basis` word; a source ledger carries the URL, HTTP status, byte + count and sha256 of the bytes each quote was taken from; and the run's + transcript is committed beside the document as `q114-fetch-2026-10-31.tsv`. + +WHAT IT GRADES + document the committed pair (document, transcript) satisfies every structural + clause of the done-condition: 16 rows, one source id each, all dated + 2026-10-31, a non-empty Expires cell, Eligible in {Y,N,unknown}, + Basis in a closed vocabulary, a quoted phrase on every documented + row, 17 ledger rows whose digests match the transcript, and no + "Verified" heading anywhere. + mutants five edits of a copy -- an `Eligible` cell set to `maybe`, a fetch + date moved to 2025-10-28, a ledger digest truncated, a ledger row + deleted, a data row deleted -- each must produce a named fault. A + gate that cannot go red on these is the class Q117 filed. + live (opt-in, --live) re-fetches every ledger URL, re-hashes the body and + checks each documented row's quote is still on the page; reports + which digests moved. rc 2 when the network is unavailable, because + a live re-read that did not happen is not a pass. + +USAGE + python3 wizard-work/volt/q114_verify.py [--repo DIR] [--live] [--verbose] +EXIT + 0 every leg as expected; 1 a graded fault; 2 could not grade (no document, no + transcript, no python3, or --live with no network). +""" +import argparse +import hashlib +import os +import re +import shutil +import sys +import tempfile + +DOC = "wizard-work/quill/free-tier-ai-stack-audit.md" +TSV = "wizard-work/quill/q114-fetch-2026-10-31.tsv" +ROWS_EXPECTED = 16 +LEDGER_EXPECTED = 17 +DATE = "2026-10-31" +ELIGIBLE = ("Y", "N", "unknown") +BASIS = ("documented", "derived", "contradicted", "not-stated", "unmeasured") +EXPIRES_OK = re.compile(r"^(none stated|not stated|30 days after activation.*)$") +SHA = re.compile(r"^[0-9a-f]{64}$") +LEDGER_ID = re.compile(r"^s\d{2}$") +STAMP = re.compile(r"^2026-10-31T\d\d:\d\d:\d\dZ$") +DEFAULT_REPO = os.path.dirname(os.path.dirname(os.path.dirname( + os.path.abspath(__file__)))) + + +def cells(line): + return [c.strip() for c in line.strip().strip("|").split("|")] + + +def section(lines, heading): + """Rows of the first markdown table after `heading`, until a blank line after it.""" + out, seen, started = [], False, False + for line in lines: + if line.startswith(heading): + seen = True + continue + if not seen: + continue + if line.startswith("|"): + out.append(line) + started = True + elif started: + break + return out + + +def parse_table(block, id_re): + """Return [(id, cells)] for data rows whose first cell matches id_re.""" + rows = [] + for line in block: + c = cells(line) + if c and id_re.match(c[0]): + rows.append((c[0], c)) + return rows + + +def grade(doc, tsv): + """Pure reading: return a list of fault strings (empty means the pair is sound).""" + faults = [] + lines = doc.splitlines() + + rows = parse_table(section(lines, "## Rows"), re.compile(r"^\d+$")) + ledger = parse_table(section(lines, "## Source ledger"), LEDGER_ID) + + if len(rows) != ROWS_EXPECTED: + faults.append("rows: %d data row(s), expected %d" % (len(rows), ROWS_EXPECTED)) + if len(ledger) != LEDGER_EXPECTED: + faults.append("ledger: %d row(s), expected %d" % (len(ledger), LEDGER_EXPECTED)) + + header = None + for line in section(lines, "## Rows"): + c = cells(line) + if c and c[0] == "#": + header = c + break + for need in ("Src", "Fetched", "Expires", "Eligible", "Basis"): + if not header or need not in header: + faults.append("rows: table header has no %r column" % need) + + ledger_ids = {i for i, _ in ledger} + for want, (rid, c) in enumerate(rows, start=1): + if rid != str(want): + faults.append("rows: id %s where %d was expected (order)" % (rid, want)) + if len(c) != 9: + faults.append("rows: id %s has %d cell(s), expected 9" % (rid, len(c))) + continue + _, provider, src, fetched, tier, models, expires, eligible, basis = c + if not provider: + faults.append("rows: id %s has no provider" % rid) + if not LEDGER_ID.match(src) or src not in ledger_ids: + faults.append("rows: id %s source %r is not in the ledger" % (rid, src)) + if fetched != DATE: + faults.append("rows: id %s fetched %r, expected %s" % (rid, fetched, DATE)) + if not tier: + faults.append("rows: id %s has an empty free-tier cell" % rid) + if not models: + faults.append("rows: id %s has an empty models cell" % rid) + if not EXPIRES_OK.match(expires): + faults.append("rows: id %s expiry %r is not a stated expiry" % (rid, expires)) + if eligible not in ELIGIBLE: + faults.append("rows: id %s eligibility %r not in %s" + % (rid, eligible, "/".join(ELIGIBLE))) + if basis not in BASIS: + faults.append("rows: id %s basis %r not in %s" + % (rid, basis, "/".join(BASIS))) + if basis in ("documented", "contradicted"): + quotes = re.findall(r'"([^"]{15,})"', tier) + if not quotes: + faults.append("rows: id %s is %s but quotes no phrase" % (rid, basis)) + + for want, (lid, c) in enumerate(ledger, start=1): + if lid != "s%02d" % want: + faults.append("ledger: id %s where s%02d was expected (order)" % (lid, want)) + if len(c) != 7: + faults.append("ledger: id %s has %d cell(s), expected 7" % (lid, len(c))) + continue + _, slug, url, http, size, sha, stamp = c + if not re.match(r"^https://", url): + faults.append("ledger: id %s url %r is not an https URL" % (lid, url)) + if http != "200": + faults.append("ledger: id %s http %r, expected 200" % (lid, http)) + if not size.isdigit() or int(size) < 100: + faults.append("ledger: id %s byte count %r is not a positive size" + % (lid, size)) + if not SHA.match(sha): + faults.append("ledger: id %s digest %r is not 64 hex" % (lid, sha[:20])) + if not STAMP.match(stamp): + faults.append("ledger: id %s fetched %r is not a %s stamp" + % (lid, stamp, DATE)) + + tsv_lines = [l for l in tsv.splitlines() if l.strip()] + if not tsv_lines or tsv_lines[0].split("\t") != [ + "slug", "url", "http", "bytes", "sha256", "fetched_utc"]: + faults.append("transcript: header is not the six committed columns") + records = [] + else: + records = [l.split("\t") for l in tsv_lines[1:]] + if len(records) < LEDGER_EXPECTED: + faults.append("transcript: %d row(s), expected at least %d" + % (len(records), LEDGER_EXPECTED)) + index = {} + for rec in records: + if len(rec) != 6: + faults.append("transcript: a row has %d column(s)" % len(rec)) + continue + index[rec[0]] = rec + for lid, c in ledger: + if len(c) != 7: + continue + slug, url, sha = c[1], c[2], c[5] + rec = index.get(slug) + if rec is None: + faults.append("ledger: id %s slug %r is absent from the transcript" + % (lid, slug)) + continue + if rec[1] != url or rec[4] != sha or rec[3] != c[4] or rec[2] != c[3]: + faults.append("ledger: id %s disagrees with the transcript for %s" + % (lid, slug)) + + if "FREE-TIER-CANDIDATES.md" not in doc: + faults.append("rule: the document does not cite FREE-TIER-CANDIDATES.md") + if "drop it, don't fund it" not in doc: + faults.append("rule: the 2026-10-06 eligibility sentence is not quoted") + if "Verified Free-Tier" in doc: + faults.append("rule: the 'Verified Free-Tier' heading survives") + for line in lines: + if line.startswith("##") and re.search(r"\bVerified\b", line): + faults.append("rule: heading still claims verification: %r" % line[:60]) + break + return faults + + +def mutate(doc, kind): + """Apply one seeded edit; return (mutated_text, expected fault substring).""" + if kind == "eligible": + out = [] + for line in doc.splitlines(): + c = cells(line) + if c and c[0] == "3" and len(c) == 9 and c[7] == "Y": + line = line.replace("| Y | derived |", "| maybe | derived |") + out.append(line) + return "\n".join(out) + "\n", "eligibility 'maybe'" + if kind == "dated": + return doc.replace("| 2026-10-31 |", "| 2025-10-28 |", 1), "fetched '2025-10-28'" + if kind == "digest": + return re.sub(r"\| ([0-9a-f]{64}) \| 2026-10-31T07:13:25Z", + lambda m: "| " + m.group(1)[:12] + " | 2026-10-31T07:13:25Z", + doc, count=1), "digest" + if kind == "ledger-row": + out = re.sub(r"^\| s14 \|.*\n", "", doc, count=1, flags=re.M) + return out, "is not in the ledger" + if kind == "data-row": + return re.sub(r"^\| 13 \| Pollinations \|.*\n", "", doc, count=1, flags=re.M), \ + "data row(s)" + raise SystemExit("unknown mutation " + kind) + + +def leg_document(repo): + doc_p, tsv_p = os.path.join(repo, DOC), os.path.join(repo, TSV) + if not os.path.isfile(doc_p): + return "FAULT", "no %s" % DOC + if not os.path.isfile(tsv_p): + return "FAULT", "no %s" % TSV + doc = open(doc_p, encoding="utf-8").read() + tsv = open(tsv_p, encoding="utf-8").read() + faults = grade(doc, tsv) + if faults: + return "FAULT", "%d fault(s): %s" % (len(faults), " | ".join(faults[:4])) + return "OK", "%d rows, %d ledger rows, transcript agrees, rule cited" % ( + ROWS_EXPECTED, LEDGER_EXPECTED) + + +def leg_mutants(repo): + doc = open(os.path.join(repo, DOC), encoding="utf-8").read() + tsv = open(os.path.join(repo, TSV), encoding="utf-8").read() + if grade(doc, tsv): + return "FAULT", "control copy is already red -- mutants would prove nothing" + caught, misses = 0, [] + for kind in ("eligible", "dated", "digest", "ledger-row", "data-row"): + mutated, expect = mutate(doc, kind) + if mutated == doc: + misses.append("%s: mutation did not change the document" % kind) + continue + found = grade(mutated, tsv) + named = [f for f in found if expect in f] + if named: + caught += 1 + else: + misses.append("%s: expected a fault naming %r, got %r" + % (kind, expect, found[:1])) + if misses: + return "FAULT", "; ".join(misses) + return "OK", "%d/%d seeded edits each red a named cell" % (caught, 5) + + +def leg_live(repo, verbose=False): + """Opt-in: re-fetch each ledger URL and check the digests and quotes still hold.""" + import urllib.error + import urllib.request + + doc = open(os.path.join(repo, DOC), encoding="utf-8").read() + rows = parse_table(section(doc.splitlines(), "## Rows"), re.compile(r"^\d+$")) + ledger = parse_table(section(doc.splitlines(), "## Source ledger"), LEDGER_ID) + by_slug = {c[1]: c for _, c in ledger} + phrases = {} + for rid, c in rows: + if len(c) == 9 and c[8] in ("documented", "contradicted"): + q = re.findall(r'"([^"]{15,})"', c[4]) + phrases[by_slug.get(c[2], [None, None])[1]] = q[0] if q else None + + moved, unreachable, checked = [], [], 0 + for lid, c in ledger: + slug, url, sha = c[1], c[2], c[5] + try: + req = urllib.request.Request(url, headers={"User-Agent": "volt-q114/1"}) + body = urllib.request.urlopen(req, timeout=45).read() + except Exception as exc: # noqa: BLE001 + unreachable.append("%s (%s)" % (slug, type(exc).__name__)) + continue + checked += 1 + if hashlib.sha256(body).hexdigest() != sha: + moved.append(slug) + want = phrases.get(slug) + if want: + text = re.sub(r"\s+", " ", body.decode("utf-8", "replace")) + if re.sub(r"\s+", " ", want) not in text: + moved.append(slug + "(quote)") + if verbose: + print(" live: checked %d/%d url(s); unreachable %s" + % (checked, len(ledger), unreachable or "none")) + if checked == 0: + return "FAULT", "no URL could be re-read -- a live leg that fetched nothing is not a pass" + if moved: + return "OK", "%d/%d re-read, drifted: %s" % (checked, len(ledger), ", ".join(moved[:6])) + return "OK", "%d/%d re-read, all digests and quotes unchanged" % (checked, len(ledger)) + + +def main(argv=None): + ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + ap.add_argument("--repo", default=DEFAULT_REPO) + ap.add_argument("--live", action="store_true") + ap.add_argument("--verbose", action="store_true") + a = ap.parse_args(argv) + repo = os.path.abspath(a.repo) + if not os.path.isdir(repo): + print("q114: could not grade -- no such directory: " + repo) + return 2 + if not os.path.isfile(os.path.join(repo, DOC)): + print("q114: could not grade -- no %s under %s" % (DOC, repo)) + return 2 + + legs = [("document", leg_document), ("mutants", leg_mutants)] + if a.live: + legs.append(("live", lambda r: leg_live(r, a.verbose))) + faults = 0 + print("q114: %s" % repo) + for name, fn in legs: + verdict, detail = fn(repo) + if verdict != "OK": + faults += 1 + print(" %-9s %-5s %s" % (name, verdict, detail)) + print("q114: %d leg(s), %d passed, %d fault(s)" + % (len(legs), len(legs) - faults, faults)) + return 1 if faults else 0 + + +if __name__ == "__main__": + sys.exit(main())