Skip to content

Add ts CLI ad-template config diagnostics and browser audit - #823

Open
prk-Jr wants to merge 262 commits into
mainfrom
feature/ts-cli-ad-templates
Open

Add ts CLI ad-template config diagnostics and browser audit#823
prk-Jr wants to merge 262 commits into
mainfrom
feature/ts-cli-ad-templates

Conversation

@prk-Jr

@prk-Jr prk-Jr commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Adds operator tooling to verify server-side ad-template ([creative_opportunities]) configuration: static path/slot diagnostics via ts config ad-templates …, and browser-backed live verification via ts audit … (local Chrome/Chromium over CDP).
  • Adds ts audit ad-templates generate <url> to bootstrap [creative_opportunities] from a live site. One run crawls the publisher's sections (sitemap via robots.txt, else navigation links), samples a landing page and an article per section, reconciles each slot across the pages it appeared on, and writes the result into an existing trusted-server.toml in place, preserving every other section and comment.
  • Where the evidence proves it, the run infers a {network_id}/{section} ad-unit template plus the section_root/section_segment policy it depends on, instead of pinning each slot to the one literal path it happened to be scraped from. A wrong template makes a publisher bid against inventory that does not exist, so inference refuses rather than guesses — see the table below.
  • Everything the run writes is validated before it replaces the file: the candidate goes through Settings::from_toml, the same load path the runtime uses at startup, on the --dry-run path too. An unloadable trusted-server.toml is a full-site outage once pushed, not a degraded ad stack.
  • Lets publishers catch slot/config drift before deploy — match a path to configured slots, assert expected slots in CI, explain runtime ad-stack gating, and confirm configured slots actually render (DOM/GPT/APS evidence) on live pages.
  • Keeps the CLI host-only: browser deps (chromiumoxide) are excluded from the wasm32-wasip1 build, and the runtime ad-stack gate is shared with publisher.rs so the CLI cannot drift from server behavior.

Stacks on the EdgeZero CLI base (server-side-ad-templates-impl). After merging the base back in, the diff against it is feature-only.

Behavior change: bare ts audit <url> remains a hidden alias for ts audit generate <url>; the base branch's draft-artifact generation moved to the explicit ts audit generate <url>.

Deploy ordering — read before pushing generated config. A config carrying section_root or section_segment is not rollback-safe. CreativeOpportunitiesConfig is deny_unknown_fields, so a binary predating ad-unit templating (cee1e20e) rejects the entire Settings load — not just the ad-template section — and every route serves an error. Deploy the template-aware binary first, then push, and do not roll that binary back while the config is live. A run that did not template writes neither key and leaves the config exactly as rollback-safe as it was. The command prints this warning itself.

closes #701

Changes

