Three violations, found and repaired by the system itself. Left is what was uploaded; right is what came back. Every clip below is footage this project generated and then fixed with its own methods: no stock, no borrowed IP.
|
France · Loi Évin · alcohol. French law bans alcohol advertising on television outright. The wine becomes pale juice and the bottle becomes a carafe; the toast, the light and the laugh survive untouched. |
|
EU · AVMSD art. 9 · tobacco. A lit cigarette becomes an espresso cup, and every wisp of smoke leaves the frame. This one took two passes: the first edit held everywhere except the drag itself, which is exactly why the product re-watches every fix instead of trusting the model's word. |
|
UAE · Federal Decree-Law 55/2023 · modesty. The hemline drops to the knee in the same grey fabric, mid-stride, same street, same woman. |
Note
These are the marketing clips, and they were held to the product's own standard. The first take of the skirt pair came back with legible Chanel storefronts in the background (third-party branding, on the homepage of a rights-clearance tool), so it was thrown away and regenerated with brands banned.
Every frame below is the running console, shot from the deployed instance rather than drawn.
A global brand ships one commercial into forty markets and checks maybe six of them.
The regulated half of the risk already carries an invoice: France bans TV alcohol advertising under Loi Évin, Quebec bans advertising aimed at children under 13, the UK pre-clears every TV ad through Clearcast, Nigeria requires ARCON vetting before air. The unregulated half costs more: Pepsi and Kendall Jenner, Dolce & Gabbana in China, H&M's hoodie. Every one of those was caught by the public rather than by a process.
Pilots get a pre-flight checklist and an instrument panel. Ad launches get a consultant, a focus group, and luck.
This is the instrument.
Hand it a commercial and a market list, then watch it clear customs in real time.
The output is two things: a decision (can this air here, and on what evidence) and a localized master (the version that can).
| Jurisdictions | 21 market packs resolving to a global baseline, the EU, 16 countries and 80 broadcasters, 1,063 market-rule pairings once inheritance resolves |
| Rules | every one naming a real statute, broadcaster code or cultural norm, with a live citation |
| Dimensions | one fixed taxonomy the analyst emits and every rule is written against |
| Repair methods | from a €0.04 single-frame patch to a €3.68 Veo-generated bridge, priced in euro before you press, and the generative two take an instruction of your own |
| Agents | a Google Cloud Agent Builder (ADK) SequentialAgent named customs_crew, with the adjudicators fanned out in parallel |
| The refusal | when a rule targets a protected characteristic, a rule-layer Guard refuses to auto-edit and hands it to a human |
The expensive part of this problem is looking at the film. The cheap part is opinions about what was seen.
So the system separates them, and never lets them mix:
flowchart LR
A["🎬 one commercial"] --> B["Analyst<br/>watches each shot ONCE<br/>Gemini vision"]
B --> C["neutral observations<br/>timecoded · boxed · no verdicts<br/>18-dimension taxonomy"]
C --> D1["Adjudicator · FR"]
C --> D2["Adjudicator · EU"]
C --> D3["Adjudicator · AE"]
C --> D4["Adjudicator · …98"]
D1 --> E["findings<br/>observation × rule × citation"]
D2 --> E
D3 --> E
D4 --> E
E --> F["🚦 decision + localized master"]
style A fill:#e8f0fe,stroke:#4285F4,color:#17222c
style B fill:#fef7e0,stroke:#FBBC05,color:#17222c
style C fill:#e6f4ea,stroke:#34A853,color:#17222c
style E fill:#fce8e6,stroke:#EA4335,color:#17222c
style F fill:#e8f0fe,stroke:#4285F4,color:#17222c
The 18 dimensions, and what each watches for
One fixed vocabulary. The analyst may only emit these, and every one of the 128 rules is written against one of them, which is what makes the join between a fact and a market's opinion a lookup rather than an argument.
A finding is always a join: this observation × that market's rule × a citation that resolves. Which makes "is this fact wrong?" and "is this rule wrong?" separable questions, and that is the whole reason findings are cheap, parallel and defensible.
The analyst is forbidden from expressing an opinion. It writes "a woman raises a glass of red wine", never "this violates French law". Ninety-eight adjudicators then argue about that one sentence, simultaneously, each holding a different rulebook.
Five stages, in order, from src/customs/crew.py. The stage names below are the ones the code emits.
And two the crew meets only when something has to change:
|
verifier Re-runs the real analyst pass over the changed shots (the same instrument that found the problem) to confirm the violation is gone and that nothing new broke. |
Stage by stage, with the file that does it
1 · ingest: crew.IngestAgent. media.detect_shots runs an ffmpeg scene-score ladder, analyst.merge_micro_shots folds away sub-second fragments. Then one Gemini audio call per shot for a transcript. Then media.detect_flashes, which is the one observation that is measured rather than asked of a model: it reads full-frame luminance and every window over 3.0 flashes/second becomes a photosensitivity observation. A vision model cannot see a strobe (a strobe is a property of the sequence, not of any frame), and this is exactly how the milestone gate once missed a planted 6 Hz strobe.
2 · analyst: crew.AnalystAgent. One Gemini vision call per shot, keyframes as image parts, the 18-dimension taxonomy interpolated into the prompt. Emits observations with bounding boxes and evidence frames. No verdicts allowed.
3 · adjudicators: crew.AdjudicatorAgent inside an ADK ParallelAgent, one per market, each on its own thread. N markets cost one market's latency, not N. Each does a pure dimension-equality join against its YAML pack, then one batched Gemini call, then one grounded Google Search citation per triggered rule. Severity, sourced, the citation, remediability and the finding id are all decided in code: the model may only adjust severity downward, clamped to [−20, 0]. An unresolvable citation caps severity at 40 and can never trigger remediation.
4 · guard: crew.GuardAgent. Runs after the parallel branch joins, which makes it the single place findings are written to SQLite, on one thread. See The Guard.
5 · publisher is crew.PublisherAgent, the genuinely agentic stage: a BaseAgent wrapping an LlmAgent with five tools, three of which are live Grafana MCP calls. It issues each call itself, reads every result, decides what to do when one fails, and composes the prose it writes into the overview dashboard's description. That last step is a real MCP write, on every run, in words no template produced.
This is the Grafana Labs track, and the requirement is that the project actively use the Grafana stack at runtime, primarily through the Grafana Cloud MCP server. It does, in three directions, and you can watch all three happen on the live instance.
Important
The MCP server is the official grafana/mcp-grafana v1.1.0, run as a stdio subprocess with a service-account token. Grafana's own documentation notes the hosted Cloud MCP endpoint "authenticates users interactively — there is no service-account or machine-token option." This crew is unattended, so the open-source server is the documented correct choice, not a workaround.
The Publisher agent calls MCP tools to build its own instrument panel: 9 dashboards / 40 panels, an annotation per finding, and the alert rules that will later wake the Remediator. Seven distinct mcp-grafana tools are called at runtime: create_folder, update_dashboard, alerting_manage_rules, get_panel_image, query_loki_logs, search_dashboards, get_dashboard_by_uid.
What lands in the stack:
The clock trick that makes it read as a timeline: Prometheus rejects backdated samples, so each run maps the film's timecode onto the wall clock at run start. Video second n is written at wall clock t₀ + n. The panel's x-axis reads as the timecode because it is the timecode.
Two alert rules evaluate every 30 seconds. When customs_blocking crosses 70 for an {asset, market, rule_id} triple, Grafana posts to the customs-webhook contact point, which is POST /webhook/alert on this service. The webhook reads labels only, names the finding, and hands off to a worker.
An alert in Grafana is what starts a Veo render. Not a cron, not a queue: a threshold in a dashboard the agent built itself.
That grid is one visual drawn by two systems. The taxonomy icons down the side and each scene's opening frame across the top are the console's: no Grafana panel can put an image on an axis, which was verified by reading the renderers, not by guessing. The squares between them are Grafana's own status-history panel, live, with its row labels and time axis hidden because the console is those axes. Columns are as wide as their scenes are long, so a column's share of the width is its share of the film, which is exactly what the panel's hidden time axis measures.
Click a square and a data link fires /launch/remediate with the click's coordinate. The console resolves which open finding lives there and starts the Omni rewrite for that scene.
Why the panels are embedded from a self-hosted viewer
Grafana Cloud will not be framed. Fourteen URL forms were probed with real browser and iframe headers (/d/, every ?kiosk variant, /d-solo/, public dashboards, snapshots), and every one returns an enforcing Content-Security-Policy: frame-ancestors 'none'. PUT /api/admin/settings answers 403, because on Cloud that switch belongs to Grafana: their embedding guide grants it per-tenant, through the account team, as an origin allowlist.
So grafana-viewer/ is a stock grafana-oss with allow_embedding=true that holds no data of its own: its Loki and Mimir datasources read the same Grafana Cloud stores the crew writes to, at the same datasource UIDs the dashboards name, on a purpose-minted access policy scoped to logs:read + metrics:read and pinned by label to {app="customs"}. It answers frame-ancestors 'self' <the console>, so only the console may frame it.
The crew still writes to Grafana Cloud over MCP. The viewer is a second pair of eyes on the same data.
Not a claim, four renders. Every panel below is windowed to the run's own mapped clock, because that is where this system's telemetry lives: wall second t0+n is video second n. Point the stock dashboards at now-6h and they say No data, which is the first thing anyone gets wrong here.
Loki, kind="observation": one line per keyframe. Labels carry app, asset, dimension, flagged and kind; the body carries the analyst's own sentence, its confidence, the shot and the window, and which markets objected.
Loki, kind="finding": the join, with the statute in it. market and rule_id are labels, so a market's findings are a selector rather than a scan. The body holds the class, the severity, the rationale, and the grounded citation_url that was resolved at adjudication time.
| Loki, by label | Mimir, on the film's clock |
|---|---|
![]() |
![]() |
| Counted from the stream labels alone, no body parsing. | customs_risk, one sample per video second, which is what makes the timecode an x axis. |
The full inventory of what this system keeps in Grafana (9 dashboards, 40 panels, 9 metric series, 3 log streams, 3 alert rules, and which of the 8 write operations goes over MCP versus the provisioning API) is a screen in the console itself: /grafana.
Fourteen screens, each one a way of asking the same question at a different distance -- and a guided tour, for anyone who would rather be shown.
A first-time reader met two doors and a paragraph. There is a third door now: fourteen slides that say what the thing is, and then a walk across the live console -- twelve stops, each one dimming a real screen and spotlighting the thing worth looking at. Every number on a slide is read from the instance at render time, and nothing in the walk is a screenshot.
Hand it a file, pick the markets, and watch it clear customs live. No password: the visitor door asks for nothing, and what bounds a stranger is the generation cap behind it.
Every clearance this instance has performed. Hover a card and the whole film plays as a five-second timelapse. The squares beneath it are the console's own drawing by default, with a switch on every card to Grafana's live panel of the same answer -- and in the drawn one, hovering a dot plays that second of the film, because the card's clip is the whole film in five seconds and the chart knows the mapping.
The verdict, market by market, flipping in place as each adjudicator returns.
And on the same page, the crew's own lanes dashboard, live, framed, and clickable:
One grid drawn by two systems: our icons down the side and each scene's opening frame across the top, Grafana's live status history as the body between them. Click a square to start the Omni rewrite for that scene.
Every move the crew made, in the order it made them, each stage saying in plain words what it is doing, with its own shorthand underneath.
Every scene the crew looked at, how it opens and closes, and the neutral sentences it wrote before any market saw them.
One panel per scene. Closed, a scene is its first and last frame and a count; opened, every finding written against it, each with the statute behind it and a priced fix.
The original and the localized master, playing in lockstep on the second that changed.
What the models actually made for one run, next door to the cutting room rather than behind a tab on the event log: every change record with the stills a patch wrote, the seconds Gemini Omni or Veo rendered, and for a bridge the two anchor frames it was handed. Those two frames are the entire brief, which makes them the only way to tell whether it invented something or was given it.
The cross-run cutting room, and the answer to a different question: not "what happened to this film" but what has this system actually changed. One card per scene, before beside after, played rather than photographed -- each side is the span the finding names, cut from a master on the server and cached, with the kept still as its poster. Hovering runs both clips together.
One card per scene and not per market, because three jurisdictions objecting to the same two seconds is one shot with three objections and only one of them paid for the render: every market that objected is named on the card, and so is the one the fix on show was made for. A revoice is a sound edit rather than a picture edit -- as a still it looks like a frame that did not change -- so those live on the other side of a picture/sound toggle, with their soundtrack and real controls.
Every card names its fix type in words, because whether a model re-rendered the span or a still was patched over it is exactly the difference between the fixes that hold and the fixes the verifier sends back. And an edit can be removed: the one control in this console that destroys evidence, so it asks for a word first, says what it takes, and takes it everywhere at once.
A second ADK surface: one LlmAgent with eleven tools. Ask in sentences; it answers by opening the evidence beside you and pricing any fix before it runs.
Ask it for Grafana and it builds Grafana. build_dashboard composes a dashboard from the run's own labels; chart takes any of fourteen panel types with the LogQL or PromQL behind each. Either way the result is a real dashboard on the stack, opened beside the answer as Grafana's own render, with the link to the live one under it.
Every rule, filed under the observation that can trigger it.
Every caption the analyst ever wrote is a Loki line, so "which frames have a rabbit in them" is a question rather than a feature somebody anticipated, and what comes back is the frames themselves across every run. The match is semantic: the question and the candidate captions go to Gemini together, so bunnies finds "an animated rabbit character" without anybody guessing which word a vision model chose months ago, and each card says in the model's own words why it is there. A carrot is not a rabbit. mode=literal switches to a regex over the caption alone for when you mean the characters, a rule id or a brand. The agent has it as search_frames, which returns the counts first, so how many frames show a short skirt is answered with a number and the pictures beside it.
Every other screen answers a question about one commercial. This one reads across all of them, out of the same two stores the crew wrote during those runs: 17 panels and 8 kinds of chart over every clearance this instance has performed. Which subject your creative keeps tripping over, which jurisdictions are actually hard, the dimension-against-market matrix, the severity distribution, worst severity per market against the line where a finding starts to block, market status over time out of Mimir, and the raw finding stream underneath it all.
The division of labour is the design. Grafana charts the data because it holds it; the console draws the axis because Grafana has never heard of an eighteen-part taxonomy or a broadcaster channel's mark. The hero is one LCD block per commercial, lit to the highest severity any market ever recorded against it, with that film's own first frame directly underneath it in the same order: green cleared, blue noted, red past the 70 where a finding begins to block. Both halves sort by value and then by name, because the store promises neither: sort_desc says nothing about how a tie is broken, seven of these films sit at 95, and five executions of that one query came back in five different arrangements of them. The panel does it in a Grafana transformation, the console does it in Python, and the sixteen posters line up with the sixteen blocks position for position.
Two of the three alert rules ask Grafana to wake the Remediator. The third asks it to stop: the loop that fixes a finding by generating video is the loop that can empty a day's allowance while nobody is watching. Every charge writes customs_spend_eur_total and customs_budget_remaining_eur to Mimir on the real clock, and customs_budget_low fires when what is left crosses a floor set above the most expensive single fix. The webhook then holds automatic remediation for the rest of the UTC day: findings still block, alerts still arrive and are recorded in the feed with the reason, and a person at the console can still spend what is left one fix at a time. The pause is on the path nobody is watching, which is the only one that needed one.
The whole Grafana surface on one page: 9 dashboards and every panel on them, the 9 metric series and 3 log streams the crew writes and which clock each sits on, the annotations, all three alert rules with the expression that fires them, and every write operation with the MCP tool it uses or the REST call it falls back to when mcp-grafana 1.1.0 has no tool for it. Read from the definitions the Publisher provisions from, so it cannot drift into describing a stack nobody has.
Everything that is working wears one mark: a rotating ring of the four brand colours. An uploading master, a run mid-analysis, a market tile whose fix is landing, the exact frame being repaired, the stage narrating it, and a beacon in the topbar that says what is running wherever you are.
Five methods are offered in the console picker, each one a different amount of footage to disturb. The mark beside each is the gesture it performs.
Automatic fixes name themselves by what they change rather than by how much they disturb: relettering for on-screen text, prop_swap for an object, revoice for a spoken line. The first two land through the picker's own two patch paths (a relight propagated into every live frame when the finding's box can be located, a freeze over the span when it cannot), and revoice replaces the audio span with TTS. plan() chooses among them by the observation's dimension, and it is a pure function: no model call.
bridge is never chosen automatically. It regenerates pixels and costs real money, so it only ever runs because an operator picked it (or clicked a data link on a Grafana panel) and the day's budget allowed it. The budget is €45/day, system-wide, which buys somewhere between 12 and 23 bridges.
One shot, one edit. Before touching a pixel, the Remediator sweeps every other open finding this market holds on the same shot into one combined instruction. Fixing them one at a time regenerates the same seconds repeatedly and pays for each, and the second fix would undo the first.
flowchart LR
G[("<b>Grafana</b><br/>alert rule<br/>every 30s")] -- "customs_blocking ≥ 70<br/>{asset, market, rule_id}" --> WH["POST<br/>/webhook/alert"]
WH --> REM["<b>Remediator</b><br/>plan · price<br/>edit the span"]
REM --> S["staged file"]
S --> C{"craft gate<br/>length · resolution<br/>audio · drift"}
C -- "fails" --> X["discarded<br/>master untouched<br/>finding back to open"]
C -- "passes" --> M["new master"]
M --> V{"<b>Verifier</b><br/>re-runs the<br/>REAL analyst"}
V -- "violation survived<br/>or something new broke" --> X
V -- "gone, and nothing<br/>else broke" --> R["<b>resolved</b><br/>Grafana clears<br/>its own alert"]
R -.-> G
style G fill:#fff4e5,stroke:#F46800,color:#17222c
style C fill:#fef7e0,stroke:#FBBC05,color:#17222c
style V fill:#fef7e0,stroke:#FBBC05,color:#17222c
style X fill:#fce8e6,stroke:#EA4335,color:#17222c
style R fill:#e6f4ea,stroke:#34A853,color:#17222c
The Verifier does not inspect the edit and does not ask the model "did that work?". It re-runs the real analyst pass over the changed shots and asks the same instrument that found the problem whether it still sees it. Then it answers the second half of the question (did anything new break?) because an edit that removes a bottle can also remove the finding next to it, or introduce one.
When a rule is written on a protected characteristic, the honest answer is not an edit.
guard.apply is a pure function that reads exactly two things: the pack rule matched by rule_id, and the finding's own class. It never reads the rationale, the severity, or any other model-authored field. It never calls a model. Which means it is un-promptable: a crafted finding cannot argue its way past it.
Two rules in the corpus carry protected_basis: true. They are AE-LGBT-01 and SA-LGBT-01. A finding matching either gets remediation_blocked and the verbatim reason "rule basis targets a protected characteristic; human decision required", and the console shows the statute alongside a human decision.
The refusal is enforced a second time at the point of action: remediate._refuse_if_blocked raises before a frame is touched.
Tip
Guardrails belong in rule layers, not prompts. Anything a prompt grants, a prompt can take away.
A market is a YAML file, not code.
GLOBAL ── the baseline nobody escapes
└─ EU ── continental: AVMSD and friends
└─ FR, DE, BE … ── national statute
└─ RTL, TF1, VRT … ── broadcaster codes, stricter than the law
21 pack files, 128 authored rules, resolving to 98 selectable jurisdictions (1 global, 1 continental, 16 national, 80 channel) and 1,063 market-rule pairings. Every rule names its basis in prose and carries a citation. The class split is 94 legal / 25 policy / 9 offence, and class matters: an offence-class finding never blocks a market on its own, and never triggers an automatic edit.
Adding a market
Drop a YAML file in markets/. Declare a parent to inherit everything above you on the ladder, and add only what your jurisdiction says differently. Rules are matched to observations by dimension, which must be one of the 18 in markets/_taxonomy.yaml.
market: PT
name: Portugal
parent: EU
regulators: [ERC, ASAE]
pre_clearance: none
rules:
- id: PT-ALC-01
dimension: alcohol_tobacco_drugs
klass: legal
severity: 70
basis: >
Decreto-Lei 106/2015 restricts alcohol advertising on television
between 07:00 and 22:30.
remedy: Replace the alcoholic drink with a non-alcoholic one.# 1. Environment
python3.12 -m venv .venv && .venv/bin/pip install -r requirements.txt
cp .env.example .env # then fill in the Google Cloud + Grafana values
# 2. Provision the Grafana surface: dashboards, all three alert rules,
# the webhook contact point, the share links. Idempotent.
.venv/bin/python scripts/provision_grafana.py
# 3. Clear an ad from the terminal
.venv/bin/python scripts/run_pipeline.py docs/samples/test_ad.mp4 --markets FR,EU,AE
# 4. Or run the console and use a browser
.venv/bin/uvicorn customs.app:app --reload
# http://127.0.0.1:8000Needs ffmpeg on PATH, a Google Cloud project with Vertex AI enabled, and a Grafana Cloud stack. scripts/deploy.sh ships it to Cloud Run; scripts/deploy_viewer.sh ships the embeddable Grafana viewer beside it.
docs/samples/test_ad.mp4 was generated with Veo and deliberately loaded with seven documented landmines plus a clean control shot, so Google's own tools made the ad, and then failed it.
.venv/bin/python -m pytest -q
# 624 passed, 8 deselected624 offline tests across 24 files, no network, no API keys, no Grafana. The eight deselected ones are live-API probes you opt into explicitly.
The suite exists because most of this system's failure modes are silent: a guard that stops refusing, a citation that stops resolving, a craft gate that accepts a master that lost its soundtrack, a green can appearing in a modesty fix because a default leaked across dimensions. Every one of those has a test, and several of them have a test because it happened.
Warning
This is a hackathon build. The following are known, deliberate, and load-bearing to say out loud.
- The ladder is wired, and one rung of it is thin. Every pack now declares its parent, so a German commercial is judged against German law, the AVMSD and the global baseline, and the 80 channel nodes inherit through their country (this was the honest limit here until 2026-09-07: fifteen national packs declared no parent and inherited nothing). What remains thin is the channel rung itself: three Belgian broadcasters have pack files of their own and the other 77 are names that inherit their country and add nothing.
- The corpus is deliberately small. The archive shows the four commercials this project generated with Veo, because they are the ones it holds the rights to. Everything borrowed during development is hidden rather than deleted (
config.WITHHELD_ASSETS, twenty-four films): the rows, frames and log lines are all still there, every read path refuses them, and emptying that tuple brings them back. - Omni refuses third-party IP. Gemini Omni declines to edit footage containing recognisable third-party content, which is one of the reasons the corpus above is our own. The refusal is quoted verbatim in the mission feed and nothing is charged. Patch methods remain the path for that footage.
- Veo has a celebrity filter. A bridge over a shot it reads as depicting a public figure is refused with support code 15236754. Never charged, and a retry cannot help: the footage is the refusal.
- The Omni model id is a deliberately old alias.
gemini-omni-flash-previewdeprecates 2026-09-30; the newer1.1preview is access-gated behind a quota error that granting quota does not clear. A preflight asks the model whether it still exists on the way up, so when that date passes the picker greys the method out with a sentence instead of offering a button that can only fail;OMNI_MODELin the environment re-probes on the next cold start. - Image-generation quota is 2/min on this project pending a support case, which is why
per_frameis slow. - Grafana Cloud cannot be framed, so live panels come from the self-hosted viewer described above. Annotation markers are blank in the framed panels: they live in the Cloud instance's own database, not in Loki.
- A single Cloud Run instance. SQLite plus one writer thread is the concurrency model. It survives the parallel fan-out because every stage opens its own connection and only the guard writes findings.
src/customs/
crew.py the ADK SequentialAgent: ingest → analyst → adjudicators → guard → publisher
analyst.py one Gemini vision call per shot, 18-dimension taxonomy, no verdicts
adjudicate.py the join: observation × rule × grounded citation, severity decided in code
guard.py un-promptable refusal, reads rule metadata only
remediate.py six methods, priced, group-aware, guarded twice
verify.py re-runs the real analyst on the changed shots, rules on bystanders
media.py ffmpeg: shots, flashes, spans, craft gate, thumbnails, previews
grafana_ops.py MCP first, REST where mcp-grafana 1.1.0 has no write tool
telemetry.py Mimir over OTLP on the film's own clock, Loki lines, annotations
costs.py what each method costs before you press
app.py FastAPI console: SSE, 21 templates, no build step
markets/ 21 packs → 98 jurisdictions → 128 rules
grafana/dashboards/ 9 dashboards, provisioned as JSON
grafana-viewer/ stock grafana-oss that is allowed to be framed
tests/ 24 files, 624 tests
Built with: Python · FastAPI · Google Cloud Agent Builder (ADK) · Gemini (vision, text, TTS, image) · Veo 3.1 · Gemini Omni · Grafana Cloud (Mimir, Loki, MCP) · ffmpeg · SQLite · Cloud Run
Grafana is upstream of the work, not a report produced afterwards.






























