walkthru 0.0.2__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 (50) hide show
  1. walkthru-0.0.2/.editorconfig +17 -0
  2. walkthru-0.0.2/.gitattributes +1 -0
  3. walkthru-0.0.2/.github/workflows/ci.yml +48 -0
  4. walkthru-0.0.2/.github/workflows/npm-ci-ts.yml +30 -0
  5. walkthru-0.0.2/.gitignore +124 -0
  6. walkthru-0.0.2/DECISIONS.md +255 -0
  7. walkthru-0.0.2/LICENSE +21 -0
  8. walkthru-0.0.2/PKG-INFO +74 -0
  9. walkthru-0.0.2/PLAN.md +284 -0
  10. walkthru-0.0.2/README.md +57 -0
  11. walkthru-0.0.2/misc/docs/WALKTHRU-AGENT-BRIEF.md +297 -0
  12. walkthru-0.0.2/misc/docs/demo2reel-01-Tooling-Capability-Survey-A-Play-Commands-and-Record-a-Demo-System.md +299 -0
  13. walkthru-0.0.2/misc/docs/demo2reel-02-The-Demo-Document-A-Data-Model-and-Open-Closed-Architecture-for-Editable-Demo-Tour-Artifacts.md +292 -0
  14. walkthru-0.0.2/pyproject.toml +98 -0
  15. walkthru-0.0.2/schema/demo-document.schema.json +1189 -0
  16. walkthru-0.0.2/schema/fixtures/full-demo.json +350 -0
  17. walkthru-0.0.2/schema/fixtures/minimal-demo.json +52 -0
  18. walkthru-0.0.2/tests/__init__.py +0 -0
  19. walkthru-0.0.2/tests/builders.py +229 -0
  20. walkthru-0.0.2/tests/fakes.py +114 -0
  21. walkthru-0.0.2/tests/test_engine.py +132 -0
  22. walkthru-0.0.2/tests/test_export.py +101 -0
  23. walkthru-0.0.2/tests/test_firewall.py +41 -0
  24. walkthru-0.0.2/tests/test_reelee_target.py +256 -0
  25. walkthru-0.0.2/tests/test_schema.py +81 -0
  26. walkthru-0.0.2/tests/test_timeline.py +118 -0
  27. walkthru-0.0.2/tests/test_walkthru.py +13 -0
  28. walkthru-0.0.2/ts/package-lock.json +2514 -0
  29. walkthru-0.0.2/ts/package.json +65 -0
  30. walkthru-0.0.2/ts/scripts/codegen.mjs +94 -0
  31. walkthru-0.0.2/ts/src/index.test.ts +12 -0
  32. walkthru-0.0.2/ts/src/index.ts +4 -0
  33. walkthru-0.0.2/ts/src/schema.codegen.test.ts +28 -0
  34. walkthru-0.0.2/ts/src/schema.generated.ts +72 -0
  35. walkthru-0.0.2/ts/src/schema.roundtrip.test.ts +58 -0
  36. walkthru-0.0.2/ts/tsconfig.json +20 -0
  37. walkthru-0.0.2/walkthru/__init__.py +114 -0
  38. walkthru-0.0.2/walkthru/adapters/__init__.py +10 -0
  39. walkthru-0.0.2/walkthru/adapters/export/__init__.py +16 -0
  40. walkthru-0.0.2/walkthru/adapters/export/json_target.py +40 -0
  41. walkthru-0.0.2/walkthru/adapters/export/webvtt.py +38 -0
  42. walkthru-0.0.2/walkthru/core/__init__.py +116 -0
  43. walkthru-0.0.2/walkthru/core/engine.py +196 -0
  44. walkthru-0.0.2/walkthru/core/events.py +154 -0
  45. walkthru-0.0.2/walkthru/core/schema.py +334 -0
  46. walkthru-0.0.2/walkthru/core/timeline.py +177 -0
  47. walkthru-0.0.2/walkthru/ecosystem/__init__.py +12 -0
  48. walkthru-0.0.2/walkthru/ecosystem/reelee/__init__.py +37 -0
  49. walkthru-0.0.2/walkthru/ecosystem/reelee/render_target.py +433 -0
  50. walkthru-0.0.2/walkthru/ports/__init__.py +98 -0