Area Change
trusted-server-core/src/creative_opportunities.rs [creative_opportunities] config types, match_slots, shared evaluate_ad_stack_gate; compile_page_pattern as the single glob definition; derive_section made public so tooling checks inference against the runtime's own derivation rather than a second implementation
trusted-server-core/src/publisher.rs Route should_run_server_side_ad_stack through the shared gate (behavior-preserving)
trusted-server-cli/src/commands/config/ad_templates.rs ts config ad-templates {lint,match,check,explain} static diagnostics
trusted-server-cli/src/app_config.rs Shared effective app-config loader for the config + audit commands
trusted-server-cli/src/ad_templates/{expected,compare,output}.rs Pure expected-slot projection, DOM/GPT/APS evidence comparison, stable JSON model, terminal-safe text escaping
trusted-server-cli/src/commands/audit/{mod,page,collector,browser,ad_templates}.rs, commands/audit/ad_template_collector.js ts audit page + ts audit ad-templates verify: chromiumoxide collector, read-only GPT/APS/DOM init script, verifier orchestration, cross-origin refusal
trusted-server-cli/src/commands/audit/generate/crawl_plan.rs New. Turns links and sitemap entries into a bounded page set — one landing page and one article per section, ranked by nav/sitemap corroboration, capped by section and page budgets, same-origin enforced on both sources
trusted-server-cli/src/commands/audit/generate/evidence.rs New. Cross-page slot evidence: formats unioned, divergent unit paths retained, network ids required to agree, and detection of one placement fragmented across per-render div ids
trusted-server-cli/src/commands/audit/generate/unit_template.rs New. {network_id}/{section} inference with positional network binding, a single-varying-segment rule, the witness rule, and replay through the runtime's own renderer
trusted-server-cli/src/commands/audit/generate/page_patterns.rs New. Expands observed paths into per-section glob pairs (/news and /news/*) without extrapolating past a witnessed section
trusted-server-cli/src/commands/audit/generate/validate.rs New. Write-side gate: the candidate config must load through Settings::from_toml before it replaces the file; a pre-existing failure downgrades to a warning so an already-broken config can still be updated
trusted-server-cli/src/commands/audit/generate/{mod,gpt_slots}.rs Crawl orchestration, challenge-rate refusal, per-page diagnostics; slot reconstruction from live GPT plus normalization of ephemeral div-id noise (React _R_/_r_ ids, -container, hex UUIDs)
trusted-server-cli/src/commands/audit/generate/{browser_collector,collector,analyzer}.rs Batch collection on one reused browser, hydrated-DOM link extraction, in-page sitemap fetch, device profiles, request pacing, consent answering, proxy support; GPT slot reads wait for the registry to hold still for the settle-quiet window, inside the settle-max budget, before they are trusted
trusted-server-cli/src/run.rs, src/lib.rs Command wiring and parser tests for the audit namespace
trusted-server-cli/Cargo.toml Host-only edgezero-core + serde_json deps (cfg-gated off wasm, like the existing browser deps)
docs/guide/cli.md ts audit ad-templates generate documented: crawl behavior, refusal table, consent platforms, proxy auditing, the deploy-ordering contract, and the settle window as a per-phase bound rather than a whole-page one
docs/superpowers/{specs,plans}/2026-06-26-server-side-ad-template-cli* Design spec and implementation plan

Test plan

Per CLAUDE.md, a bare cargo test/cargo clippy --workspace fails at the workspace root — the repo has multiple wasm runtimes with runtime-specific SDKs, so the target-matched aliases are the real gate.

  • cargo fmt --all -- --check
  • cargo clippy-fastly && cargo clippy-axum && cargo clippy-cloudflare && cargo clippy-cloudflare-wasm && cargo clippy-spin-native && cargo clippy-spin-wasm
  • cargo clippy -p trusted-server-cli --target <host-triple> --all-targets --all-features -- -D warnings
  • cargo test-fastly && cargo test-axum && cargo test-cloudflare && cargo test-spin
  • cargo test --manifest-path crates/trusted-server-integration-tests/Cargo.toml --test parity (13 passed)
  • Host CLI tests: cargo test -p trusted-server-cli --target <host-triple>475 passed / 0 failed (440 lib + 35 integration)
  • JS tests: cd crates/trusted-server-js/lib && npx vitest run (829 passed)
  • JS format: cd crates/trusted-server-js/lib && npm run format
  • Docs format: cd docs && npm run format
  • WASM build: cargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1 (plus cargo build -p trusted-server-cli --target wasm32-wasip1 — browser deps stay out of wasm)
  • Chrome-gated browser fixtures (./scripts/test-cli.sh) for evidence collection, scroll-phase attribution, and GPT registry stability — 7 passed, including a fixture whose second registration burst lands on a real timer
  • Manual: full crawl against a live publisher through ts dev proxy — template and section policy inferred, per-render div-id fragments refused, generated config loads through Settings::from_toml

Notable fixture-based coverage, all offline: crawl planning (cross-origin rejection on links and sitemap entries, utility/asset filtering, query/fragment collapsing, budget truncation), evidence reconciliation (format union, network-id conflict, fragment detection with a co-occurrence false-positive guard), and one test per template-inference refusal case.

How to use

Static ts config ad-templates … commands only read local effective config (no browser). Browser-backed ts audit … commands launch a local Chrome/Chromium.

Configure slots

In your (gitignored) trusted-server.toml — fictional values shown:

[creative_opportunities]
gam_network_id = "123"
section_root = "homepage"     # required when a template uses {section}

[[creative_opportunities.slot]]
id = "atf"
gam_unit_path = "/{network_id}/news/{section}"   # or a literal path
div_id = "ad-atf-"            # treated as a prefix; matches e.g. ad-atf-0
page_patterns = ["/", "/news", "/news/*"]
formats = [{ width = 300, height = 250 }]

# optional provider hints
[creative_opportunities.slot.providers.aps]
slot_id = "atf"

Generate slots from a live site (needs local Chrome/Chromium)

# Crawl the site's sections and write [creative_opportunities] in place
ts audit ad-templates generate https://www.example.com/

ts audit ad-templates generate https://www.example.com/ --dry-run    # preview, no write
ts audit ad-templates generate https://www.example.com/ --replace    # overwrite instead of merge

# Bound the crawl
ts audit ad-templates generate https://www.example.com/ --max-sections 20 --max-pages 41
ts audit ad-templates generate https://www.example.com/ --max-pages 1   # single page, as before

# Set the patterns yourself; disables pattern inference
ts audit ad-templates generate https://www.example.com/ \
  --page-pattern '/' --page-pattern '/news' --page-pattern '/news/*'

# Check whether the publisher splits ad units by device
ts audit ad-templates generate https://www.example.com/ --profiles desktop,mobile

# Behind bot protection: carry a session, pace the crawl, or use a visible browser
ts audit ad-templates generate https://www.example.com/ \
  --cookie '<NAME>=<VALUE>' --page-delay-ms 1500
ts audit ad-templates generate https://www.example.com/ --headful

Re-running merges: a slot seen again keeps its hand-tuned fields and gains this run's patterns and newly observed formats, and a hand-written gam_unit_path template is preserved. --replace discards existing slots, including any template written by hand.

Consent platforms. Publishers gate slot definition behind their consent platform, and the audit runs in a throwaway profile with no consent cookie — so such a site would define no slots at all and look identical to a site with no ad stack. The crawl therefore answers the two IAB interfaces every compliant platform exposes (TCF v2 and US Privacy) as a consenting, out-of-scope reader, before any page script runs. This changes only what the audit browser sees. --no-assume-consent observes the un-consented page instead.

Auditing a production hostname served locally. ts dev proxy serves a production hostname from a local Trusted Server; auditing through it keeps the page's origin, cookie scope, and any origin checks in the ad stack matching production rather than localhost:

ts dev proxy --map www.example.com=127.0.0.1:7676 --upstream-plaintext --rewrite-host

ts audit ad-templates generate https://www.example.com/ \
  --browser-proxy 127.0.0.1:18080 --danger-accept-invalid-certs

Note that a local Trusted Server injects its own configured slots, so a run through the proxy can rediscover config it already has; slot ids absent from the current config are the publisher's own.

When generation keeps literal paths, and when it refuses

Situation Result
Only one page was crawled Literal path — one observation cannot distinguish a literal from a template
The ad unit never varied by section Literal path
A section's slug is not derivable from its URL Refused slot omitted; the failed round-trip is reported
No root page was seen, so section_root is unknown Refused slot omitted rather than guessing a fallback
Two path segments could both be the section No template; the ambiguity is reported
The unit varies by device, geo, or anything the URL cannot supply Refused slot omitted with the conflicting evidence reported
Several slots are one placement under per-render div ids Fragments reported and skipped, with the stable prefix they share
Crawled pages report different GAM network ids Run fails — the pages are not one property
More than a quarter of crawled pages return no slots Run fails — the signature of bot protection serving challenge pages

Static diagnostics (no browser)

# Summarize config + deploy-time implications
ts config ad-templates lint

# Which configured slots match a path (or full URL)
ts config ad-templates match /news/story --details

# CI assertion: exact expected slot set
ts config ad-templates check /news/story --expected-slot atf
ts config ad-templates check /weather --expect-no-slots
ts config ad-templates check /news/story --expected-slot atf --allow-extra-slots

# Explain the runtime ad-stack gates for a modeled request
ts config ad-templates explain /news/story --bot --consent-denied --prefetch

Browser-backed audit (needs local Chrome/Chromium)

# Generic read-only page summary
ts audit page https://www.example.com/

# Verify configured slots render on live pages (DOM/GPT/APS evidence)
ts audit ad-templates verify https://www.example.com/news/story
ts audit ad-templates verify https://www.example.com/news/story --json
ts audit ad-templates verify https://www.example.com/ --strict --scroll

# Carry a session past an origin challenge (repeatable)
ts audit ad-templates verify https://www.example.com/ --cookie '<NAME>=<VALUE>'

# Allow a known redirect between your own properties (e.g. apex → www)
ts audit ad-templates verify https://example.com/ --allow-cross-origin-redirect

# Point at a specific browser / tune settle timing
ts audit ad-templates verify https://www.example.com/ \
  --chrome /path/to/chrome --settle-quiet-ms 1000 --settle-max-ms 15000

The settle window bounds each wait phase rather than the whole page: the initial settle, the optional post-scroll settle, and — in generate only — a final wait for the GPT slot registry to stop changing before it is read. GPT can finish registering after the document and resource stream have gone quiet, so a single read there would sometimes miss a later batch. Raise --settle-max-ms if a run warns that slot registration did not hold still. verify needs no such wait: its collector is injected ahead of publisher scripts and records each slot as it is defined.

Shared config flags (all of the above)

--app-config <path>     # default: <app.name>.toml beside edgezero.toml
--manifest <path>       # default: edgezero.toml
--no-env                # skip the app-config environment overlay

Exit behavior

  • Default verify is auditor-assist: exits 0 even with missing/partial evidence.
  • --strict exits 1 when a confirmable matched slot is missing or partially confirmed; video, native, and out-of-page slots are unconfirmable and do not fail the gate. A page-level navigation failure, or a redirect that leaves the requested origin, also exits non-zero.
  • Runtime gates (e.g. [auction].enabled = false) mark a page "skipped" so --strict does not fail it.

Local live test (deterministic, no external site)

Many large ad publishers block headless/non-evasive browsers, so verify against them sees a challenge page rather than the article (this tool does not evade bot detection — --cookie forwards a clearance a human already earned, and --headful runs a visible browser). When a page comes back without slots, the run now reports GPT's observable state — whether the library reached apiReady, how many queued commands never drained, how many scripts ran — which distinguishes "the library never loaded" from "this page has no ads".

To exercise the full pipeline reliably without an external site, serve a local fixture:

# 1. Fixture page that stubs just enough GPT (no network), served over HTTP
mkdir -p demo/news
cat > demo/news/story.html <<'HTML'
<!doctype html><html><head><meta charset="utf-8">
<div id="ad-atf-0"></div>
<script>
(function(){var s=[];var gt={cmd:[]};
gt.defineSlot=function(p,z,d){var o={getAdUnitPath:function(){return p},getSlotElementId:function(){return d},getSizes:function(){return z.map(function(x){return {getWidth:function(){return x[0]},getHeight:function(){return x[1]}}})}};s.push(o);return o};
gt.pubads=function(){return {getSlots:function(){return s}}};var op=gt.cmd.push.bind(gt.cmd);gt.cmd.push=function(c){op(c);c()};
window.googletag=gt;googletag.cmd.push(function(){googletag.defineSlot('/123/news/atf',[[300,250]],'ad-atf-0')});})();
</script></head><body></body></html>
HTML
(cd demo && python3 -m http.server 8771 &)

# 2. Verify against it (config from the example above, with page_patterns ["/news/*"])
ts audit ad-templates verify http://127.0.0.1:8771/news/story.html --json
#   => slots[0].status == "confirmed"

For a realistic end-to-end generation run, ts dev proxy in front of a local Trusted Server is the reliable path — see the proxy example above.

Checklist

  • Changes follow CLAUDE.md conventions
  • No unwrap() in production code — use expect("should ...")
  • No println!/eprintln! in library code (CLI output uses writeln!; errors use log)
  • New code has tests
  • No secrets or credentials committed

prk-Jr added 30 commits May 5, 2026 17:57
…ities.toml

Adds the creative_opportunities field to Settings struct to deserialize
configuration for the server-side ad auction feature. Includes build.rs
stubs for types required during build-time configuration validation.

Creates creative-opportunities.toml with example slot configuration and
updates trusted-server.toml with the [creative_opportunities] section
defining GAM network ID, auction timeout, and price granularity settings.

Tests pass with proper TOML parsing of the creative_opportunities section.
…ared auction state

- Add `ad_slots_script: Option<String>` and `ad_bids_state: Arc<RwLock<Option<String>>>` fields to `HtmlProcessorConfig`
- Update `from_settings` to initialize both new fields with safe defaults
- Prepend `ad_slots_script` inside the existing `<head>` handler before integration inserts
- Add `element!("body", ...)` handler that uses `end_tag_handlers()` to inject `__ts_bids` before `</body>`; falls back to empty `{}` when auction state is `None`
- Add `IntegrationRegistry::empty_for_tests()` test helper
- Add three new tests covering all injection paths
…gibility gates; max-age=0

- Make handle_publisher_request async; add orchestrator and slots_file params
- Dispatch origin request with send_async before running auction in parallel
- Gate auction on GET, no prefetch, no bot, matched slots, TCF purpose-1 consent
- Run server-side auction and write bucketed bids to ad_bids_state Arc<RwLock>
- Compute ad_slots_script after response headers; set Cache-Control: private, max-age=0
- Fix Stream arm to thread actual ad_slots_script and ad_bids_state through
- Add build_auction_request, build_bid_map, build_bids_script, build_ad_slots_script helpers
- Update route_tests.rs to pass empty slots_file to route_request
- build_bid_map now returns serde_json::Map with full bid objects (hb_pb,
  hb_bidder, hb_adid, nurl, burl) instead of a plain CPM string map
- build_bids_script / build_ad_slots_script now emit full <script> tags
  using JSON.parse("…") for safe inline embedding; add html_escape_for_script helper
- build_ad_slots_script uses correct property names (gam_unit_path, div_id,
  formats, targeting) matching the client-side TSJS bundle expectations
- Replace map_or(false, …) with is_some_and(…) on lines 546, 549, 567
- Add # Panics doc sections to handle_publisher_request and create_html_processor
… from slotRenderEnded; slim-Prebid lazy loader
- Enable APS and adserver_mock in auction config; set providers and mediator
- Increase auction_timeout_ms from 500ms to 3000ms — 500ms was too tight
  for HTTPS round-trips to mocktioneer, leaving the mediator zero budget
- Fix mediation request: send numeric price instead of opaque encoded_price;
  mocktioneer requires a decoded price field and does not support encoded_price
- Expand creative-opportunities slot page_patterns to include /news/**
Define SlotRenderEndedEvent, SlotRenderEvent, and TestWindow types to
eliminate all @typescript-eslint/no-explicit-any violations in
gpt/index.ts and gpt/index.test.ts. Extend GptWindow with
__tsjs_slim_prebid_url so installSlimPrebidLoader avoids the any cast.
Set gam_network_id to 88059007 (autoblog production network). Update
atf_sidebar_ad slot to /88059007/autoblog/news with div_id
ad-atf_sidebar-0-_r_2_ (desktop ATF sidebar, 300x250); restrict
page_patterns to article paths only (/20**, /news/**) since that div
does not exist on the homepage. Add homepage_header_ad slot targeting
/88059007/autoblog/homepage with ad-header-0-_R_jpalubtak5lb_ for
970x90/728x90/970x250 leaderboard formats. Reduce auction_timeout_ms
from 3000 to 500 to cap TTFB at the spec-recommended ceiling.
The bids script set window.__ts_bids but never invoked the
__tsAdInit function, leaving GPT slots undefined and server-side
targeting (hb_pb, hb_bidder) never applied. Both the winning-bid
path (build_bids_script) and the no-auction fallback (html_processor
None branch) now guard-call the function after the assignment.
Adds [slot.providers.pbs.bidders] support so PBS bidder params
live in creative-opportunities.toml alongside APS params, without
needing PBS stored requests configured server-side.

PrebidAuctionProvider now sends imp.ext.prebid.storedrequest.id
as a fallback for slots with no inline PBS params, and skips
non-PBS provider keys (e.g. "aps") that belong to separate auction
providers. PrebidImpExt gains an optional storedrequest field;
empty bidder maps are omitted during serialisation.

Wires mocktioneer and criteo (placeholder IDs) for both
autoblog creative-opportunity slots.
…id entries from destroyed slots no longer persist
Apply the requested origin boundary to every collected page, not just the
root navigation. A section page that redirected off the audited origin
previously folded its slots, formats and ad-unit paths into the generated
config, and a later device profile's own root redirect was never checked at
all. Both sites now skip such a page with a path-only note, and the later
profile stops counting it towards profile coverage, so the existing
zero-coverage refusal still fires when every page is lost.

Restrict slot prefix reconciliation to the operator's original configured
slots. matching_slot_index searched the whole mutable merged list, so a slot
appended during this run became a prefix candidate for later discoveries:
ad-top absorbed a later ad-top-sidebar, discarding its unit path and provider
state while emitting no broad-prefix diagnostic. Run additions now match by
exact identity instead, making the result order independent.

Replace the real publisher named in the scroll and staleness design document
with generic wording, per the documentation policy in CLAUDE.md.

Tests cover a redirected section page, a later-profile root redirect, and an
order-sensitive merge with an unrelated existing slot alongside ad-top and
ad-top-sidebar. Reverting the two production changes fails exactly these
three tests and nothing else.
ChristianPavilonis added a commit that referenced this pull request Aug 26, 2026
# Conflicts:
#	crates/trusted-server-core/src/publisher.rs
prk-Jr added 3 commits August 27, 2026 21:43
Main added the dedicated `[creative_opportunities].enabled` template switch
(#1008) with its own publisher-local ad-stack gate, while this branch moved the
same gate into core as `evaluate_ad_stack_gate`. Resolve in favor of the shared
gate and give it the new switch, so the CLI diagnostics keep reporting the same
verdict the runtime reaches:

- Drop `ServerSideAdStackConfig`/`should_run_server_side_ad_stack` and route the
  publisher call site through `evaluate_ad_stack_gate` with `ad_templates_enabled`.
- Keep `is_server_side_ad_eligible_navigation`; the inactive-template cache
  policy needs the request-only half of the gate.
- Add `AdStackGateName::AdTemplatesEnabled` and widen the exhaustive gate tests
  to the eighth gate, absorbing the coverage of main's deleted unit test.
- Feed the switch from both CLI gate call sites, add the `Gates` JSON field, the
  `explain` gate row, and the `lint` switch line and status.
# Conflicts:
#	crates/trusted-server-cli/src/commands/audit/generate/slot_toml.rs

@aram356 aram356 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Round-6 re-review at fec84adef. Round 5 is fully resolved — every finding closed, two beyond what was asked (cli_definition_is_valid, the ErrorKind upgrade), and the borrowed-root precision question settled by a disjointness argument that was independently verified. The two new feature series are largely sound: the scroll pass is genuinely unified rather than tripled, the div-id reconciliation is order-independent and idempotent, the round-3 prefix hazard is confirmed not reintroduced (every runtime consumer resolves exact/longest-first), and HEAD passes the real-world token sweep. Requesting changes for two reproduced false positives in the new stale-slot diagnostic — an ambiguity-refused placement and a live broad prefix are both reported "not observed during this crawl" with advice that cannot help — plus a set of doc-accuracy and hardening items, most with one-click fixes.

12 of the inline comments below carry a one-click GitHub suggestion. Every suggestion was verified in a scratch worktree, in isolation and as a batch: cargo fmt --all -- --check, cargo clippy -p trusted-server-cli --target aarch64-apple-darwin --all-targets -- -D warnings, the CLI test suites, and docs prettier, all clean with byte-exact drift checks (the Chrome-gated fixture behind the timing-margin suggestion was not executed; that one is arithmetic-derived). The remaining comments describe fixes in prose because the change spans non-adjacent regions or is a design call.

Blocking

🔧 wrench

  • Ambiguity-refused placements reported "not observed", with advice that cannot help — see inline at crates/trusted-server-cli/src/commands/audit/generate/mod.rs:746
  • A live broad prefix is reported unobserved when its exact sibling is configured — see inline at crates/trusted-server-cli/src/commands/audit/generate/slot_toml.rs:378

Non-blocking

♻️ refactor / 🤔 thinking / ⛏ nitpick

  • ♻️ Split-off siblings silently lose the parent's hand-tuned fields — see inline at crates/trusted-server-cli/src/commands/audit/generate/slot_toml.rs:353
  • 🤔 The 8+8 token rule refuses stable date-plus-word div ids — see inline at crates/trusted-server-cli/src/commands/audit/generate/gpt_slots.rs:285
  • ♻️ ScrollFailure duplicates eval_discard's warning vocabulary — see inline at crates/trusted-server-cli/src/commands/audit/browser_scroll.rs:8
  • ♻️ The Linux build fix papers over derive_more's macOS-only gating; ScrollFailure lacks impl Error — see inline at crates/trusted-server-cli/src/commands/audit/browser_scroll.rs:11
  • collect_open_page still takes 7 positionals after the grouping commit — see inline at crates/trusted-server-cli/src/commands/audit/generate/browser_collector.rs:574
  • Refusal table missing the new cross-origin page skip — see inline at docs/guide/cli.md:243 (suggestion)
  • Merge paragraph still describes pre-round-6 identity rules — see inline at docs/guide/cli.md:277 (suggestion)
  • "Not by recognising token shapes" contradicted by the shape recognizer — see inline at docs/guide/cli.md:386 (suggestion)
  • Shared-flag list omits --scroll — see inline at docs/guide/cli.md:455 (suggestion)
  • Broken HOST_TARGET shell snippet, six occurrences — see inline at docs/superpowers/plans/2026-08-24-ad-template-generate-scroll-staleness.md:61 (suggestion)
  • Ragged wrap left by the token scrub — see inline at docs/superpowers/specs/2026-08-24-ad-template-generate-scroll-staleness-design.md:96 (suggestion)
  • Dead filter predicate in the observation seed — see inline at crates/trusted-server-cli/src/commands/audit/generate/slot_toml.rs:231 (suggestion)
  • Staleness note's --replace guidance understates what it discards — see inline at crates/trusted-server-cli/src/commands/audit/generate/mod.rs:759 (suggestion)
  • observed_div_ids superset contract undocumented — see inline at crates/trusted-server-cli/src/commands/audit/generate/slot_toml.rs:208 (suggestion)
  • Bare assertions on the wording-split test and scroll default — see inline at crates/trusted-server-cli/src/commands/audit/generate/mod.rs:2254 and crates/trusted-server-cli/src/run.rs:520 (suggestions)
  • Chrome fixture's ~250 ms timing margin — see inline at crates/trusted-server-cli/src/commands/audit/generate/browser_collector.rs:1133 (suggestion)

Cross-cutting / body-level findings

  • 📝 --scroll is unreachable from ts audit generate and the legacy alias — both build the collector with no .with_scroll(...). This matches the design doc's scope statement, so it reads as intentional; recording the asymmetry since the collector now carries a knob one of its two callers cannot reach.
  • 📝 "Autoblog" survives in branch history — HEAD is clean (verified across every added line), but the name exists in commits 28881e053, f4f9a6e66, and 11b013005 before the 2f5cae6da scrub. If the repository policy extends to history, squash-merge this PR rather than merge-committing it.

CI Status

  • Analyze (actions): PASS
  • Analyze (javascript-typescript): PASS (×2)
  • Analyze (rust): PASS
  • CodeQL: PASS
  • browser integration tests: PASS
  • cargo check (cloudflare native + wasm32-unknown-unknown): PASS
  • cargo check/build/test (spin native + wasm32-wasip1): PASS
  • cargo fmt: PASS (required)
  • cargo test: PASS (required)
  • cargo test (axum native): PASS
  • cargo test (cross-adapter parity): PASS
  • cargo test (ts CLI, native): PASS
  • format-docs: PASS (required)
  • format-typescript: PASS (required)
  • integration tests: PASS
  • integration tests (Fastly EC lifecycle): PASS
  • prepare integration artifacts: PASS
  • vitest: PASS

Local verification at fec84adef: CLI suites 251 + 217 + 30 audit/generate/run-scoped tests passed, full crate green; clippy -D warnings clean; fmt clean; docs prettier clean.

Comment thread crates/trusted-server-cli/src/commands/audit/generate/mod.rs
Comment thread crates/trusted-server-cli/src/commands/audit/generate/slot_toml.rs Outdated
Comment thread crates/trusted-server-cli/src/commands/audit/generate/slot_toml.rs
Comment thread crates/trusted-server-cli/src/commands/audit/generate/gpt_slots.rs
Comment thread crates/trusted-server-cli/src/commands/audit/browser_scroll.rs Outdated
Comment thread crates/trusted-server-cli/src/commands/audit/generate/mod.rs
Comment thread crates/trusted-server-cli/src/commands/audit/generate/slot_toml.rs
Comment thread crates/trusted-server-cli/src/commands/audit/generate/mod.rs Outdated
Comment thread crates/trusted-server-cli/src/run.rs Outdated
Comment thread crates/trusted-server-cli/src/commands/audit/generate/browser_collector.rs Outdated
@prk-Jr

prk-Jr commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator Author

Round-six review follow-up:

  • --scroll remains intentionally scoped to ts audit ad-templates generate, matching the approved design; no separate legacy CLI surface was added.
  • HEAD is scrubbed, but intermediate commits retain the historical hostname. If the repository policy applies to all reachable history, this PR should be squash-merged.
  • All 19 inline findings are addressed in fccfcd48, with focused regressions. The local CLI suite, full adapter test/clippy matrix, parity suite, Vitest/type checks, formatting, docs lint, and docs build are green.

@prk-Jr
prk-Jr requested a review from aram356 August 28, 2026 12:35
ChristianPavilonis added a commit that referenced this pull request Aug 28, 2026
# Conflicts:
#	crates/trusted-server-core/src/publisher.rs

@aram356 aram356 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Round-7 re-review at 37006d74e. The resolution commit closes all 19 round-6 findings: both blockers are fixed with committed regression tests reproducing the exact round-6 probes (ambiguous-stem staleness, registry/request volatile refusals, broad-prefix-plus-exact-sibling observation, tuned-field split), all 12 posted suggestions landed, derive_more is properly re-gated with the derive and impl Error restored, and every docs item is applied accurately — suite 431 passed / 0 failed, clippy -D warnings and fmt clean. Requesting changes for one defect the fix introduced: the observation superset now also feeds observed_literals, so a refused ambiguous stem disqualifies the configured prefix from merge routing and produces a note naming a "configured literal prefix" that does not exist — contradicting the cli.md paragraph the same commit wrote.

2 of the inline comments below carry a one-click GitHub suggestion, both verified in a scratch worktree in isolation and together (fmt, clippy -D warnings, full CLI suite, docs prettier, byte-exact drift checks). A third candidate — the note-wording fix inside the blocking finding — is delivered as prose because the committed test at slot_toml.rs:1756 pins the current wording and must change with it, which a single-range suggestion cannot do.

Blocking

🔧 wrench

  • Refused stems leak into observed_literals, changing prefix routing and mislabeling the note — see inline at crates/trusted-server-cli/src/commands/audit/generate/slot_toml.rs:233

Non-blocking

🤔 thinking / ⛏ nitpick

  • 🤔 The case-alternation gate narrows the token rule but reopens a hole for single-case random suffixes — see inline at crates/trusted-server-cli/src/commands/audit/generate/gpt_slots.rs:294
  • matching_observed_div_indexes allocates a Vec per evidence div — see inline at crates/trusted-server-cli/src/commands/audit/generate/slot_toml.rs:418 (suggestion)
  • cli.md documents the split but not the split note — see inline at docs/guide/cli.md:280 (suggestion)
  • Two names for one 5-second CDP bound in a single call graph — see inline at crates/trusted-server-cli/src/commands/audit/browser_scroll.rs:8

Cross-cutting / body-level findings

  • Commit message uses a forbidden semantic prefix: fccfcd484 is fix(cli): resolve ad-template generation review gaps — CLAUDE.md's commit conventions ban semantic prefixes and every other authored commit on the branch follows the sentence-case imperative style. Reword on the next amend, or moot it by squash-merging.
  • 📝 Squash-merge reminder stands: HEAD is clean of real-world tokens, but the branch's 216 authored commits still carry customer names in commit messages (e.g. "Update creative-opportunities config to real autoblog.com GAM values"). A squash-merge keeps them out of main's history.

CI Status

  • Analyze (actions): PASS
  • Analyze (javascript-typescript): PASS (×2)
  • Analyze (rust): PASS
  • CodeQL: PASS
  • browser integration tests: PASS
  • cargo check (cloudflare native + wasm32-unknown-unknown): PASS
  • cargo check/build/test (spin native + wasm32-wasip1): PASS
  • cargo fmt: PASS (required)
  • cargo test: PASS (required)
  • cargo test (axum native): PASS
  • cargo test (cross-adapter parity): PASS
  • cargo test (ts CLI, native): PASS
  • format-docs: PASS (required)
  • format-typescript: PASS (required)
  • integration tests: PASS
  • integration tests (Fastly EC lifecycle): PASS
  • prepare integration artifacts: PASS
  • vitest: PASS

Local verification at 37006d74e: CLI 431 lib + integration suites passed, 0 failed; clippy -D warnings clean; fmt clean; docs prettier clean.

Comment thread crates/trusted-server-cli/src/commands/audit/generate/slot_toml.rs Outdated
Comment thread crates/trusted-server-cli/src/commands/audit/generate/gpt_slots.rs
Comment thread crates/trusted-server-cli/src/commands/audit/generate/slot_toml.rs Outdated
Comment thread docs/guide/cli.md Outdated
Comment thread crates/trusted-server-cli/src/commands/audit/browser_scroll.rs Outdated
@prk-Jr

prk-Jr commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator Author

Round-seven body-level follow-up:

  • The new resolution commit uses the repository convention: c484d715 Resolve ad-template generation review findings.
  • The earlier semantic-prefix commit and historical real-world names remain in branch history, so the squash-merge requirement still applies.
  • All five inline findings are addressed and resolved. The complete local CLI suite, six adapter clippy targets, Fastly/Axum/Cloudflare/Spin tests, parity, Vitest/type checks, formatting, docs lint, and docs build passed.

aram356 added a commit that referenced this pull request Sep 3, 2026

@aram356 aram356 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Round-8 re-review at 0ede1a1fd. c484d7158 resolves all six round-7 findings with evidence: the observed/literals split is plumbed through a new EvidenceTable::observed_literals() and makes the A/B routing probe symmetric, the token gate now catches the four hex and single-case hashes while dropping both CamelCase false positives, and the three nitpicks (iterator return, cli.md sentence, timeout-constant unification) landed with no behavior change at any call site. Both commit messages follow the sentence-case convention. Requesting changes for an over-correction in that same token fix — ALL-CAPS placement labels including four standard IAB format names are now refused as volatile — and for three structural issues in 03743c153, which adds a GPT-stability wait that sits outside --settle-max-ms, exits on a weaker criterion than the one implemented twenty lines below it, and returns partial evidence silently.

1 inline comment carries a one-click suggestion, verified in a scratch worktree: cargo fmt --all -- --check clean, cargo clippy -p trusted-server-cli --all-targets -- -D warnings clean, full CLI suite green at 435 passed / 0 failed — with the one companion fixture edit named in that comment, which a single-range suggestion cannot carry. The remaining comments describe fixes in prose because they span files, need a new test seam, or are design calls.

Blocking

🔧 wrench

  • ALL-CAPS placement labels refused as volatile families — see inline at crates/trusted-server-cli/src/commands/audit/generate/gpt_slots.rs:309 (suggestion)
  • GPT-stability wait escapes --settle-max-ms, up to 5s per page per profile — see inline at crates/trusted-server-cli/src/commands/audit/generate/browser_collector.rs:966
  • Stability criterion exits on two samples 250ms apart, weaker than the sibling idiom — see inline at crates/trusted-server-cli/src/commands/audit/generate/browser_collector.rs:999
  • Bound expiry returns partial evidence silently; timeout warning misattributes the cause — see inline at crates/trusted-server-cli/src/commands/audit/generate/browser_collector.rs:976

❓ question

  • Should verify/page get the same wait, or is the asymmetry structural? — see inline at crates/trusted-server-cli/src/commands/audit/generate/browser_collector.rs:966

Non-blocking

♻️ refactor / 🤔 thinking / ⛏ nitpick

  • ♻️ New behavior has zero deterministic coverage (all three fixtures are Chrome-gated) — see inline at crates/trusted-server-cli/src/commands/audit/generate/browser_collector.rs:1435
  • 🤔 The new fixture cannot detect the early-exit weakness — see inline at crates/trusted-server-cli/src/commands/audit/generate/browser_collector.rs:1195
  • Order-sensitivity undocumented; the wait reaches no docs — see inline at crates/trusted-server-cli/src/commands/audit/generate/browser_collector.rs:962
  • distinct_ascii_bytes allocates 256 bytes for a ≤64-byte question — see inline at crates/trusted-server-cli/src/commands/audit/generate/gpt_slots.rs:321
  • Two assertions without messages in the multi-ancestor split test — see inline at crates/trusted-server-cli/src/commands/audit/generate/slot_toml.rs:1838

Cross-cutting / body-level findings

  • 📝 No progress output during the stability wait: CollectionProgress emits nothing while collect_stable_gpt_slots polls, so a page that burns the full budget shows as a frozen Auditing … [n/m]: /path line for 5s with no explanation. Worth a phase line or a note once the budget question above is settled.
  • 📝 Squash-merge reminder stands: HEAD remains clean of real-world tokens, but the branch's authored commit messages still carry customer names from earlier rounds. A squash-merge keeps them out of main's history.

CI Status

  • Analyze (actions): PASS
  • Analyze (javascript-typescript): PASS (×2)
  • Analyze (rust): PASS
  • CodeQL: PASS
  • browser integration tests: PASS
  • cargo check (cloudflare native + wasm32-unknown-unknown): PASS
  • cargo check/build/test (spin native + wasm32-wasip1): PASS
  • cargo fmt: PASS (required)
  • cargo test: PASS (required)
  • cargo test (axum native): PASS
  • cargo test (cross-adapter parity): PASS
  • cargo test (ts CLI, native): PASS
  • format-docs: PASS (required)
  • format-typescript: PASS (required)
  • integration tests: PASS
  • integration tests (Fastly EC lifecycle): PASS
  • prepare integration artifacts: PASS
  • vitest: PASS

Local verification at 0ede1a1fd: CLI 436 lib tests + integration suites passed, 0 failed; clippy -D warnings clean; fmt clean.

Comment thread crates/trusted-server-cli/src/commands/audit/generate/gpt_slots.rs
Comment thread crates/trusted-server-cli/src/commands/audit/generate/browser_collector.rs Outdated
Comment thread crates/trusted-server-cli/src/commands/audit/generate/browser_collector.rs Outdated
Comment thread crates/trusted-server-cli/src/commands/audit/generate/browser_collector.rs Outdated
Comment thread crates/trusted-server-cli/src/commands/audit/generate/gpt_slots.rs
Comment thread crates/trusted-server-cli/src/commands/audit/generate/slot_toml.rs
The single-case branch of `looks_random_suffix` returned before the
word-shape check ever ran, so any all-caps run of four or more distinct
letters read as a hash. Four standard IAB format names were among the
casualties: a div id such as `promo-20260820BILLBOARD-sidebar` had its
whole family dropped from the generated config, with a warning telling
the operator to change markup that is in fact stable. The mirror case
was missing entirely, since a lowercase hash like `zzqxwvkm` passed.

Make the branch case-symmetric and gate it on vowel density instead of
case, keeping the pure-hex check ahead of it so `deadbeef` and
`ABCD1234` still catch. Swap the `ABCDEFGH` fixture for `XKMPQRST`: a
sequential alphabet run carries a word's vowel spread and is no longer
a hash shape.

Also replace the 256-byte flag array in `distinct_ascii_bytes` with a
four-word bitset, and give the two message-less assertions in the
multi-ancestor split test the wording the third one already had.
The wait budgeted against the hardcoded five-second page-operation
constant and started its own clock after both settle passes had already
finished, so worst-case page time grew by an amount no flag could tune.
Take the budget from `settle_max` and the dwell from `settle_quiet`
instead, and give each CDP read back its own fixed bound, so a wedged
evaluate is reported distinctly from an exhausted budget.

Exit on a dwell rather than on one matching pair. Two reads 250ms apart
can both observe the same registration burst and miss the next, which
is the batching the wait exists to survive; mirror the `quiet_since`
idiom of `wait_for_page_settle` and require the reading to repeat for
the dwell window.

Warn when the budget expires with a snapshot still in flight. Both
expiry paths returned silently while every other exit warned, so the
one condition the wait exists to detect produced a partial slot list
indistinguishable from a stabilized read. An empty registry stays
silent, because the caller's googletag-state diagnostic names the
actual cause.

Extract the classification into `gpt_registry_reading` so the
empty-reset, order-sensitivity and repeat-match rules have deterministic
coverage off the Chrome-gated path, and document why order sensitivity
is kept: slot discovery takes the network id from the first usable entry
and emits slots in registry order. Add a fixture whose second burst
lands on a real timer rather than on observation, which the previous
criterion cannot pass, and a note at the call site recording why
verification needs no counterpart wait.
Three conflicts, all from main's EdgeZero v0.0.7 upgrade (0f8b44d).

`run.rs` needed a union rather than a side. Main added the `active-version`,
`healthcheck`, `rollback` and `config gc` commands against a `dispatch`
returning `Result<(), String>`, while this branch changed that signature to
`Result<RunOutcome, String>` to carry assertion exit codes. Main's four new
arms therefore take the branch's `.map(|()| RunOutcome::Success)` wrapper.
Two of those arms had auto-merged outside the conflict markers without it,
which would not have compiled.

Kept both test sets, placed at main's own insertion points so the merge does
not reorder the tests main already had. `parses_audit_with_default_outputs`
and `parses_audit_with_custom_outputs` stay deleted: they cover the
merge-base audit surface this branch replaced, and main never touched them.

`Cargo.lock` needed the `toml_edit` entry version-qualified, because main's
upgrade puts a second `toml_edit` in the graph; the workspace pins 0.23.10,
so the CLI keeps that one alongside this branch's `tracing`.

`docs/guide/cli.md` had both sides adding a section at the same anchor.
Main's `config gc` prose continues the `config push` discussion above it, so
it leads, followed by this branch's ad-template diagnostics heading.
@prk-Jr
prk-Jr requested a review from aram356 September 4, 2026 14:00

@aram356 aram356 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Round-8 review at 8f0715c59. The two newest commits independently resolved four of this round's concerns before they were posted: the GPT stability wait is now bounded by the operator's --settle-max-ms with the per-read CDP bound kept separate, the ALL-CAPS placement-label false positives are fixed by a case-symmetric vowel gate (probed in both directions: LEADERBOARD-class labels eligible, XKMPQRST/deadbeef/ABCD1234 still refused), the extracted registry-reading helpers gained deterministic tests, and the wait is documented in cli.md. The earlier cold-start Chrome flake did not recur across five runs. Requesting changes for the two defects that survived: the empty-registry path still burns the full budget — now 12s instead of 5s per GPT-less page (measured 19.2s end-to-end on a googletag: undefined page) — and the refused-plus-written stem gap still lets a configured prefix swallow a sibling with an inherited floor price (reproduced through the real merge path), plus one unanswered direction question on the multi-ancestor split warnings.

1 of the inline comments below carries a one-click GitHub suggestion, verified in a scratch worktree (fmt, clippy -D warnings, full CLI suite, byte-exact drift check). The two blocking fixes are delivered in prose: the empty-registry exit needs loop-state changes across three regions, and the observed_literals filter change must land together with the test at evidence.rs:462-483 that codifies the current behavior.

Blocking

🔧 wrench

  • Empty-registry burn survived the re-bounding and grew 5s → 12s per GPT-less page — see inline at crates/trusted-server-cli/src/commands/audit/generate/browser_collector.rs:1060
  • A refused-plus-written stem still drops out of observed_literals, re-arming the configured prefix — see inline at crates/trusted-server-cli/src/commands/audit/generate/evidence.rs:215

❓ question

  • Split warnings name every tuned ancestor while routing is longest-wins — which direction? — see inline at crates/trusted-server-cli/src/commands/audit/generate/slot_toml.rs:294

Non-blocking

🤔 thinking / ⛏ nitpick

  • 🤔 Worst-case page time is now 3×--settle-max-ms, documented as design — see inline at crates/trusted-server-cli/src/commands/audit/generate/browser_collector.rs:711
  • 🤔 The vowel gate still refuses vowel-free consonant-abbreviation labels — see inline at crates/trusted-server-cli/src/commands/audit/generate/gpt_slots.rs:317
  • --settle-max-ms 0 silently skips the GPT read — see inline at crates/trusted-server-cli/src/commands/audit/generate/browser_collector.rs:1031
  • The "do not port this into audit::browser" comment overclaims its scope — see inline at crates/trusted-server-cli/src/commands/audit/generate/browser_collector.rs:705 (suggestion)
  • Third name for the same 5-second CDP bound persists — see inline at crates/trusted-server-cli/src/commands/audit/generate/browser_collector.rs:37

CI Status

  • Analyze (actions): PASS
  • Analyze (javascript-typescript): PASS
  • Analyze (javascript-typescript): PASS
  • Analyze (rust): PASS
  • CodeQL: PASS
  • browser integration tests: PASS
  • cargo check (cloudflare native + wasm32-unknown-unknown): PASS
  • cargo check/build/test (spin native + wasm32-wasip1): PASS
  • cargo fmt: PASS (required)
  • cargo test: PASS (required)
  • cargo test (axum native): PASS
  • cargo test (cross-adapter parity): PASS
  • cargo test (ts CLI, native): PASS
  • format-docs: PASS (required)
  • format-typescript: PASS (required)
  • integration tests: PASS
  • integration tests (Fastly EC lifecycle): PASS
  • prepare integration artifacts: PASS
  • vitest: PASS

Local verification at 8f0715c59: CLI suites 457 lib + 35 integration tests passed, 0 failed; Chrome-gated generate set 4 passed under TS_AUDIT_BROWSER_TESTS=1; clippy -D warnings clean; fmt clean.

Comment on lines +1060 to +1063
GptRegistryReading::Empty => {
previous_nonempty = None;
stable_since = None;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 wrench — The empty-registry burn survived the re-bounding and grew from 5s to 12s per GPT-less page. This Empty arm only resets state and falls through to the sleep; there is no early exit for a registry that can never become non-empty, and the budget is now the full --settle-max-ms default (12s) instead of the old 5s constant. Measured at HEAD: a page with googletag: undefined takes 19.2s end-to-end (12s in this loop); collects_lazy_gpt_slot_only_when_scroll_is_enabled went 5.2s (parent of the stability commit) → 18.8s now. The caller's own diagnostic proves the wait is wasted — it reports googletag: undefined the moment the loop gives up, and that state is knowable on the first read.

Fix (apply manually — needs a previous_empty flag beside the loop state at line 1028 and clearing in the Changed/Repeated arms):

GptRegistryReading::Empty => {
    // A registry that reads empty twice has no registration in flight to
    // wait for; the caller's googletag-state diagnostic names the cause.
    if previous_empty {
        return latest_nonempty;
    }
    previous_empty = true;
    previous_nonempty = None;
    stable_since = None;
}

And please add a deterministic test for collect_stable_gpt_slots itself covering the empty path — the loop is still reachable only through Chrome-gated fixtures, which is why this survived the extracted-helper tests the stability commit added.

Comment on lines +215 to +219
self.order
.iter()
.filter(|div_id| !self.ambiguous_stems.contains(*div_id))
.filter(|div_id| !self.refused_div_ids.contains(*div_id))
.map(String::as_str)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 wrench — A stem that is both refused and written still drops out of observed_literals(), re-arming the configured prefix. Reproduced through the real merge path at HEAD: configured literal ad-x (floor_price 5.0) observed concretely on page A while page B refused the stem — the discovered ad-x-extra is silently swallowed into the configured slot and inherits a floor price it was never observed under, the exact loss class the role separation was built to prevent.

Proposed fix (apply manually — later_refusal_removes_a_previously_accepted_literal at lines 462-483 codifies the current drop and must be updated in the same change to assert the written-slot exception, which is why this cannot be a one-click suggestion):

self.order
    .iter()
    .filter(|div_id| !self.ambiguous_stems.contains(*div_id))
    // A stem refused on one page but written as a concrete slot from
    // another is still a literal element. Dropping it re-arms a
    // configured prefix that then swallows the sibling.
    .filter(|div_id| {
        !self.refused_div_ids.contains(*div_id) || self.slots.contains_key(*div_id)
    })
    .map(String::as_str)

That test currently asserts only on observed_literals() and never on whether the stem was also written as a slot, so it does not cover the swallowing case.

Comment on lines +294 to +310
if let Some(discovered_div) = slot.div_id.as_deref() {
for parent in merged[..existing_count].iter().filter(|configured| {
configured.div_id.as_deref().is_some_and(|prefix| {
!prefix.is_empty()
&& observed_literals.contains(prefix)
&& discovered_div != prefix
&& discovered_div.starts_with(prefix)
}) && configured.has_tuned_fields()
}) {
split_warnings.insert(format!(
"discovered div `{discovered_div}` was split from configured div_id prefix \
`{}`; the new slot does not inherit that configured slot's floor price, \
targeting, or provider settings",
parent.div_id.as_deref().unwrap_or_default(),
));
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question — Split warnings name every tuned ancestor, but routing is strictly longest-wins (matching_div_id_index), so with nested tuned prefixes (ad and ad-side both configured) the broader-ancestor note describes an inheritance that never would have happened, and note volume scales with nesting depth. Unchanged through two rounds. Which direction: restore max_by_key so the warning matches routing exactly, or keep the multi-parent report and mark the routing parent in the text (e.g. "split from configured div_id prefix ad-side (and 1 broader configured prefix)")?

// any point before the read is already accumulated in its store. This
// command has no such hook and reads a `getSlots()` snapshot instead, which
// is why only this side can observe a half-registered registry.
let gpt_slots = collect_stable_gpt_slots(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 thinking — Worst-case page time is now 3 × --settle-max-ms: the initial settle, the post-scroll settle, and this GPT wait each get the full budget as sequential phases — 36s per page at the default, and the new expiry warning's advice ("raise --settle-max-ms") triples in cost. The phases each honor their own bound, so this is a design multiplier rather than a bug, and cli.md now documents it as such — but the GPT wait runs last and could take the remaining per-page budget (settle_max.saturating_sub(page_start.elapsed())) instead, matching the operator's mental model of a per-page cap. Author's call; flagging the arithmetic (19.2s measured on a single GPT-less page) rather than requesting.

Comment on lines +317 to +321
let single_case = value.bytes().all(|byte| byte.is_ascii_uppercase())
|| value.bytes().all(|byte| byte.is_ascii_lowercase());
if single_case && distinct >= 4 && !has_vowel_structure(value) {
return true;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 thinking — The vowel gate's residue: vowel-free consonant abbreviations still refuse as random — probed MPUTOPRHT, DFPSLTTOP, LDRBRDTOP, SKYSCRPR, STCKYFTR (all real placement-label shapes) all return looks_random_suffix = true once they reach 8 chars with ≥4 distinct bytes. This is far narrower than the old ALL-CAPS class and tightening further risks the opposite error, so no code change requested — but a comment acknowledging the residue would stop the next reviewer from rediscovering it, since an 8-char token genuinely carries no signal separating DFPSLTTOP from a hash.

let mut stable_since = None;

loop {
if start.elapsed() >= budget {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick--settle-max-ms 0 silently skips the GPT read entirely: the budget check runs before the first read, so a zero budget returns an empty vec having never evaluated GPT_SLOTS_SCRIPT, and the expiry warning stays silent on an empty snapshot by design — config generated from zero GPT evidence with only the generic diagnostic. validate_settle_window accepts --settle-max-ms 0 --settle-quiet-ms 0. Reading once before consulting the budget makes the degenerate case return real evidence.

Comment on lines +705 to +710
// Verification needs no counterpart wait, so do not port this into
// `audit::browser`: its evidence collector is injected before publisher
// scripts run and wraps `googletag.defineSlot`, so every slot defined at
// any point before the read is already accumulated in its store. This
// command has no such hook and reads a `getSlots()` snapshot instead, which
// is why only this side can observe a half-registered registry.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick — This comment overclaims its scope: the guarantee is true for the ad-template verifier (whose collector wraps googletag.defineSlot before publisher scripts run) but ts audit page routes through the same audit::browser collector with init_scripts: Vec::new() and no such hook — it reads a bare snapshot. A future reader takes this as a blanket guarantee about audit::browser.

Suggested change
// Verification needs no counterpart wait, so do not port this into
// `audit::browser`: its evidence collector is injected before publisher
// scripts run and wraps `googletag.defineSlot`, so every slot defined at
// any point before the read is already accumulated in its store. This
// command has no such hook and reads a `getSlots()` snapshot instead, which
// is why only this side can observe a half-registered registry.
// The ad-template verifier needs no counterpart wait, so do not port this
// into `audit::browser`: its evidence collector is injected before
// publisher scripts run and wraps `googletag.defineSlot`, so every slot
// defined at any point before the read is already accumulated in its
// store. This command has no such hook and reads a `getSlots()` snapshot
// instead, which is why only this side can observe a half-registered
// registry. (`ts audit page` also reads a bare snapshot, but it only
// reports what it saw rather than generating config from it.)

/// whatever rendered by then.
const NAVIGATION_LOAD_TIMEOUT: Duration = Duration::from_secs(12);
const BROWSER_CLOSE_TIMEOUT: Duration = Duration::from_secs(5);
const PAGE_OPERATION_TIMEOUT: Duration = Duration::from_secs(5);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick — Third name for the same 5-second bound persists: PAGE_OPERATION_TIMEOUT here and CDP_OPERATION_TIMEOUT in browser_scroll.rs:9 are both Duration::from_secs(5) bounding single CDP operations, and the stability commit made the duplication more visible by naming one of them in the new per-read timeout warning. Import the shared constant and delete this one (~10 call-site renames in this file).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Build a standalone validation tool for configuring creative opportunities

3 participants