Skip to content

Commit f46f60c

Browse files
docs: complete Phase 0 production truth audit
1 parent fb51a59 commit f46f60c

1 file changed

Lines changed: 369 additions & 0 deletions

File tree

Lines changed: 369 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,369 @@
1+
# Phase 0 — Production Truth Audit
2+
3+
Status: **AUDIT COMPLETE**
4+
5+
This audit establishes what the current production bot can actually prove today. It does **not** declare the current product North-Star complete or trustworthy. No runtime code, credentials, production workflow behavior, source configuration, or public posting behavior was changed as part of this audit.
6+
7+
## Audit baseline
8+
9+
Production code audited against `main` commit:
10+
11+
- `f7245a57b5e3f00173576eb60af680ab6e30edbd`
12+
- commit title: `feat: Structured Production Outcome Contract (#47)`
13+
14+
Live evidence was also checked against GitHub Actions run `33904826788` (run #3301), which executed the production monitor on this same commit and uploaded a `production-outcome` artifact.
15+
16+
## Executive verdict
17+
18+
The current bot has a number of strong safety foundations, especially configured-source authority, bounded timeline completeness checks, delivery resume/idempotency, and media delivery deduplication.
19+
20+
However, it does **not** yet satisfy the new product North Star.
21+
22+
The most important result of Phase 0 is:
23+
24+
> A successful/HEALTHY production run is not currently proof that every configured source was completely observed, and the current pipeline can still silently discard real source posts before durable persistence.
25+
26+
Architecture work may proceed only with these gaps treated as explicit migration requirements.
27+
28+
## Current source inventory
29+
30+
`config/sources.json` currently contains:
31+
32+
- 24 configured X sources;
33+
- 23 enabled sources;
34+
- 1 disabled source (`flamehanie`, recorded as suspended on X);
35+
- 11 enabled `full_feed` sources;
36+
- 12 enabled `keyword_filter` sources;
37+
- `include_replies=true` for all configured sources.
38+
39+
The configured list is authoritative for normal non-Fanfic X retrieval.
40+
41+
## Actual current data flow
42+
43+
The effective main path is approximately:
44+
45+
```text
46+
configured X sources
47+
-> per-source X timeline retrieval
48+
-> optional author-scoped recovery search after timeline failure
49+
-> configured-author filter
50+
-> source-mode gate (full_feed / keyword_filter)
51+
-> post-ID dedupe
52+
-> global chronological ordering
53+
-> event/category organizer
54+
-> AI writer / translation-caption path
55+
-> theme renderer
56+
-> media preparation + Telegram delivery
57+
-> draft/review inbox
58+
-> seen/archive/state persistence
59+
```
60+
61+
This is not yet the desired target:
62+
63+
```text
64+
source retrieval
65+
-> persist every raw observation
66+
-> source ledger + completeness proof
67+
-> reversible relevance classification
68+
-> source-first editorial queue
69+
-> independent media/original/translation/caption artifacts
70+
```
71+
72+
## Source collection findings
73+
74+
### Configured-source authority — PASS
75+
76+
`source_authority_hardening.py` restricts normal non-Fanfic X collection, search, event recovery and delivery to configured authors. Author-scoped recovery does not widen the source set.
77+
78+
This is worth preserving.
79+
80+
### Timeline completeness boundary — PASS with bounded-limit caveat
81+
82+
`x_completeness.py` correctly refuses to call a bounded source window complete when the timeline result limit is reached before the collector crosses the requested lower time boundary. It raises `XCompletenessError` instead.
83+
84+
This is a strong existing invariant and should survive the migration.
85+
86+
Caveat: scheduled windows use a finite safety budget. A very high-volume source can therefore become explicitly PARTIAL/UNPROVEN. That is safe behavior, but the future ledger must expose the reason source-by-source.
87+
88+
### Replies — PASS for configured policy
89+
90+
All configured source rows request replies, and the completeness collector uses the tweets-and-replies timeline when `include_replies=true`.
91+
92+
### Retweets — PRODUCT/POLICY BLOCKER
93+
94+
The current completeness timeline explicitly skips retweets.
95+
96+
Therefore a current `COMPLETE` source timeline really means complete for the retained non-retweet policy, not necessarily complete for every activity visible on that account.
97+
98+
For the new `persist first, classify later` architecture, retweets should be observed/persisted and classified unless the admin explicitly defines retweets as out of scope. They must not disappear merely as an implementation side effect.
99+
100+
## Silent-drop findings
101+
102+
### Raw observation before filtering — BLOCKER
103+
104+
There is no durable canonical raw-observation layer before relevance/source-mode filtering.
105+
106+
`StateStore.archive` and the SQLite archive are useful retained-item archives, but they receive items only after earlier collection/filtering decisions. They cannot recover a source post that was already eliminated before queue/delivery.
107+
108+
### `keyword_filter` source mode — BLOCKER
109+
110+
Twelve enabled sources use `keyword_filter`.
111+
112+
The mode gate decides acceptance using `text + quoted_text` and configured Jeonghan terms/angel markers. It can therefore drop a true relevant source post when, for example:
113+
114+
- the post is media-only;
115+
- the caption is generic;
116+
- a thread continuation omits Jeonghan's name;
117+
- a new/unfamiliar nickname is used;
118+
- a relevant photo/video has weak textual signals.
119+
120+
Because the gate is applied before a canonical raw observation is durably persisted, these are true silent-drop paths under the new North Star.
121+
122+
### Existing `zero_silent_miss` instrumentation — PARTIAL
123+
124+
The current observability layer is valuable downstream. It tracks lifecycle status for returned/queued items and quarantines malformed pending rows rather than losing them.
125+
126+
But it observes counts around the configured/source-mode filter; it does not persist the actual objects rejected by that gate. Therefore its existence does not mean the new no-silent-drop requirement is satisfied.
127+
128+
## Dedupe findings
129+
130+
### Retained-item post-ID dedupe — PASS
131+
132+
Configured-author results are deduplicated deterministically by post ID, preferring the richer representation when duplicates are observed.
133+
134+
### Dedupe before raw persistence — MIGRATION GAP
135+
136+
For the target architecture, duplicate observations should still have retrieval/provenance evidence before canonical post-level collapse. The existing dedupe is appropriate for editorial candidates, but should not serve as the only observation record.
137+
138+
## Cursor and retry findings
139+
140+
### Scheduled cursor — BLOCKER
141+
142+
The current scheduled monitor uses one global `last_auto_run` cursor.
143+
144+
If one source is partial, the runtime correctly avoids advancing that global cursor. This is safer than losing content, but it means the cursor is not source-specific.
145+
146+
Consequences:
147+
148+
- one bad source pins progress for all sources;
149+
- retry is primarily global-window retry rather than a durable per-source retry contract;
150+
- production cannot independently answer `where is source A's cursor versus source B's cursor?`.
151+
152+
The target source ledger requires a cursor/outcome per configured source/window.
153+
154+
### Manual configured-source replay — useful but not equivalent
155+
156+
A configured-source complete-window fetch exists for manual 24h/source operations. This is useful infrastructure, but it is not the same as a persistent per-source scheduled cursor and retry ledger.
157+
158+
## Ordering and editorial UX findings
159+
160+
### Main presentation order — BLOCKER
161+
162+
`organize_updates()` groups updates into event/conversation groups and then sorts groups by `started_at`.
163+
164+
`PrivateReviewApplication.deliver_updates()` explicitly presents those groups oldest-to-newest.
165+
166+
Therefore the current product remains event/time-first. It does not finish source A before beginning source B.
167+
168+
### Review inbox — PARTIAL
169+
170+
The SQLite review inbox is draft-centric and supports pending/ready/rejected plus source/category metadata. It is a useful base.
171+
172+
It does not yet model the target source-level review session with:
173+
174+
- source progress;
175+
- COMPLETE/PARTIAL/UNPROVEN badge;
176+
- raw/relevant/uncertain/hidden counts;
177+
- source-specific retry/defer;
178+
- `Show hidden`;
179+
- source-first `Next` semantics.
180+
181+
## Relevance findings
182+
183+
### Current model — BLOCKER
184+
185+
The current source-mode gate is effectively destructive acceptance filtering for `keyword_filter` sources.
186+
187+
The target model must instead persist first and then assign a reversible state:
188+
189+
- `RELEVANT`
190+
- `UNCERTAIN`
191+
- `NOT_RELEVANT`
192+
193+
`NOT_RELEVANT` must remain countable, auditable and revealable.
194+
195+
## Original / translation / caption findings
196+
197+
### Original update representation — PASS foundation
198+
199+
`Update` preserves useful original fields including source text, quoted text, reply/conversation metadata and media references.
200+
201+
### Independent artifact lifecycle — PARTIAL
202+
203+
The archive schema already has separate `text`, `translated_text`, and `caption` columns, and translation-fusion code stores substantial evidence/fidelity metadata.
204+
205+
However, the production editorial lifecycle is still largely draft/event oriented. The target requires explicit independent per-post artifacts/states such as:
206+
207+
- original ready;
208+
- translation pending/ready/fallback;
209+
- caption pending/ready;
210+
- needs review.
211+
212+
A `Retranslate` action must not recollect media/source, and a `Rewrite` must not imply translation failure.
213+
214+
## Media findings
215+
216+
### Delivery reliability and exact-media dedupe — STRONG FOUNDATION
217+
218+
The current media layer already has useful production-grade behavior:
219+
220+
- Telegram cached-file fast path;
221+
- source-URL identity;
222+
- downloaded-byte SHA-256 identity;
223+
- Telegram `file_unique_id` identity;
224+
- persistent delivery receipts;
225+
- repeat suppression;
226+
- content fallback preparation;
227+
- text delivery can survive media unavailability.
228+
229+
These behaviors should be preserved.
230+
231+
### Asset-state contract — PARTIAL
232+
233+
The target still needs a first-class per-asset state/provenance/retry contract rather than relying mainly on delivery/cache behavior.
234+
235+
## Delivery and durability findings
236+
237+
### Telegram retry/resume — PASS foundation
238+
239+
The current private delivery path persists group draft plans before network delivery and checkpoints after acknowledged items. Pending delivery, seen state and message/media receipt mechanisms provide a strong base for idempotent resume.
240+
241+
### Durable archives — PARTIAL relative to North Star
242+
243+
The JSON and SQLite archives are valuable, but they are archives of retained pipeline items, not a guaranteed pre-classification observation ledger.
244+
245+
## Production health/outcome findings
246+
247+
### Workflow `success` is not collection truth — BLOCKER
248+
249+
The production GitHub Actions workflow can complete successfully when the Python process exits successfully. The structured Production Outcome was intended to provide a stronger truth contract, which is the right direction.
250+
251+
### Production Outcome source accounting — BLOCKER
252+
253+
`production_outcome_runtime.py` currently infers source completion by looping through enabled configured sources after the scheduled scan and treating a source as complete when its handle is not found in `collector.last_errors`.
254+
255+
That is not equivalent to consuming explicit per-source completion evidence.
256+
257+
It can also misrepresent an early-return/not-due run because source accounting is not tied to a durable source-attempt ledger.
258+
259+
### Live artifact proves the contract can report false confidence
260+
261+
The audited live production run `33904826788`:
262+
263+
- ran against the audited `main` SHA;
264+
- completed its live-monitor step successfully;
265+
- uploaded a `production-outcome` artifact;
266+
- artifact classified the run as `healthy`;
267+
- but the same artifact contained:
268+
- `configured_source_count = 0`;
269+
- `active_source_count = 0`;
270+
- `attempted_source_count = 0`;
271+
- `complete_source_count = 0`;
272+
- `collection_complete = false`;
273+
- `cursor_advanced = false`;
274+
- empty `cursor_reason`;
275+
- `useful_work_performed = true`.
276+
277+
Therefore `HEALTHY` is currently **not** evidence that source collection was complete. This is a direct live-production observation, not merely static-code inference.
278+
279+
### Discovery accounting — PARTIAL/BROKEN CONTRACT
280+
281+
The production outcome discovery hook calculates approximate queue-time values but does not currently wire those values into the outcome builder's discovery counters. Zero discovery fields can therefore mean `not measured`, not necessarily `nothing discovered`.
282+
283+
## Watchdog finding
284+
285+
The watchdog is correctly designed to prefer the structured outcome over workflow conclusion when it can retrieve and validate the artifact.
286+
287+
However, because the outcome itself does not yet contain reliable source truth, downstream recovery decisions cannot become trustworthy merely by consuming that artifact. Source ledger evidence has to become the upstream contract first.
288+
289+
The artifact download code should also remain under regression coverage because GitHub artifact downloads use redirects and are operationally distinct from ordinary GitHub JSON API calls.
290+
291+
## Gap matrix
292+
293+
| Area | Phase 0 verdict | Reason |
294+
|---|---|---|
295+
| Configured-source authority | PASS | external authors blocked from normal X pipeline |
296+
| Source inventory | PASS | explicit config exists |
297+
| Timeline lower-bound completeness | PASS | capped timeline fails closed |
298+
| Reply retrieval | PASS | all configured sources currently include replies |
299+
| Retweets | BLOCKER / policy gap | skipped before target persistence |
300+
| Raw observation persistence | BLOCKER | no pre-filter canonical store |
301+
| `keyword_filter` behavior | BLOCKER | real source posts can vanish before persistence |
302+
| Retained post-ID dedupe | PASS | deterministic and richer-copy preserving |
303+
| Global scheduled cursor safety | PASS as legacy safety | partial scan holds cursor |
304+
| Per-source cursor | BLOCKER | does not exist |
305+
| Per-source retry ledger | BLOCKER/PARTIAL | no durable scheduled source ledger |
306+
| Source-first ordering | BLOCKER | event/time-first presentation |
307+
| Reversible relevance states | BLOCKER | destructive filtering instead |
308+
| JSON/SQLite archive | PARTIAL | useful, but too late in pipeline |
309+
| Media delivery/dedupe | STRONG FOUNDATION | robust delivery identities and resume behavior |
310+
| Media asset lifecycle | PARTIAL | delivery-centric, not full asset-state contract |
311+
| Original representation | PASS foundation | Update preserves useful source context |
312+
| Translation/caption separation | PARTIAL | data exists, lifecycle not fully independent |
313+
| Review inbox | PARTIAL | draft-centric, not source-ledger UX |
314+
| Delivery durability/idempotency | PASS foundation | persistent drafts/queue/receipts |
315+
| Production Outcome source truth | BLOCKER | current `HEALTHY` is not completeness proof |
316+
| Discovery outcome counters | PARTIAL/BROKEN | not fully wired |
317+
| Workflow success as health | NOT ACCEPTABLE AS PROOF | process success != source completeness |
318+
319+
## False assumptions Phase 0 explicitly rejects
320+
321+
1. `GitHub Actions success` does not mean every source was complete.
322+
2. `HEALTHY` in the current outcome does not mean source collection was complete.
323+
3. A module named `zero_silent_miss` does not mean no source post can be silently lost before persistence.
324+
4. Having an archive does not mean there is a raw-observation store.
325+
5. Having timeline completeness logic does not mean there is a per-source cursor.
326+
6. Having a review inbox does not mean the UX is source-first.
327+
7. Having translation metadata does not yet mean original/translation/caption are independent editorial artifacts.
328+
329+
## Migration dependency map
330+
331+
The safe dependency order after this audit is:
332+
333+
```text
334+
raw observation contract/store
335+
-> source/window ledger
336+
-> per-source completeness + cursor rules
337+
-> reversible relevance classification
338+
-> source-first queue
339+
-> independent media/translation/caption artifacts
340+
-> personalized voice/theme layers
341+
```
342+
343+
Do **not** begin with a large Rust rewrite.
344+
345+
Rust should enter only after the first versioned observation/source-ledger contracts are explicit enough to test across the Python boundary.
346+
347+
## Phase 1 entry requirements
348+
349+
Phase 1 should create `RawObservation v1` and a shadow persistence path without replacing legacy delivery yet.
350+
351+
Minimum requirements:
352+
353+
- every observed configured-source post is persisted before `full_feed/keyword_filter` relevance decisions;
354+
- source handle/source ID and provider are explicit;
355+
- retrieval attempt ID and requested window are explicit;
356+
- post ID, timestamps, original text, quote/reply context and media references are preserved;
357+
- duplicate observations retain provenance while canonical post identity remains deterministic;
358+
- retweet behavior becomes an explicit product policy/state rather than an invisible hard skip;
359+
- failed/partial source observations remain tied to the source attempt;
360+
- legacy delivery remains behind the existing compatible path while shadow data is compared;
361+
- no public auto-publishing is introduced.
362+
363+
## Phase 0 exit gate
364+
365+
**Met.**
366+
367+
We can now answer, with code and live-production evidence, where the existing system is strong and where it cannot prove the new product invariants.
368+
369+
Phase 1 may begin, but only as an incremental raw-observation/shadow-storage change. The production bot should not yet be described as source-complete, no-silent-drop, or source-first.

0 commit comments

Comments
 (0)