- Tiered all 127 unknown source hosts into the registry (224 hosts, 140 ccTLD mappings; idaoffice.org = related property, never a source). Leads 92 % → 94 %; 11 figures without a lead.
- Wrote
scripts/engine/capture_facts.py(fetch lead URL, visible text, value-near-marker match, cached, polite). Smoke test 0/12: 403 (immi.homeaffairs.gov.au), JS shells (punetejashtme.gov.al), and a matcher miss on mfa.am. Full run (546 fetches, 120 hosts): 21 raw hits; added confidence scoring (claim word near the value; weak matches never count) → 17 strong captures, 4 of them money figures → gate 0 → 2.1 %. 403 bot-blocks: travel.state.gov (80 attempts), mofa.go.jp, immi.homeaffairs.gov.au; JS shells: visa.vfsglobal.com, moi.gov.ae; 26 of our own source links return 404 (content defect). scripts/engine/source_queue.py→reports/source-queue-2026-09-03.md: 642 figures (fee+processing+stay) by lead host and reason (value_not_on_lead_page 229, blocked_403 123, no_lead 113, fetch_failed 94, js_or_thin_page 53, lead_url_404 26, weak_match_confirm 4).scripts/engine/truth_gate.py(R1 provenance, R2 second source, R3 max-age, R4 disagreement): 623 PASS (mostly pages with no third-party money), 122 FAIL on R1; corpus gate NOT YET (4/187).- Committed + pushed.
Expedify AI Content Engine — build tracker
Plan v1.0 (signed 2026-09-02) · generated 2026-09-03 10:36 · refreshes every 5 min · data: status.json
Current phasePhase B — Truth layer
Steps complete16 / 4535 % of the plan's checklist
Money figures sourced0.0 %187 distinct third-party figures · gate ≥ 95 %
With a scoped lead94 %6 % have no lead yet
Convention disagreements00 self-contradicting pages
Unknown source hosts0need human tiering
Next step: Phase B step 5–6: FIX-FACT the 24 convention pages APPLIED 2026-09-03 via
scripts/engine/staleness.py (demotion file changes per max-age) and source_watch.py + timer on the top-value lead URLs that DO fetch (home-affairs.ec.europa.eu, ica.gov.sg, gov.uk, canada.ca…). In parallel, people work: the 642-line source queue (John/Expedify) and the 26 dead source links. Previous: scripts/engine/truth_gate.py (deterministic gate over a page's facts: every rendered number has a fact id; no fact past max-age; money facts need an agreeing second source; captures from data/facts/captures/ promote lead → sourced). The human source queue (reports/source-queue-2026-09-03.md) runs in parallel — it is people work, not engine work. Staged fixes under handoff/fix-fact-2026-09-03/ await John's decision to apply to live. Order: (a) scripts/fixes/fix_convention_statements_2026-09-03.py (exact-match, count-guarded; 17 hubs ×4 occurrences incl. a families[] block the extractor did not vote on, 7 guides, Ireland verdict) → extractor: 0 disagreements, 0 self-contradictions; 2 diff-review rounds (round 1: Indonesia rows + 5 leftover FAQ sentences + last_reviewed), round 2 clean → COMMITTED + pushed 2026-09-03; (a2) for-costa-ricans + for-mexicans regenerated as non_party into handoff/fix-fact-2026-09-03/ (STAGED, live untouched per John's new rule); root cause = idp-routes.json stale → John: do all three. DONE 2026-09-03: scripts/fixes/build_corrected_idp_routes_2026-09-03.py → data/idp-routes.corrected-2026-09-03.json (19 lcc + 20 dest re-derived, 55 routes recomputed, new convention_required: non_party value); scripts/gen_idp_nationality_staged.py (wrapper, generator unmodified) staged all 19 nationality pages under handoff/fix-fact-2026-09-03/; 2 review rounds (CTA slug clobber, non-party×non-party copy, stale validity/multi fields, Nepal→Indonesia name clobber — all fixed) → COMMITTED + pushed 2026-09-03. OPEN: wire the corrected routes file into the checker only after site-astro renders non_party; apply the 19 staged pages to live = John's call; (b) human-tier the ~100 unknown source hosts listed in the report; (c) capture script for the 172 money figures with a scoped lead; (d) human source queue for the 15 with none; (e) fix the 1 malformed literal (japan/visa/for-indians footer). source the 661 third-party money figures (550 have a tier-1/2 candidate URL on the page → capture script; 111 → human source queue). John still to send the developer requests.Phases
Phase A — Plan to v1.0 (no build)
CompleteSteps
- ✓John answers v0.4 §8 open items 1–6 — DONE 2026-09-02: files-only; legal read = Expedify (we supply claim list); cap + short post-mortem; Expedify names reviewer; request Astro read access; prototype deleted
- ✓Fill v0.4 §9 numbers — DONE 2026-09-02 (cap 10/wk + 5 %; floor indexed+90d+200 impr; cohort 80/50/≤30; margin +15 % or −3 pos; review 8 min/25 pp/wk; Opus ≤20/wk; max-age 90/90/180/365; fail-closed fallbacks; 3 deferrals with triggers) — ADR 0005
- ✓Measurement source — direct GSC API via service account (Ahrefs = reference only); request added to developer requests §1b
- ○Obtain Astro content-collection schema; record its field list in findings.md — moved to Phase C prerequisites (requested, pending Expedify)
- ✓Client PRD aligned to v1.0 (§6 post-mortem, §7 asks incl. GSC service account)
- ✓ADRs written in expedify
docs/adr/: 0001 files-never-deploys, 0002 legal read = Expedify, 0003 cap + post-mortem, 0004 named reviewer (growth-cap numbers ADR waits on §9) - ✓Developer requests drafted:
docs/Expedify_Developer_Requests_2026-09-02.md(repo access, reviewer, legal sign-off, release safety, footprint, cap/cohort/conversion) - ○Send the requests to Expedify; track each as a blocked card when execution unfreezes
- ✓John signs v1.0 → unfreeze (2026-09-02; file renamed to _v1.0.md)
Phase B — Truth layer (plan P1 → components C1)
In progressGate: ≥ 95 % of money figures carry a tier 1/2 source; remainder suppressed. Falsified otherwise → Phase D shrinks to sourced pages only.
Steps
- ✓Define
facts:schema — DONE 2026-09-02docs/content engine/facts.schema.json(tiers 0–3, statuses incl. not_stated, conditional source/second_source/sign_off requirements):{id, claim_type: fee|processing_time|eligibility|convention|validity, value, unit, source_url, authority_tier: 1 govt|2 embassy|3 aggregator, captured_at, max_age_days, verification_status, second_source_url?}; sections reference facts by{{fact:id}}instead of literal numbers - ✓
scripts/engine/extract_facts.py— DONE 2026-09-02, read-only, 745 pages → 7,521 candidates indata/facts/candidates/, reportreports/facts-extraction-2026-09-02.md; 5 unit tests intests/test_extract_facts.py. Money gate baseline: 3,316 fee facts = 1,658 first-party + 997 not_stated + 550 with a tier-1/2 candidate on page + 111 with none → 661 third-party figures need real sourcing - ✓Independent challenge round 1 (12 findings: 3 blockers — empty convention DB, misleading gate arithmetic, tier heuristic; 7 majors) → INCORPORATED 2026-09-02: extractor v2 + schema v0.2 +
data/facts/authority_registry.json(112 hosts) + 11 tests on real data. Gate baseline (v2.1): 187 distinct third-party money figures, 0 % captured, 92 % with a scoped lead, 8 % none. 25 convention disagreements (17 stale hubs + 8 self-contradicting driving guides) - ✓Independent challenge round 2 → 4 majors + 4 minors (incl. 2 v2 regressions) → INCORPORATED (v2.1, 16 tests, every page schema-validated)
- ✓Round 3: all 8 confirmed fixed, verdict COMMIT; 3 follow-ups (money ranges, FAQ-question votes, 'in any N-day' windows, road-authority hosts) folded in → v2.2, 17 tests → COMMITTED + pushed 2026-09-02
- ✓Tier the unknown source hosts — DONE 2026-09-03: 127 hosts tiered (registry 112 → 224 hosts, ccTLD map 140), 0 unknown left; leads 92 % → 94 %, no-lead 11 figures; idaoffice.org registered as related property, never a source
- ✓Capture script
scripts/engine/capture_facts.py— DONE 2026-09-03: fetches lead URLs (cached, polite, honest UA), matches value near a currency/unit marker, confidence = claim word nearby (weak matches never count). First full run: 21 hits / 546 fetches; money gate 0 → ~2 %; travel.state.gov + mofa.go.jp + immi.homeaffairs.gov.au block bots (403), VFS/moi.gov.ae are JS shells, most ministry leads are sections not fee pages - ✓Human source queue
scripts/engine/source_queue.py→reports/source-queue-2026-09-03.md(~180 figures grouped by lead host and failure reason: value_not_on_lead_page / blocked_403 / js_or_thin_page / no_lead / weak_match_confirm) - ○Source the candidates: convention facts from
data/idp-countries-by-convention.json(audited 2026-08-30, tier 1 via UN treaty lists); fees/times fromdata/genuine_fees_*.json+ VisaHQ matrix (tier 3 → needs tier 1/2 confirmation); everything else → human source queue card - ✓
scripts/engine/truth_gate.py— DONE 2026-09-03 (R1 provenance, R2 second source, R3 max-age, R4 disagreement; merges captures; per-page verdict PASS/HOLD/FAIL + corpus gate). First run: corpus gate NOT YET, 4/187 money figures sourced. Remaining design gap: the 'every rendered number has a fact id' check needs the facts block retrofit (step 7) - ○Write
scripts/engine/staleness.py: for live pages, emit a *demotion change* tometa(hide figure → banner →noindex: true) per policy; runs from a timer, opens a W5 card - ○Write
scripts/engine/source_watch.py+ops/systemd/expedify-source-watch.{service,timer}: fetch the issuing-body URLs behind top-value facts (start: top 20 by page value), hash the relevant block, diff, open a W2 card with the diff attached - ○Legal/compliance read of IDP-validity + fee statements (owner per §8 item 2)
- ○Retrofit run on the corpus; commit facts blocks in tranches (≤ 50 files per PR, playbook order: visa hubs → driving guides → country hubs → tuples)
- ○Ledger: SYSTEM.md entry for the timer + scripts
Phase C — Contract + gates (plan P2 → C2, C3 single-brief, C4, C5, C9)
PendingGate: dry-run batch builds clean on the site; applied set == manifest.
Steps
- ○Write
docs/policies/corpus-admission.md(right-to-exist test) +scripts/engine/admit_brief.py(mechanical parts: sibling payload check, refresh-not-new lookup, query-overlap check vsreports/SERP data, commercial-value flag from a per-page-type value table) - ○Brief template
briefs/_template.yamlwithfacts_plan:(which fact ids the page will carry and their planned sources) - ○Create path
scripts/engine/create_page.py: brief → registry-composed draft via luna-content (Gemma) →validate_page.py→truth_gate.py→dedup_gate.py(siblings + competitor corpus viapipelines/query_competitor_corpus.py) →citation_gate.py(only where the page type warrants) →kanban_request_review(opus-reviewer, evidence-ID contract, ≤ 3 rounds) - ○Sampled approval
scripts/engine/approve_batch.py: picks k files (start k = batch = 5), prints the 3 questions per file, records verdicts + time-on-page toreviews/approvals/<batch_id>.json; batch-size rule (double after 2 clean, halve on any rejection, any rejection rejects the batch); alarm when median review time < threshold - ○Handoff
scripts/engine/handoff.py: copies approved files tohandoff/<batch_id>/, writesmanifest.json(file, sha256, action, approver, verdicts, redirect_from), idempotent re-run; pinmanifest.schema.json - ○Growth-cap + audit log
scripts/engine/cap.py: refuses a handoff that would exceed URLs/week or %-modified/week; appends every handoff toreviews/audit-log.jsonl - ○Site-side §6 items 1–6 landed by Expedify's developer (schema read access, build-diff == manifest, atomic release + rollback, kill switch, deploy checks vs indexation watcher, build close-out) — track as one blocked card per item
- ○Dry run: 20-page batch through the whole path into
handoff/, developer builds it on a preview → measures writer throughput (pages/day) for §9 - ○Ledger entries for scripts, board conventions, handoff folder
Phase D — Validation cohort (plan P3 → C6)
PendingGate: ≥ 80 % indexed @ 30 d; ≥ 50 % with ≥ 1 impression-bearing query @ 90 d; no manual action. Fail ⇒ STOP.
Steps
- ○Pick 30–60 URLs (page types + hardest tuples) with John/client; record list in findings.md
- ○Measurement
scripts/engine/measure.py: pull GSC (source per Phase A decision), URL-inspection indexation, conversion events; writedata/measure/; per-page cost ledger from Phase C logs - ○Per-page (not sampled) approval for every cohort file
- ○Release in tranches over several weeks; robots lifted incrementally by the developer; indexation watcher reporting to aos-notify
- ○Hold 8–12 weeks; weekly readout card
Phase E — Improve loop on the cohort (plan P4 → C7, W3, W5)
PendingGate: refreshed beats control at 90 d by the §9 margin; else keep only FIX-FACT/TECHNICAL.
Steps
- ○
scripts/engine/triage.py: evidence floor check → deterministic typed decision (NO-ACTION default; FIX-FACT / REFRESH / CONSOLIDATE / PRUNE / REWORK-INTENT / TECHNICAL-ONLY) with a stated hypothesis and evidence IDs; cannibalization detection (same query, multiple URLs) - ○Randomized 30–40 % held-out control; weekly site-wide change cap enforced by
cap.py - ○Decisions → W2/W5 cards; Opus only on ambiguous cases
- ○Optionally rebuild the diagnostic half (SERP top-3 gap analysis) from the deleted prototype's design notes in memory — only after the evidence-ID contract is generalised
Phase F — Scale, research, citation (plan P5–P7 → parameterized C3, C8, W4, W6)
PendingGate: only if D and E passed.
Steps
- ○Parameterize
create_page.pyintobuild_family.py("visa|IDP|passport family for [country set]") undercap.py+ commercial gate - ○
scripts/engine/research.py: source-watch events + SERP-shift detection as primary triggers → candidate briefs →admit_brief.py; keyword sweep secondary, low-frequency; idempotency rule vs in-flight W2 cards - ○Citation experiment: metric + baseline on the cohort, A/B on a subset, rich-result eligibility re-verified first
Waiting on decisions / inputs
- ✓
Delivery model→ files-only, confirmed 2026-09-02 (ADR 0001). - ✓
Measurement source→ direct GSC API on Expedify's property; service account requested. - ✓
Named reviewer→ Expedify names a real person (ADR 0004); name itself pending from Expedify. - ?Astro content-collection schema — requested 2026-09-02; which
metakeys does the site read (incl.last_verified)? PENDING EXPEDIFY - ✓
Legal read owner→ Expedify, we supply the claim checklist (ADR 0002). - ✓
Disclosure→ cap presented with a short non-identifying post-mortem (ADR 0003). - ✓
§9 numbers→ all agreed 2026-09-02 (plan §9 table). Deferred with triggers: writer throughput (dry run), cohort pick (client), manifest schema (Astro schema). - ✓
v1.0 sign-off→ signed 2026-09-02; execution unfrozen.
Staged fixes (not applied to live)
- fix-fact-2026-09-03 — 19 files staged, awaiting John (e.g. driving-document/for-swiss/index.yaml, driving-document/for-egyptians/index.yaml, driving-document/for-argentines/index.yaml, driving-document/for-spaniards/index.yaml)
Work log
- Found root cause: generator reads data/idp-routes.json, never updated after the 08-30 audit (19 lcc / 20 dest discrepancies; routes = 12 licence × 23 dest subset, 55 routes with a changed destination).
- John's rule: fixes to NEW files. Built
data/idp-routes.corrected-2026-09-03.json(script clones existing route templates per status pair; authored copy for the new non-party-destination case). Wrapperscripts/gen_idp_nationality_staged.pystages pages from the corrected file without touching the generator or content/. - Staged 19 pages; 19/19 verdicts match the corrected status. Review round 1: 8 findings (3 substantive in the routes recomputation) → all but the generator-internal ones fixed; rebuilt + restaged; round 2 found the Nepal→Indonesia name clobber in sub_names → placeholder fix + Indonesia diagonal + next_review/sources-note polish; rebuilt, restaged, verified (0 clobbers, 19/19 parse) → committed.
- Dumped all 25 flagged pages' convention sentences via
x_votes; found Ireland's verdict ("either type") that the voter treated as ambiguous. - Wrote
scripts/fixes/fix_convention_statements_2026-09-03.py: exact-match/regex replacements with per-file expected counts; dry-run ABORTED on first pass (hubs had 4 occurrences not 3; Indonesia guide 3 not 2) → located the extra occurrences (families[]block, requirements row note) → counts corrected → applied to 24 files with backups. - Re-ran extractor (
reports/facts-extraction-2026-09-03.md): disagreements 0, self-contradictions 0, all YAML valid, 17 tests OK. Residual-wording grep: clean. - Diff review round 1: 1 blocker (Indonesia rows), 2 majors (leftover FAQ sentences; review metadata), minors (voice, script anchors). Restored the 24 backups, extended the script (Indonesia rows via non-party template, 7 FAQ edits,
last_reviewed+next_review, anchored+asserted date bumps, three-sentence neither wording), re-applied. Extractor: 0/0 again; 17 tests OK; YAML valid. Round 2 dispatched.
- Discovery: mapped where facts live (verdict prose, roadRules table, pricingBox, visaTable, requirements rows); recorded in findings.md.
- Wrote
docs/content engine/facts.schema.json(draft 2020-12; validated with jsonschema). - Wrote
scripts/engine/extract_facts.py(read-only) +tests/test_extract_facts.py(5 tests, pass). - Run 1: 745 pages, 7,498 candidates; found 203 gov fees mis-tagged first_party (row text "+ Expedify fee") and 23 hedged guides with no convention fact. Fixed both (first-party = quote box / expedify_fee prop / amount adjacent to "Expedify"; hedged guide → not_stated fact). Run 2: 7,521 candidates, 1 residual mis-tag (canada/visa/for-indians CAD 185).
- Disagreements vs data files: 0 (content was generated from the same DBs — expected; the DBs themselves are the thing to source).
- Independent adversarial review (subagent, 24 tool calls): 12 findings, 3 blockers. All blockers + majors incorporated: extractor v2, schema v0.2, authority registry (112 hosts), 11 tests incl. real-data loaders + schema validation of real output. Run 3: 6,280 facts; gate 0.0 % captured / 76 % scoped lead / 24 % none; 23 real convention disagreements (13 stale country hubs + India bug etc.). Round-2 re-check dispatched.
- Review round 2: blockers 1/2/3/5 confirmed fixed, ids partially; 4 majors (value-bearing ids + 10 schema failures, voter tie-breaks masking inconsistent pages, fx/$0 inflating the denominator, generic-gov + dubai/schengen country scoping) + 4 minors. ALL incorporated (v2.1): value-free ids
claim_type.<subject-key>, per-path convention votes with inconsistent pages reported as disagreement, fx inside any "(~ …)" group incl. "/"-separated, $0 by row subject, rolling windows → qualifier, generic gov^gov.<cc>$+.gv.at, country aliases (dubai→UAE, schengen→None), own-country advice → tier 1, no invented captured_at, dead code removed, unknown hosts fully listed. Tests 16 (every page validated with FormatChecker; no duplicate ids). Run 4: 187 distinct third-party money figures, 0 % captured, 92 % scoped lead, 8 % none; 25 disagreements of which 8 are self-contradicting driving guides. - Review round 3: all 8 confirmed fixed; verdict commit. Folded in its 3 follow-ups + registry road hosts (lto.gov.ph, nhtsa.gov, …) + europa.eu → v2.2, 17 tests. Final run: 187 distinct third-party figures / 0 % / 92 % lead / 8 % none; 25 disagreements, 7 self-contradicting guides.
- Committed + pushed (expedify main) with data/facts/candidates/ gitignored as regenerable output.
- Audited project state; found PRD (v0.2-derived, publishes) vs v0.3 (files, no publish) conflict and v0.3 internal contradictions.
- John: pause all execution, solidify the plan first. Verified nothing running (no procs/timers/cron; no running/ready/review cards).
- Archived t_e87b5d09 (japan improve) + t_6b4a559a, t_872c6ed1, t_7f8140a6, t_54478ab4, t_714daa4d on luna-expedify; commented on parent t_d3e2c562.
- Wrote
docs/AI_Content_Engine_Project_Plan_v0.4.md(components C1–C9, workflows W1–W6, site-side contract, decisions ledger, §9 blanks). Committed 78764724, pushed to expedify-hk/content-workspace main. - Deleted parked prototype
pipelines/page-content-audit/on John's instruction. - Revised client PRD to v0.4 (handoff not publish; 0–7 phases; open decisions). Unversioned (outbound/ gitignored).
- Created these planning files (task_plan.md, findings.md, progress.md) per planning-with-files; added them to the expedify repo .gitignore.
docs/AI_Content_Engine_Project_Plan_v0.4.md(new, committed)~/Agentic-OS/outbound/Expedify/Expedify_AI_Content_Engine_PRD.md(revised)task_plan.md,findings.md,progress.md(new, ignored).gitignore(+3 planning files)docs/adr/0001..0004-*.md,docs/Expedify_Developer_Requests_2026-09-02.md(new)docs/AI_Content_Engine_Project_Plan_v0.4.md,docs/Expedify_AI_Content_Engine_PRD.md(decisions recorded)- Decision session with John: v0.4 §8 items 1–5 resolved (see task_plan Decisions). Plan §8 + PRD §6/§7 updated; ADRs 0001–0004 written to
docs/adr/; developer requests drafted. - §9 session with John: all 13 lines + measurement source agreed; plan §9 rewritten as AGREED table; ADR 0005 (growth cap); dev requests §1b (GSC service account); PRD §7 item 7. Plan marked v1.0 CANDIDATE pending John's explicit sign-off.
- John signed v1.0 (file renamed, status line updated, ADR/PRD references bumped). Phase A complete; execution unfrozen; Phase B not started.
Recent commits (expedify repo)
a7ceabfb | 2026-09-03 | engine: Phase B step 3b-4 — host tiering, capture script, human source queue, truth gate |
8466e6d6 | 2026-09-03 | engine: progress tracker generator (task_plan/progress/report/git → static page + status.json, wrangler deploy) |
f3d4efd4 | 2026-09-03 | engine: corrected IDP routes copy + 19 idp_nationality pages staged from it (live untouched) |
ee097337 | 2026-09-03 | content: FIX-FACT convention statements on 24 pages (17 country hubs, 7 driving guides) |
094a5965 | 2026-09-02 | engine: Phase B step 1-2 — facts schema v0.3, authority registry, read-only fact extractor |
56674bff | 2026-09-02 | docs: sign AI Content Engine plan v1.0 — §9 numbers agreed, growth-cap ADR 0005 |
14e3a1ee | 2026-09-02 | docs: close plan v0.4 §8 decisions — ADRs 0001-0004, developer requests, PRD post-mortem |
0ea1cc70 | 2026-09-02 | docs: client-facing AI Content Engine PRD, revised 2026-09-02 to plan v0.4 |
7f487c13 | 2026-09-02 | chore: ignore planning-with-files working memory |
78764724 | 2026-09-02 | docs: AI Content Engine plan v0.4 — reconciled, plan-only |
535a3ebe | 2026-08-31 | ad-campaign |
99e8eede | 2026-08-31 | Merge pull request #2 from expedify-hk/fix/expedify-claims-corrections |