pubkit 0.1.1__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. pubkit-0.3.0/CHANGELOG.md +138 -0
  2. {pubkit-0.1.1 → pubkit-0.3.0}/PKG-INFO +72 -9
  3. {pubkit-0.1.1 → pubkit-0.3.0}/README.md +71 -8
  4. {pubkit-0.1.1 → pubkit-0.3.0}/docs/FAILURE-MODES.md +69 -0
  5. {pubkit-0.1.1 → pubkit-0.3.0}/pyproject.toml +1 -1
  6. {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/__init__.py +1 -1
  7. {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/adapters/medium.py +1 -1
  8. pubkit-0.3.0/src/pubkit/browserctl.py +490 -0
  9. {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/cli.py +89 -10
  10. pubkit-0.3.0/src/pubkit/core/assets.py +96 -0
  11. {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/core/browser.py +1 -1
  12. pubkit-0.3.0/src/pubkit/core/login.py +430 -0
  13. {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/core/runner.py +7 -0
  14. pubkit-0.3.0/src/pubkit/render/tables.py +199 -0
  15. pubkit-0.3.0/tests/fixtures/fake_editor.html +119 -0
  16. pubkit-0.3.0/tests/fixtures/login_server.py +153 -0
  17. pubkit-0.3.0/tests/test_browser.py +182 -0
  18. {pubkit-0.1.1 → pubkit-0.3.0}/tests/test_core.py +113 -0
  19. pubkit-0.3.0/tests/test_login.py +412 -0
  20. pubkit-0.1.1/CHANGELOG.md +0 -62
  21. pubkit-0.1.1/src/pubkit/browserctl.py +0 -78
  22. {pubkit-0.1.1 → pubkit-0.3.0}/.gitignore +0 -0
  23. {pubkit-0.1.1 → pubkit-0.3.0}/LICENSE +0 -0
  24. {pubkit-0.1.1 → pubkit-0.3.0}/NOTICE +0 -0
  25. {pubkit-0.1.1 → pubkit-0.3.0}/docs/ADAPTERS.md +0 -0
  26. {pubkit-0.1.1 → pubkit-0.3.0}/docs/ARCHITECTURE.md +0 -0
  27. {pubkit-0.1.1 → pubkit-0.3.0}/docs/QUICKSTART.md +0 -0
  28. {pubkit-0.1.1 → pubkit-0.3.0}/examples/inside-ai-infra/img/part1-01-model-sizes.png +0 -0
  29. {pubkit-0.1.1 → pubkit-0.3.0}/examples/inside-ai-infra/img/part1-02-cpu-vs-gpu-ANIMATED.gif +0 -0
  30. {pubkit-0.1.1 → pubkit-0.3.0}/examples/inside-ai-infra/img/part1-03-starving-cores-ANIMATED.gif +0 -0
  31. {pubkit-0.1.1 → pubkit-0.3.0}/examples/inside-ai-infra/img/part1-04-gpu-comparison.png +0 -0
  32. {pubkit-0.1.1 → pubkit-0.3.0}/examples/inside-ai-infra/img/part2-01-one-token-one-read-ANIMATED.gif +0 -0
  33. {pubkit-0.1.1 → pubkit-0.3.0}/examples/inside-ai-infra/img/part2-02-decode-ceilings.png +0 -0
  34. {pubkit-0.1.1 → pubkit-0.3.0}/examples/inside-ai-infra/img/part2-03-prefill-vs-decode.png +0 -0
  35. {pubkit-0.1.1 → pubkit-0.3.0}/examples/inside-ai-infra/img/part2-04-kv-cache-cost.png +0 -0
  36. {pubkit-0.1.1 → pubkit-0.3.0}/examples/inside-ai-infra/img/part2-05-batching-ANIMATED.gif +0 -0
  37. {pubkit-0.1.1 → pubkit-0.3.0}/examples/inside-ai-infra/img/part2-06-vram-budget.png +0 -0
  38. {pubkit-0.1.1 → pubkit-0.3.0}/examples/inside-ai-infra/img/part2-07-throughput-latency.png +0 -0
  39. {pubkit-0.1.1 → pubkit-0.3.0}/examples/inside-ai-infra/img/part3-01-interconnect.png +0 -0
  40. {pubkit-0.1.1 → pubkit-0.3.0}/examples/inside-ai-infra/img/part3-02-llmd-request-path.png +0 -0
  41. {pubkit-0.1.1 → pubkit-0.3.0}/examples/inside-ai-infra/img/part3-03-llmd-objects.png +0 -0
  42. {pubkit-0.1.1 → pubkit-0.3.0}/examples/inside-ai-infra/img/part3-04-slos.png +0 -0
  43. {pubkit-0.1.1 → pubkit-0.3.0}/examples/inside-ai-infra/part-1.md +0 -0
  44. {pubkit-0.1.1 → pubkit-0.3.0}/examples/inside-ai-infra/part-2.md +0 -0
  45. {pubkit-0.1.1 → pubkit-0.3.0}/examples/inside-ai-infra/part-3.md +0 -0
  46. {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/__main__.py +0 -0
  47. {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/adapters/__init__.py +0 -0
  48. {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/adapters/api_base.py +0 -0
  49. {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/adapters/devto.py +0 -0
  50. {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/adapters/substack.py +0 -0
  51. {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/adapters/x.py +0 -0
  52. {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/core/__init__.py +0 -0
  53. {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/core/adapter.py +0 -0
  54. {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/core/anchors.py +0 -0
  55. {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/core/auth.py +0 -0
  56. {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/core/capabilities.py +0 -0
  57. {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/core/checks.py +0 -0
  58. {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/core/ir.py +0 -0
  59. {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/core/loader.py +0 -0
  60. {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/core/state.py +0 -0
  61. {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/core/transport.py +0 -0
  62. {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/py.typed +0 -0
  63. {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/registry.py +0 -0
  64. {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/render/__init__.py +0 -0
  65. {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/render/html.py +0 -0
  66. {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/scaffold.py +0 -0
  67. {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/workflows/__init__.py +0 -0
  68. {pubkit-0.1.1 → pubkit-0.3.0}/src/pubkit/workflows/airflow.py +0 -0
@@ -0,0 +1,138 @@
1
+ # Changelog
2
+
3
+ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
4
+ Versioning is [semantic](https://semver.org/); adapters may change behaviour on
5
+ a minor bump when a platform changes underneath them.
6
+
7
+ ## [0.3.0] — 2026-09-11
8
+
9
+ Sign-in is the one step a human performs and a machine has to judge, and every
10
+ way it fails is silent. This release makes each of them say something.
11
+
12
+ ### Added
13
+
14
+ - **`login_preflight()`** — playwright, browser, host reachability, a writable
15
+ session store and any existing session are checked before a window opens.
16
+ A login that cannot work no longer costs five minutes to find out.
17
+ - **Live observation during sign-in** — console errors, failed requests and
18
+ challenge-host scripts are recorded while the human works, and submissions to
19
+ the platform are counted. A button that never fires is now distinguishable
20
+ from a bot check that never completes; previously both were silence.
21
+ - **Post-flight proof** — the saved state is checked for cookies on the
22
+ platform's own domain and for the cookie names that authorise writing, its
23
+ expiry is reported, and a signed-in page is fetched and checked. **A session
24
+ is written only after it has been shown to work.**
25
+ - **`pubkit auth login <platform> --attach`** — watch a Chrome you started
26
+ yourself (`--remote-debugging-port=9222`) instead of launching one, for
27
+ platforms that will not complete a sign-in in a fresh profile.
28
+ - **`pubkit auth login --minutes`** to set the wait.
29
+ - **`core/login.py`** — one `LoginFlow` per platform declaring success markers,
30
+ required cookies, cookie domain, probe URL and signed-in markers. A new
31
+ platform inherits the whole validation suite rather than new special cases.
32
+ - **`tests/fixtures/login_server.py`** — a sign-in page that fails the six ways
33
+ real ones do: dead button, blocked challenge, partial cookies, short-lived
34
+ session, ghost session, and the happy path. 26 tests cover the matrix; the
35
+ unit half needs no browser at all.
36
+ - **Class E in `docs/FAILURE-MODES.md`** — six new entries.
37
+
38
+ ### Changed
39
+
40
+ - A launched login window now uses a **persistent pubkit profile** and real
41
+ Google Chrome where it is installed, falling back to bundled Chromium.
42
+ pubkit does not try to disguise an automated browser; where a platform
43
+ declines one, `--attach` uses the browser you actually sign in with.
44
+ - `pubkit auth verify` runs the identical post-flight checks as `login`, so a
45
+ session cannot pass one and fail the other, and prints each finding.
46
+ - `BROWSER_PLATFORMS` derives from the login flow table — one source of truth.
47
+
48
+ ## [0.2.0] — 2026-09-11
49
+
50
+ The release that makes `pubkit publish --to medium --confirm` actually work
51
+ unattended. 0.1.x could plan a Medium publish; it could not perform one.
52
+
53
+ ### Added
54
+
55
+ - **Browser lifecycle** (`browserctl.BrowserPool`, `attached()`). This was the
56
+ missing seam: the runner called `adapter.authenticate()` on a `MediumAdapter`
57
+ whose `_page` was still `None`. Now one browser serves the whole run, one
58
+ context per platform so a Medium session is never presented to Substack, and
59
+ sessions are refreshed on the way out because cookies rotate. API-only runs
60
+ never launch Chromium at all.
61
+ - **Table rendering** (`render.tables`, `core.assets.materialise`). The planner
62
+ said `table_to_image` and listed the asset ids; nothing produced the files.
63
+ Tables now render to quantised PNGs at 2x, cached on cell content so a re-run
64
+ neither re-renders nor re-uploads an unchanged table. The source IR is never
65
+ mutated — the same document keeps native tables on Dev.to in the same run.
66
+ - `pubkit auth verify <platform>` — check a session before a publish depends on
67
+ it, rather than discovering it expired halfway through.
68
+ - **Browser integration tests** against a deliberately hostile fixture editor
69
+ that reproduces multiple content roots, image stripping, figures landing
70
+ above the caret, and uploads that sit on a `blob:` URL before resolving.
71
+ They skip cleanly where Chromium is unavailable.
72
+
73
+ ### Fixed
74
+
75
+ - `marker_regex` was double-escaped, so every `fingerprint()` call threw
76
+ `SyntaxError: Invalid regular expression` inside the page. Verification was
77
+ therefore broken on every browser adapter. Found by the new integration
78
+ tests on their first run.
79
+ - `auth login` routed on a capability flag rather than the platform list, so
80
+ `pubkit auth login substack` took the API-token path and prompted for a token
81
+ that does not exist.
82
+
83
+ ## [0.1.1] — 2026-09-11
84
+
85
+ ### Added
86
+
87
+ - Docker image on the Playwright base (`ghcr.io/arunsingh/pubkit`), so browser
88
+ adapters work in CI without hand-installing Chromium's system libraries.
89
+ - `action.yml` — the repository is now a reusable GitHub Action.
90
+ - `CITATION.cff`, and a Homebrew formula template under `packaging/`.
91
+
92
+ ### Fixed
93
+
94
+ - `pubkit doctor` ignored `PLAYWRIGHT_BROWSERS_PATH` and reported Chromium as
95
+ missing on managed images (CI runners, devcontainers, the Playwright Docker
96
+ image) where it was in fact installed — sending people off to fix a problem
97
+ they did not have. Found by running the published package in exactly such an
98
+ environment.
99
+
100
+ ## [0.1.0] — 2026-09-11
101
+
102
+ First release. Extracted from a real, painful publishing run: a 15,000-word
103
+ illustrated three-part series to Medium that surfaced 21 distinct failure modes,
104
+ all catalogued in `docs/FAILURE-MODES.md`.
105
+
106
+ ### Added
107
+
108
+ - **Post IR** — a canonical, platform-agnostic document model with a
109
+ content-addressed id used as the idempotency key everywhere.
110
+ - **Capability negotiation** — adapters declare what they support; the planner
111
+ emits explicit, printable degradations. `pubkit plan` shows them before
112
+ anything is sent.
113
+ - **Check pipeline** — placeholders, numeric assertions, cross-references,
114
+ number drift, assets, budget, structure. Pluggable via `pubkit.checks`.
115
+ - **Verified chunked transport** — per-chunk rolling-hash verification, probed
116
+ size ceilings, single-member gzip.
117
+ - **Browser adapter toolkit** — paste-based document replace with a block-count
118
+ assertion, reload-then-fingerprint verification, caption-anchored `File`
119
+ image insertion, Unicode-folding anchor resolution, tag-chip post-conditions.
120
+ - **Adapters** — Medium, Substack (browser); X, Dev.to, Hashnode (API).
121
+ - **Runner** — six resumable steps per (document, platform), sqlite state store,
122
+ two-phase publish for series, hash-pinned `--confirm` gate, per-platform rate
123
+ limiting and jittered retry.
124
+ - **CLI** — `init`, `validate`, `plan`, `publish`, `status`, `platforms`,
125
+ `doctor`, `auth login|logout|list`.
126
+ - **Workflows** — GitHub Actions examples and Airflow operators.
127
+ - **Extension points** — `pubkit.adapters`, `pubkit.renderers`, `pubkit.checks`,
128
+ `pubkit.hooks`, plus per-platform transforms.
129
+
130
+ ### Known limitations
131
+
132
+ - Browser adapter selectors will need patching when a platform redesigns its
133
+ editor. They are isolated in one `EditorSelectors` dataclass per adapter
134
+ precisely so that is a small change.
135
+ - Table→image rendering requires the `render` extra; without it, tables degrade
136
+ to lists on platforms that lack table support.
137
+ - X media upload uses the v1.1 endpoint, which is what the v2 API still
138
+ requires.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pubkit
3
- Version: 0.1.1
3
+ Version: 0.3.0
4
4
  Summary: Publish one source to many platforms, safely. Medium, Substack, X, Dev.to, Hashnode.
5
5
  Project-URL: Homepage, https://github.com/arunsingh/pubkit
6
6
  Project-URL: Issues, https://github.com/arunsingh/pubkit/issues
@@ -109,9 +109,10 @@ pubkit validate content/series/
109
109
  # 2. See exactly what each platform will get — including every degradation.
110
110
  pubkit plan content/series/ --to medium,devto,x
111
111
 
112
- # 3. Sign in. pubkit never accepts a password.
113
- pubkit auth login medium # opens a window; you sign in; it keeps the session
112
+ # 3. Sign in, once. pubkit never accepts a password.
113
+ pubkit auth login medium # a window opens; you sign in; it keeps the session
114
114
  pubkit auth login devto # API token → OS keychain
115
+ pubkit auth verify medium # confirm before a publish depends on it
115
116
 
116
117
  # 4. Drafts everywhere. Safe to re-run; a dropped connection costs one step.
117
118
  pubkit publish content/series/ --to medium,devto,x
@@ -138,6 +139,64 @@ x: part-1 (bf9abfe78ce4)
138
139
 
139
140
  Nothing is a surprise at publish time. That is the whole design goal.
140
141
 
142
+ ## Sign in once, then it runs on its own
143
+
144
+ ```bash
145
+ pubkit auth login medium
146
+ ```
147
+
148
+ A real browser window opens at Medium's login page. You sign in — password
149
+ manager, MFA, device confirmation, whatever it asks for.
150
+
151
+ Before that window opens, pubkit checks the things that would otherwise cost you
152
+ five silent minutes: playwright, a browser, whether the login host is even
153
+ reachable from here, whether the session store is writable, and whether you
154
+ already have a working session. After you sign in, it checks the cookies are for
155
+ the platform's own domain, that the ones which authorise writing are present,
156
+ how long they last — and then fetches a page only a signed-in user can see.
157
+ **The session is saved only once it has been proven to work.** A saved session
158
+ that does not work is worse than none, because the failure moves into the middle
159
+ of a publish.
160
+
161
+ If the sign-in button does nothing at all — no error, no request, the single
162
+ most confusing failure there is — pubkit says so, and says which of the two
163
+ causes it was:
164
+
165
+ ```
166
+ ✗ timeout: no sign-in seen in 5 minutes
167
+ ! last URL: https://medium.com/m/signin
168
+ ✗ form submission: the page never sent a sign-in request
169
+ the button was not wired up, or a script it waits on never finished —
170
+ try: pubkit auth login medium --attach
171
+ ```
172
+
173
+ `--attach` watches a Chrome you started yourself rather than launching one:
174
+
175
+ ```bash
176
+ open -a "Google Chrome" --args --remote-debugging-port=9222
177
+ pubkit auth login medium --attach
178
+ ```
179
+
180
+ pubkit drives nothing about that sign-in; it watches a tab you are already in.
181
+ It does not try to look like something it is not, and it does not attempt to
182
+ defeat a bot check — where a platform declines an automated browser, `--attach`
183
+ gets out of the way instead.
184
+
185
+ From then on, unattended:
186
+
187
+ ```bash
188
+ pubkit publish content/ --to medium,devto,x --confirm
189
+ ```
190
+
191
+ One browser serves the whole run, one context per platform so a Medium session
192
+ is never presented to Substack, and sessions refresh on the way out because
193
+ cookies rotate. Publishing only to API platforms never launches Chromium at all.
194
+
195
+ pubkit sees cookies. It never sees, types or stores a password — which is both
196
+ the right security posture and the only thing that works, since platforms
197
+ increasingly gate login behind challenges an automation layer has no business
198
+ trying to defeat.
199
+
141
200
  ## Write once
142
201
 
143
202
  ````markdown
@@ -287,12 +346,16 @@ validate >> PubkitPublishOperator.expand_fanout(
287
346
 
288
347
  ## Status
289
348
 
290
- v0.1. The core, the checks, the planner and the state machine are tested (32
291
- tests) and exercised end-to-end against a real published series in
292
- `examples/inside-ai-infra/`. The browser adapters encode techniques verified by
293
- hand against live editors; selectors are the part most likely to need a patch
294
- when a platform ships a redesign, which is exactly why they are isolated in one
295
- dataclass per adapter.
349
+ v0.3. The core, the checks, the planner, the state machine, the browser pool,
350
+ the table renderer and the sign-in validator are covered by 68 tests, and the
351
+ whole pipeline is exercised end-to-end against a real published series in
352
+ `examples/inside-ai-infra/`. Two hostile fixtures carry most of the weight: an
353
+ editor that strips images, splits content roots and lands figures above the
354
+ caret, and a sign-in page that fails the six ways real ones do.
355
+
356
+ Selectors remain the part most likely to need a patch when a platform ships a
357
+ redesign, which is exactly why they are isolated in one dataclass per adapter —
358
+ and login flows in one `LoginFlow` per platform.
296
359
 
297
360
  ## Docs
298
361
 
@@ -59,9 +59,10 @@ pubkit validate content/series/
59
59
  # 2. See exactly what each platform will get — including every degradation.
60
60
  pubkit plan content/series/ --to medium,devto,x
61
61
 
62
- # 3. Sign in. pubkit never accepts a password.
63
- pubkit auth login medium # opens a window; you sign in; it keeps the session
62
+ # 3. Sign in, once. pubkit never accepts a password.
63
+ pubkit auth login medium # a window opens; you sign in; it keeps the session
64
64
  pubkit auth login devto # API token → OS keychain
65
+ pubkit auth verify medium # confirm before a publish depends on it
65
66
 
66
67
  # 4. Drafts everywhere. Safe to re-run; a dropped connection costs one step.
67
68
  pubkit publish content/series/ --to medium,devto,x
@@ -88,6 +89,64 @@ x: part-1 (bf9abfe78ce4)
88
89
 
89
90
  Nothing is a surprise at publish time. That is the whole design goal.
90
91
 
92
+ ## Sign in once, then it runs on its own
93
+
94
+ ```bash
95
+ pubkit auth login medium
96
+ ```
97
+
98
+ A real browser window opens at Medium's login page. You sign in — password
99
+ manager, MFA, device confirmation, whatever it asks for.
100
+
101
+ Before that window opens, pubkit checks the things that would otherwise cost you
102
+ five silent minutes: playwright, a browser, whether the login host is even
103
+ reachable from here, whether the session store is writable, and whether you
104
+ already have a working session. After you sign in, it checks the cookies are for
105
+ the platform's own domain, that the ones which authorise writing are present,
106
+ how long they last — and then fetches a page only a signed-in user can see.
107
+ **The session is saved only once it has been proven to work.** A saved session
108
+ that does not work is worse than none, because the failure moves into the middle
109
+ of a publish.
110
+
111
+ If the sign-in button does nothing at all — no error, no request, the single
112
+ most confusing failure there is — pubkit says so, and says which of the two
113
+ causes it was:
114
+
115
+ ```
116
+ ✗ timeout: no sign-in seen in 5 minutes
117
+ ! last URL: https://medium.com/m/signin
118
+ ✗ form submission: the page never sent a sign-in request
119
+ the button was not wired up, or a script it waits on never finished —
120
+ try: pubkit auth login medium --attach
121
+ ```
122
+
123
+ `--attach` watches a Chrome you started yourself rather than launching one:
124
+
125
+ ```bash
126
+ open -a "Google Chrome" --args --remote-debugging-port=9222
127
+ pubkit auth login medium --attach
128
+ ```
129
+
130
+ pubkit drives nothing about that sign-in; it watches a tab you are already in.
131
+ It does not try to look like something it is not, and it does not attempt to
132
+ defeat a bot check — where a platform declines an automated browser, `--attach`
133
+ gets out of the way instead.
134
+
135
+ From then on, unattended:
136
+
137
+ ```bash
138
+ pubkit publish content/ --to medium,devto,x --confirm
139
+ ```
140
+
141
+ One browser serves the whole run, one context per platform so a Medium session
142
+ is never presented to Substack, and sessions refresh on the way out because
143
+ cookies rotate. Publishing only to API platforms never launches Chromium at all.
144
+
145
+ pubkit sees cookies. It never sees, types or stores a password — which is both
146
+ the right security posture and the only thing that works, since platforms
147
+ increasingly gate login behind challenges an automation layer has no business
148
+ trying to defeat.
149
+
91
150
  ## Write once
92
151
 
93
152
  ````markdown
@@ -237,12 +296,16 @@ validate >> PubkitPublishOperator.expand_fanout(
237
296
 
238
297
  ## Status
239
298
 
240
- v0.1. The core, the checks, the planner and the state machine are tested (32
241
- tests) and exercised end-to-end against a real published series in
242
- `examples/inside-ai-infra/`. The browser adapters encode techniques verified by
243
- hand against live editors; selectors are the part most likely to need a patch
244
- when a platform ships a redesign, which is exactly why they are isolated in one
245
- dataclass per adapter.
299
+ v0.3. The core, the checks, the planner, the state machine, the browser pool,
300
+ the table renderer and the sign-in validator are covered by 68 tests, and the
301
+ whole pipeline is exercised end-to-end against a real published series in
302
+ `examples/inside-ai-infra/`. Two hostile fixtures carry most of the weight: an
303
+ editor that strips images, splits content roots and lands figures above the
304
+ caret, and a sign-in page that fails the six ways real ones do.
305
+
306
+ Selectors remain the part most likely to need a patch when a platform ships a
307
+ redesign, which is exactly why they are isolated in one dataclass per adapter —
308
+ and login flows in one `LoginFlow` per platform.
246
309
 
247
310
  ## Docs
248
311
 
@@ -220,3 +220,72 @@ Publishing one document to five platforms is a fan-out where any leg can fail.
220
220
  > **pubkit:** per-platform token-bucket limiter, bounded exponential backoff
221
221
  > with jitter, and per-leg result reporting. One platform failing never blocks
222
222
  > the others; the run exits non-zero with a machine-readable summary.
223
+
224
+ ---
225
+
226
+ ## Class E — Sign-in
227
+
228
+ A login is the only step a human performs and a machine has to judge, and every
229
+ failure in it is silent. These six were all observed against live platforms.
230
+
231
+ ### E1. The submit button does nothing at all
232
+ A sign-in page opens, the email is typed, Continue is clicked, and nothing
233
+ happens. No error, no navigation, no network request, nothing in the console.
234
+ It is the hardest login failure to report because from the outside there is
235
+ nothing to report.
236
+
237
+ > **pubkit:** the page is watched while the human works. Every POST/fetch to
238
+ > the platform is counted, so "you did not finish" and "the button never fired"
239
+ > stop being the same silence. Zero submissions after a timeout is reported as
240
+ > a failure in its own right, with `--attach` as the next step.
241
+
242
+ ### E2. A bot check that never completes
243
+ Identical symptom to E1, opposite cause: the click *is* handled, but the
244
+ handler waits on a challenge script that a freshly launched browser profile
245
+ never gets a token from. Indistinguishable by eye.
246
+
247
+ > **pubkit:** requests to known challenge hosts (reCAPTCHA, hCaptcha, Arkose,
248
+ > PerimeterX, DataDome, Turnstile) are recorded separately, and a failed one is
249
+ > named in the diagnosis. The remedy is `pubkit auth login <platform> --attach`,
250
+ > which watches a Chrome the user started themselves. pubkit does not attempt
251
+ > to defeat a challenge; it gets out of the way of one.
252
+
253
+ ### E3. Waiting five minutes for something that could never happen
254
+ playwright not installed, no browser downloaded, the login host unreachable
255
+ behind a VPN or proxy, a session directory that is not writable. Each produces
256
+ the same blank window and the same long wait.
257
+
258
+ > **pubkit:** `login_preflight()` runs first, synchronously and cheaply. A
259
+ > window is never opened when it cannot work, and each failure names its own
260
+ > fix. It also says when a valid session already exists, so the user is not
261
+ > signed out of something that was working.
262
+
263
+ ### E4. The flow stops at the identity provider
264
+ Sign-in goes through Google or a corporate SSO hop, the redirect back never
265
+ completes, and what gets saved is a session for `accounts.google.com` with no
266
+ cookie for the platform at all.
267
+
268
+ > **pubkit:** the saved state is checked for cookies on the platform's own
269
+ > registrable domain, and for the specific cookie names that authorise writing.
270
+ > Missing ones are named.
271
+
272
+ ### E5. A session that validates and does not work
273
+ Both session cookies present, correct domain, good expiry — and the signed-in
274
+ page still bounces back to sign-in. Cookie inspection cannot detect this; only
275
+ asking the platform can.
276
+
277
+ > **pubkit:** before anything is written, a page that renders only for a
278
+ > signed-in user is fetched and checked for a signed-in marker. A session is
279
+ > saved only once it has been *proven* to work. A saved session that does not
280
+ > work is worse than none, because it moves the failure into the middle of a
281
+ > publish.
282
+
283
+ ### E6. A session that expires quietly
284
+ A session cookie with no persistent expiry dies with the browser; one with a
285
+ two-day expiry dies mid-week. Either way the failure surfaces days later,
286
+ inside a publish, looking like something else.
287
+
288
+ > **pubkit:** expiry is reported at login time, and a lifetime under a week is
289
+ > a warning with the number in it. `pubkit auth verify` re-runs the identical
290
+ > checks on demand — same code path, so a session cannot pass one and fail the
291
+ > other.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pubkit"
7
- version = "0.1.1"
7
+ version = "0.3.0"
8
8
  description = "Publish one source to many platforms, safely. Medium, Substack, X, Dev.to, Hashnode."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -6,7 +6,7 @@ from .core.loader import load_document, load_series # noqa: F401
6
6
  from .core.runner import Pipeline, RunReport # noqa: F401
7
7
  from .registry import build_adapter, list_adapters, register # noqa: F401
8
8
 
9
- __version__ = "0.1.1"
9
+ __version__ = "0.3.0"
10
10
  __all__ = [
11
11
  "Document", "Series", "Asset", "Figure", "Table", "Heading", "Paragraph", "Code",
12
12
  "Pipeline", "RunReport", "load_document", "load_series",
@@ -68,7 +68,7 @@ class MediumAdapter(BrowserAdapter):
68
68
 
69
69
  login_url = "https://medium.com/m/signin"
70
70
  new_story_url = "https://medium.com/new-story"
71
- marker_regex = r"\\[\\[\\s*IMAGE"
71
+ marker_regex = r"\[\[\s*IMAGE"
72
72
 
73
73
  def __init__(self, page=None, upload=None) -> None:
74
74
  super().__init__()