@@ -0,0 +1,17 @@
1
+ root = true
2
+
3
+ [*]
4
+ charset = utf-8
5
+ end_of_line = lf
6
+ insert_final_newline = true
7
+ trim_trailing_whitespace = true
8
+
9
+ [*.{py,toml,yml,yaml}]
10
+ indent_style = space
11
+ indent_size = 4
12
+
13
+ [*.md]
14
+ trim_trailing_whitespace = false
15
+
16
+ [Makefile]
17
+ indent_style = tab
@@ -0,0 +1 @@
1
+ *.ipynb linguist-documentation
@@ -0,0 +1,48 @@
1
+ # wads CI — calls the reusable workflow hosted in i2mint/wads.
2
+ #
3
+ # All configuration comes from this repo's pyproject.toml [tool.wads.ci.*].
4
+ # To customize the workflow itself (rare), replace this file with the
5
+ # full inline template `wads/data/github_ci_uv.yml` from i2mint/wads.
6
+ #
7
+ # Pinning: `@master` floats with wads. If you need version stability for
8
+ # a release-sensitive repo, change `@master` to a wads tag (e.g. `@v0.1.81`).
9
+ # CI failure does not block a published release — it blocks the publish
10
+ # step itself — so floating master is generally safe.
11
+ #
12
+ # Permissions: GitHub validates that the caller grants AT LEAST the
13
+ # permissions any job in the called workflow requests — at workflow-parse
14
+ # time, not at run-time, even if the job would be skipped via `if:`.
15
+ # The reusable workflow needs:
16
+ # contents: write for the publish job's version-bump push-back
17
+ # and for the github-pages job's gh-pages branch push
18
+ # pages: write for the github-pages job's REST API Pages config
19
+ # Both default to `write` on org-account GITHUB_TOKEN and need to be
20
+ # granted explicitly on personal-account callers (where the default is
21
+ # read-only). No `id-token: write` needed — the publish-github-pages
22
+ # action uses peaceiris/actions-gh-pages (branch-based) + REST API,
23
+ # not the OIDC `actions/deploy-pages` flow.
24
+ name: Continuous Integration
25
+ on: [push, pull_request]
26
+ jobs:
27
+ ci:
28
+ uses: i2mint/wads/.github/workflows/uv-ci.yml@master
29
+ permissions:
30
+ contents: write
31
+ pages: write
32
+ # Explicit pass-through (not `secrets: inherit`) because `inherit` does
33
+ # not reliably propagate caller-repo secrets to a reusable workflow
34
+ # owned by a different account (verified empirically: caller in a
35
+ # personal account, called in i2mint org → `${{ secrets.PYPI_PASSWORD }}`
36
+ # resolved to empty inside the reusable workflow despite the secret
37
+ # being set on the caller repo). Listing each secret here makes the
38
+ # propagation unambiguous regardless of caller-vs-called ownership.
39
+ # Missing secrets on the caller resolve to empty strings, harmless for
40
+ # the optional ones; PYPI_PASSWORD must be set for the publish job.
41
+ secrets:
42
+ PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
43
+ OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
44
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
45
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
46
+ HUGGINGFACE_TOKEN: ${{ secrets.HUGGINGFACE_TOKEN }}
47
+ KAGGLE_USERNAME: ${{ secrets.KAGGLE_USERNAME }}
48
+ KAGGLE_KEY: ${{ secrets.KAGGLE_KEY }}
@@ -0,0 +1,30 @@
1
+ # wads NPM CI — calls the reusable workflow hosted in i2mint/wads.
2
+ #
3
+ # All configuration lives in ts/package.json under the "wads"
4
+ # key (wads.ci.*). This workflow only runs when the JS subdirectory changes,
5
+ # so it never collides with the Python ci.yml.
6
+ #
7
+ # Publishing to npm is OPT-IN: set wads.ci.publish.enabled=true in package.json
8
+ # and include the marker (default "[publish-npm]") in your commit message.
9
+ # Publishing uses OIDC trusted publishing by default (configure the trusted
10
+ # publisher for this repo + workflow filename on npmjs.com). NPM_TOKEN is only
11
+ # needed as a fallback (e.g. the very first publish of a new package name).
12
+ name: NPM CI (ts)
13
+ on:
14
+ push:
15
+ paths:
16
+ - "ts/**"
17
+ - ".github/workflows/npm-ci-ts.yml"
18
+ pull_request:
19
+ paths:
20
+ - "ts/**"
21
+ jobs:
22
+ npm-ci:
23
+ uses: i2mint/wads/.github/workflows/npm-ci.yml@master
24
+ permissions:
25
+ contents: read
26
+ id-token: write # npm provenance / OIDC trusted publishing
27
+ with:
28
+ package-dir: ts
29
+ secrets:
30
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -0,0 +1,124 @@
1
+ .claude/handoffs/
2
+ .claude/scratch/
3
+
4
+ # Node / TypeScript (ts/ frontend, generated by `wads --frontend ts`)
5
+ node_modules/
6
+ *.tsbuildinfo
7
+
8
+ # Byte-compiled / optimized / DLL files
9
+ __pycache__/
10
+ *.py[cod]
11
+ *$py.class
12
+
13
+
14
+ .DS_Store
15
+ # C extensions
16
+ *.so
17
+
18
+ # TLS certificates
19
+ ## Ignore all PEM files anywhere
20
+ *.pem
21
+ ## Also ignore any certs directory
22
+ certs/
23
+
24
+ # Distribution / packaging
25
+ .Python
26
+ build/
27
+ develop-eggs/
28
+ dist/
29
+ downloads/
30
+ eggs/
31
+ .eggs/
32
+ lib/
33
+ lib64/
34
+ parts/
35
+ sdist/
36
+ var/
37
+ wheels/
38
+ *.egg-info/
39
+ .installed.cfg
40
+ *.egg
41
+ MANIFEST
42
+ _build
43
+
44
+ # PyInstaller
45
+ # Usually these files are written by a python script from a template
46
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
47
+ *.manifest
48
+ *.spec
49
+
50
+ # Installer logs
51
+ pip-log.txt
52
+ pip-delete-this-directory.txt
53
+
54
+ # Unit test / coverage reports
55
+ htmlcov/
56
+ .tox/
57
+ .coverage
58
+ .coverage.*
59
+ .cache
60
+ nosetests.xml
61
+ coverage.xml
62
+ *.cover
63
+ .hypothesis/
64
+ .pytest_cache/
65
+
66
+ # Translations
67
+ *.mo
68
+ *.pot
69
+
70
+ # Django stuff:
71
+ *.log
72
+ local_settings.py
73
+ db.sqlite3
74
+
75
+ # Flask stuff:
76
+ instance/
77
+ .webassets-cache
78
+
79
+ # Scrapy stuff:
80
+ .scrapy
81
+
82
+ # Sphinx documentation
83
+ docs/_build/
84
+ docs/*
85
+
86
+ # PyBuilder
87
+ target/
88
+
89
+ # Jupyter Notebook
90
+ .ipynb_checkpoints
91
+
92
+ # pyenv
93
+ .python-version
94
+
95
+ # celery beat schedule file
96
+ celerybeat-schedule
97
+
98
+ # SageMath parsed files
99
+ *.sage.py
100
+
101
+ # Environments
102
+ .env
103
+ .venv
104
+ env/
105
+ venv/
106
+ ENV/
107
+ env.bak/
108
+ venv.bak/
109
+
110
+ # Spyder project settings
111
+ .spyderproject
112
+ .spyproject
113
+
114
+ # Rope project settings
115
+ .ropeproject
116
+
117
+ # mkdocs documentation
118
+ /site
119
+
120
+ # mypy
121
+ .mypy_cache/
122
+
123
+ # PyCharm
124
+ .idea
@@ -0,0 +1,255 @@
1
+ # walkthru — Decisions & Deviations
2
+
3
+ > Deliverable 2 of the build brief. Every place this project deviates from
4
+ > `misc/docs/WALKTHRU-AGENT-BRIEF.md` or either report, plus every genuinely open judgment
5
+ > call — with the reasoning and what in the code justified it. Candid, not deferential, per
6
+ > the brief's instruction. Granular follow-up lives in the repo's enhancement issues.
7
+
8
+ Legend: **[deviation]** departs from a stated brief/report conclusion · **[call]** an open
9
+ judgment the brief left to us · **[confirm]** inspection confirmed a brief assumption.
10
+
11
+ ---
12
+
13
+ ## D1. SSOT authored in Python (Pydantic), not Zod-first — **[deviation]**
14
+
15
+ **Brief said:** Zod is the presumptive SSOT; `zodal` is the presumptive schema layer; author
16
+ the Demo Document in TS-Zod / `zodal` and emit JSON Schema for the renderer.
17
+
18
+ **Decision:** Author the SSOT **once in Python Pydantic v2**; emit JSON Schema; **codegen Zod
19
+ v4** for the TS side.
20
+
21
+ **What justified it:**
22
+
23
+ - `reelee` defines its content schemas as **Pydantic** `BaseModel`s registered via
24
+ `lacing.register_body_schema`, exports JSON Schema (`reelee export-schemas`), and
25
+ **`reelee-web` codegens its Zod types from that export** (`schemas/` → `src/types/generated/`,
26
+ never hand-written). The federation's SSOT direction is **Python → JSON Schema → Zod**, the
27
+ opposite of the brief's bias.
28
+ - Matching this lets the Demo Document register as a `lacing` body schema and round-trip
29
+ losslessly with the renderer side, instead of fighting the grain of the ecosystem.
30
+ - `zodal` turned out to be **collection-centric** (`defineCollection`, `DataProvider`,
31
+ list/filter/sort) and TS-only — a poor fit for authoring a single nested document schema. It
32
+ is the right tool for *presenting a library of* Demo Documents/Steps as a collection, not for
33
+ defining the document itself.
34
+
35
+ **Cost / risk:** the *live* engine runs TS-side (capture taps acture in the browser), so TS
36
+ consumes codegened types rather than authoring them. This is exactly how `reelee-web` already
37
+ operates, so the toolchain exists. Ecosystem-independence is preserved: Pydantic is a
38
+ permissive third-party dep, not an ecosystem package; the `lacing` hook is an optional
39
+ `ecosystem/` adapter.
40
+
41
+ ---
42
+
43
+ ## D2. First `RenderTarget` is reelee's Ken Burns contract, not Remotion — **[deviation]**
44
+
45
+ **Brief/report said:** treat the hand-off as Remotion-style "input props"; Report 02 used
46
+ Remotion as a proxy because "Reelee could not be identified from public sources."
47
+
48
+ **Decision:** The first `RenderTarget` adapter targets **`reelee`'s actual contract** — a
49
+ `reelee.Project` graph projected to an ordered `list[PanelView]`, rendered by
50
+ `render_kenburns_video(project, out, ...)` (MoviePy/ffmpeg). Remotion becomes a *secondary,
51
+ external* adapter built only if a real need appears.
52
+
53
+ **What justified it:** `reelee` is a real, inspectable ecosystem package and is **not** a
54
+ React/props renderer. Its render input is `PanelView` records (`reelee/storyboard_export.py`) +
55
+ `render_kenburns_video` kwargs (`reelee/kenburns_video.py`). The brief itself instructed:
56
+ "inspect `reelee`'s actual input contract and write the first `RenderTarget` adapter against
57
+ it — the real contract supersedes the Remotion proxy; only the field-mapping adapter changes."
58
+ This decision follows that instruction.
59
+
60
+ **Unchanged:** the boundary principle (we own representation, the renderer owns pixels; hand off
61
+ validated JSON; the renderer may ignore what it doesn't understand) holds exactly as written.
62
+
63
+ **Resolved (was an open question): feed `PanelView`s, don't rebuild a `reelee.Project`.** Issue #3
64
+ left open whether the adapter constructs a `reelee.Project` graph or feeds panels directly.
65
+ `render_kenburns_video(project, …)` immediately calls `collect_panel_views(project)` — it
66
+ re-derives panels from an `nw`/`lacing`/falaw annotation + content-addressed-artifact graph.
67
+ Reconstructing that graph from a `DemoDocument` would bleed reelee's whole internal model across
68
+ the firewall, only for reelee to throw our panels away and rebuild its own. So
69
+ `walkthru.ecosystem.reelee` maps the resolved `Timeline` **directly** to `list[PanelView]` and
70
+ drives the film with the *same lower-level primitives* `render_kenburns_video` uses internally
71
+ (`burns.ken_burns_path` + an injectable `film_renderer`, default
72
+ `reelee.kenburns_video.default_film_renderer`). Per-panel screen time comes from the **walkthru
73
+ timeline** (the SSOT already composes it), not reelee's shot-timing strategies. The step-level
74
+ `poster` (AssetRef) added in this work is the panel image (`PanelView.image_path`).
75
+
76
+ ---
77
+
78
+ ## D3. The command layer is `acture`; reuse its record/replay primitives — **[confirm + call]**
79
+
80
+ **Brief said:** reuse the command layer's registry + middleware; find the cleanest capture
81
+ interception point.
82
+
83
+ **Finding:** the layer is **`acture`** (TS/React, Apache-2.0). It already ships, in
84
+ `acture/packages/e2e-playwright/src/sequence.ts`:
85
+
86
+ - `replaySequence(registry, sequence, {ctx, onStep, stopOnError})` — generative replay.
87
+ - `recordSequence(registry) → {steps, stop()}` — capture, by wrapping `registry.dispatch`.
88
+
89
+ **Decision/call:** `walkthru`'s `play()` is a cue/narration/camera-aware **superset of
90
+ `replaySequence`**, and its `ActionRecorder` is a Demo-Document-emitting **superset of
91
+ `recordSequence`**. We do **not** invent a new engine or a new extension paradigm — observers
92
+ wrap `registry.dispatch` exactly as `acture-telemetry`/`acture-devtools` already do. acture has
93
+ **no core middleware chain by design** (dispatch interception is an opt-in adapter concern);
94
+ `acture-walkthru` slots in as one more opt-in dispatch-observer package beside
95
+ `acture-e2e-playwright` and `acture-telemetry`.
96
+
97
+ **Implication:** much of the TS engine is *composition over acture*, not new code. This is the
98
+ single biggest scope reduction inspection produced.
99
+
100
+ ---
101
+
102
+ ## D4. Command atom mirrors acture's `SequenceStep {commandId, params}` — **[confirm]**
103
+
104
+ The brief's `command:{id, params}` maps to acture's macro step shape exactly:
105
+ `SequenceStep { commandId: string, params?: unknown }` (`sequence.ts`). The full command
106
+ *definition* is a richer `CommandRecord {id, title, params: ZodType, when, execute, …}`, but
107
+ the *serializable macro step* — what a Demo Document stores — is `{commandId, params}`. We adopt
108
+ `{ command: { id, params } }` in the Demo Document and bridge `id ↔ commandId` in the acture
109
+ adapter. acture constrains `params` to the JSON-Schema-representable Zod subset; our Pydantic
110
+ models honor the same subset so codegen stays faithful.
111
+
112
+ ---
113
+
114
+ ## D5. License = MIT — **[call]**
115
+
116
+ **Brief said:** pick a permissive license consistent with the ecosystem.
117
+
118
+ **Decision:** **MIT.** The Python house default across `dol`, `nw`, `lacing`, `reelee`, and
119
+ `zodal` is MIT. `acture` is the lone **Apache-2.0** package; since `acture-walkthru` builds on
120
+ acture, Apache-2.0 is the relevant *inbound* license, and MIT is outbound-compatible with it
121
+ (MIT code may depend on Apache-2.0). MIT keeps walkthru aligned with the Python orbit it
122
+ publishes into while remaining compatible with the acture tie.
123
+
124
+ ---
125
+
126
+ ## D6. Layout = wads current defaults (Python `name/name/` at root); frontend CI deferred — **[call]**
127
+
128
+ **Brief proposed:** `py/src/walkthru/…`. An earlier draft of this doc used a `py/` + `ts/`
129
+ split. Both are superseded.
130
+
131
+ **Decision:** Use **wads current defaults**. The Python package sits at the **repo root** in the
132
+ `name/name/` form — `walkthru/walkthru/` — exactly what `wads populate` produces (hatchling,
133
+ `requires-python >= 3.10`, MIT, ruff `D100`, `tests/`, `[tool.wads.ci.*]` SSOT, the 5-line
134
+ `.github/workflows/ci.yml` uv stub, plus the wads `.gitignore`/`.gitattributes`/`.editorconfig`).
135
+ The TypeScript side lives in a sibling **`ts/`** subdir as a **single** npm package
136
+ (`acture-walkthru`).
137
+
138
+ **What justified it (and the constraint):** the user maintains ~200 repos in the `name/name`
139
+ form and wants the Python convention left untouched. wads already models the frontend as a
140
+ *separate, additive* overlay (`profiles.py` / `npm_config.py` + a path-filtered workflow), so
141
+ supporting a TS frontend alongside the unchanged Python layout costs nothing. Confirmed by
142
+ reading wads: `populate` (Python) and `apply_npm_overlay` (frontend) are independent code paths.
143
+
144
+ **Frontend CI deferred — coordination, not indecision.** wads' frontend overlay is brand-new and
145
+ unused (zodal was set up independently/earlier), so its defaults can change freely. We filed
146
+ **i2mint/wads#39** to generalize the single npm overlay into a **js/ts language-profile
147
+ registry** (extensible, with a monorepo seam), to be built in a separate session. To avoid the
148
+ two sessions colliding, walkthru:
149
+
150
+ - ships the **Python side on wads current defaults now** (this commit), with a **Python-only**
151
+ `ci.yml`; and
152
+ - **defers `ts/` scaffolding + `npm-ci.yml`** until the wads `ts` profile lands, then generates
153
+ them via `wads --frontend ts`.
154
+
155
+ **Single TS package, not a monorepo (for MVP).** `acture`/`zodal` are pnpm+turbo monorepos, but
156
+ walkthru's MVP needs only one publishable package. The core/adapter firewall is enforced
157
+ *within* one package (subpath exports + an import-boundary lint such as dependency-cruiser +
158
+ optional peer deps) — separate packages would be premature. The wads single-package overlay
159
+ fits; we graduate to the `ts-monorepo` profile (reserved in #39) only if adapters ever need
160
+ independent versioning.
161
+
162
+ **Pre-release CI toggles.** Because there is no release artifact, generated docs, or metrics
163
+ history yet, `[tool.wads.ci].publish/docs/metrics` are set to `enabled = false` (testing +
164
+ ruff stay on so CI gives real signal). Flip `publish` on at the first release; `docs` on once
165
+ docs exist. This is a phase toggle, not a convention change.
166
+
167
+ ---
168
+
169
+ ## D7. Pydantic v2 over plain dataclasses for the Python SSOT — **[call]**
170
+
171
+ The brief left the Python mirror open (dataclasses / pydantic / generated). Chose **Pydantic
172
+ v2**: it gives validation *and* JSON-Schema emission in one step (the whole point of the SSOT),
173
+ and it is what the federation already uses, so `lacing` schema registration and the reelee
174
+ round-trip come for free. The `core` remains pure — schema validation is side-effect-free; only
175
+ `play()`'s injected observers/ports perform effects.
176
+
177
+ ---
178
+
179
+ ## D8. Anchor is the single SSOT for cue/narration → step association — **[call]**
180
+
181
+ **Brief listed** `cueRefs?` and `narrationRef?` on `CommandStep`, *and* anchors on the cue and
182
+ narration tracks — i.e. the association stored in two places.
183
+
184
+ **Decision:** Drop the step-side `cueRefs`/`narrationRef`. The **track item's `anchor`**
185
+ (`{stepId, localOffsetMs[, durationMs]}`) is the *only* place a cue/narration is tied to a step.
186
+ The engine resolves "what fires at this step" by filtering the tracks on `anchor.stepId`
187
+ (`_cues_for` / `_narration_for`).
188
+
189
+ **Why:** storing the same association on both the step and the track item is a dual-source-of-truth
190
+ that drifts on every edit (the user's SSOT principle, and Report 02's "wrong abstraction"
191
+ warning). The anchor is also strictly richer — it carries `localOffset` (and `duration` for
192
+ narration) that a bare id reference cannot — so it must exist regardless; the step-side ref is pure
193
+ redundancy. Finding a step's cues is an O(n) track scan, which is irrelevant at demo sizes. If a
194
+ real need for fast reverse lookup ever appears, it is a derived index, not new SSOT.
195
+
196
+ ---
197
+
198
+ ## D9. Lifecycle protocol realized as a typed event stream, not named hooks — **[deviation]**
199
+
200
+ **Brief/Report 02 §B.1** describe the lifecycle as an `Observer` object with named methods
201
+ (`onStepEnter`, `beforeCommand`, `onCueBegin`, …).
202
+
203
+ **Decision:** Realize it as a stream of **typed event dataclasses** (`StepEnter`, `BeforeCommand`,
204
+ `CueBegin`, …) consumed by observer **callables** (`Observer = Callable[[Event], Awaitable|None]`).
205
+ Each event type maps 1:1 to a brief hook name, so nothing is lost; the canonical walk is the async
206
+ generator `iter_events(document, executor)`, and `play()` is a thin driver that fans events to
207
+ observers.
208
+
209
+ **Why:** it matches Thor's functional-over-OOP convention and the iterables guidance (a lazy
210
+ `yield`-based stream rather than an object with 13 optional override points). Composition is
211
+ trivial (an observer is just a function; filtering by `isinstance` selects the events you care
212
+ about), and the same event vocabulary serves both `play` (generative) and `record` (capture) —
213
+ the "one lifecycle protocol, driver inverted" the brief wants. A named-hook adapter can be layered
214
+ on later for anyone who prefers it (progressive disclosure), but it is not the core mechanism.
215
+
216
+ **Async core — [call].** The engine and ports are `async` because the real executors (acture
217
+ dispatch, Playwright) and recorders do I/O. The pure-core tests need no async plugin — they use
218
+ `asyncio.run` over fakes. `executor` and observers may be sync *or* async; the engine awaits
219
+ whatever is awaitable (`_maybe_await`).
220
+
221
+ ---
222
+
223
+ ## D10. Zod codegen is a self-contained `ts/` build script, not reelee-web's toolchain — **[call]**
224
+
225
+ **Open question (was PLAN §9):** where the Zod codegen lives — a `schema/` build script vs.
226
+ reusing reelee-web's codegen toolchain.
227
+
228
+ **Decision:** a self-contained `ts/scripts/codegen.mjs` that consumes the committed
229
+ `schema/demo-document.schema.json` (the Pydantic-emitted artifact) and writes
230
+ `ts/src/schema.generated.ts`. It uses the **same tools** reelee-web uses — `json-schema-to-zod` +
231
+ zod v4 — but **not** reelee's `export-schemas` CLI or `lacing` body-schema registry.
232
+
233
+ **Why:** reelee-web's `scripts/codegen.mjs` is coupled to `python -m reelee export-schemas` and a
234
+ `lacing` `index.json` registry — machinery walkthru does not have, and whose adoption PLAN §9 still
235
+ defers (the `lacing` body-schema question). Reusing the *technique* without the *coupling* keeps
236
+ the TS side runnable with no Python toolchain and preserves the core/adapter firewall: TS depends
237
+ only on the JSON contract, never on `walkthru`/`reelee`/`lacing`.
238
+
239
+ **Two gotchas handled:** (1) `json-schema-to-zod` does **not** dereference `$ref` (CLI *or* API) —
240
+ it renders Pydantic's `$defs` refs as `z.any()`; we inline them first with
241
+ `@apidevtools/json-schema-ref-parser` (the Demo Document schema is a DAG, so full dereference is
242
+ safe). (2) Drift is guarded both sides: Python pins JSON Schema ↔ Pydantic
243
+ (`test_committed_json_schema_is_up_to_date`); TS pins the committed Zod ↔ JSON Schema via
244
+ `codegen.mjs --check` (run by `ts/src/schema.codegen.test.ts`). Discriminated unions arrive as
245
+ `oneOf` → json-schema-to-zod's "exactly one variant passes" `superRefine`, which is faithful
246
+ (closed union; unknown `type`/`kind` rejected — covered by the round-trip negatives).
247
+
248
+ ---
249
+
250
+ ## Open judgment calls deferred to issues (not yet decided)
251
+
252
+ - Whether `play()` needs a **Python mirror** for MVP or whether TS-only suffices until render.
253
+ - Whether to register the Demo Document as a **`lacing` body schema** in MVP or defer.
254
+
255
+ These are tracked as enhancement issues — the project's permanent development memory.
walkthru-0.0.2/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Thor Whalen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,74 @@
1
+ Metadata-Version: 2.4
2
+ Name: walkthru
3
+ Version: 0.0.2
4
+ Summary: Turn a sequence of app commands into an editable, re-renderable demo/tour artifact.
5
+ Project-URL: Homepage, https://github.com/thorwhalen/walkthru
6
+ License: MIT
7
+ License-File: LICENSE
8
+ Keywords: annotation,automation,demo,screencast,tour,walkthrough
9
+ Requires-Python: >=3.10
10
+ Requires-Dist: pydantic>=2
11
+ Provides-Extra: reelee
12
+ Requires-Dist: burns; extra == 'reelee'
13
+ Requires-Dist: mixing; extra == 'reelee'
14
+ Requires-Dist: reelee; extra == 'reelee'
15
+ Provides-Extra: testing
16
+ Description-Content-Type: text/markdown
17
+
18
+ # walkthru
19
+
20
+ Turn a sequence of application **commands** into an **editable, re-renderable demo/tour
21
+ artifact** — and play that sequence while observers record video, draw visual cues, and
22
+ narrate.
23
+
24
+ `walkthru` owns the **representation** (the *Demo Document*) and the **playback/capture
25
+ engine**. It does **not** render the final video — it hands a validated artifact off to a
26
+ renderer (the `reelee` ecosystem, Remotion, moviepy/ffmpeg, …). *Owning representation, not
27
+ pixels, is the load-bearing boundary of the whole design.*
28
+
29
+ ## Two modes, one data model, one engine
30
+
31
+ - **Generative** — an author supplies a Demo Document (commands + annotations); `walkthru`
32
+ plays it while recording, with optional cues (highlight, spotlight, hotspot, callout,
33
+ synthetic cursor), pauses, and narration.
34
+ - **Capture** — a human operates the app manually; `walkthru` records the video **and** the
35
+ underlying command stream **and** annotations, producing the *same* Demo Document.
36
+
37
+ The only difference between the modes is *who fills in the document*.
38
+
39
+ ## The core
40
+
41
+ ```
42
+ play(demoDoc, executor, observers) -> Outcome
43
+ ```
44
+
45
+ A pure higher-order function that walks the document and emits lifecycle events
46
+ (`onStepEnter` → `beforeCommand` → `afterCommand` → `onStepExit`, `onCueBegin/End`,
47
+ `onNarration`, …). It never records, renders, or speaks — every effect is an injected
48
+ observer or port.
49
+
50
+ ## Ecosystem-biased, ecosystem-independent
51
+
52
+ The core and ports depend on **nothing** from our ecosystem. Integration with
53
+ [`acture`](https://github.com/thorwhalen/acture) (the command layer),
54
+ [`reelee`](https://github.com/thorwhalen/reelee) (the renderer), and `zodal` ships as
55
+ **optional adapters**. You can `pip install walkthru` / `npm i acture-walkthru` and use the
56
+ core with your own adapters.
57
+
58
+ ## Packages
59
+
60
+ | Package | Registry | Role |
61
+ |---|---|---|
62
+ | `walkthru` | PyPI | Python core + schema SSOT + render hand-off |
63
+ | `acture-walkthru` | npm | TS core + the live capture/play engine over `acture` |
64
+
65
+ ## Status
66
+
67
+ 🚧 **Planning.** See [`PLAN.md`](./PLAN.md) for the implementation plan,
68
+ [`DECISIONS.md`](./DECISIONS.md) for design decisions and deviations from the brief, and the
69
+ repo's **enhancement issues** for the running development journal. Source documents live in
70
+ [`misc/docs/`](./misc/docs/).
71
+
72
+ ## License
73
+
74
+ MIT — see [`LICENSE`](./LICENSE).