UTS Phase 21 — two-tracking-systems reconcile (guide progress vs block interactions)
✅ DONE (verify-and-document) 2026-06-27 — verdict: the two systems are ALREADY fully independent at every layer, which is exactly what the operator decided they should be (D21.1). There is NO reconcile work — merging them would violate the decision. Grounded by a bidirectional code trace (two parallel Explore passes over schema / services / handlers / endpoints / templates / JS / Studio insights). Decisions locked: D21.2 (
current_step_index) = keep + document — applied (honest schema comments onuserguideprogress.go+ regeneratedent/userguideprogress.go); D21.3 (block-insights asymmetry) = out of scope / future. Doc-only deliverable — no migration, no deploy (the schema edit is comment-only; the column and behavior are unchanged). Pending GoLand push.
Program: Under the Tuscan Sun (overview). Owner: Travis + Claude. Scope: the last web-first UTS phase before the Flutter pass (22+). Decision-heavy by design — the overview framed this as "keep distinct or reconcile"; the operator chose keep distinct, and the trace confirms the code already does.
D21.1 (operator-locked, 2026-06-27): responses stay INDEPENDENT per context
The same content (a Step / StepCollection — site-scoped, owner-less) can be referenced by both a guide's step_collection (guide.go:57) and a post's step/step_collection block (postcontentblock.go:75-80). But a user's response/progress is tracked separately by context:
- Answer it in the guide lightbox →
UserGuideProgress(per user/guide/attempt) +UserStepResponse(tied to the attempt + aStepCollectionItem) + thecompleted_stepsM2M. - Answer it in a post-body block →
UserBlockInteraction(per user/content_block/step).
Operator intent: a common question (e.g. "did you like this?") reused in a guide and in a post is the same question but two different askings — answering it in one place must NOT mark it answered in the other. The two tracking systems are the correct expression of that. Reconciling/merging them is forbidden (it would couple two deliberately-separate surfaces).
Verification — independence holds bidirectionally (the trace)
Two parallel read-only traces (guide-system, block-system) each mapped write paths, read/render paths, Studio insights, and a cross-system check. Neither system touches the other at any layer. Evidence:
| Layer | Guide progress | Block interactions | Cross-ref? |
|---|---|---|---|
| Schema | UserGuideProgress (edges: user, guide, completed_steps→StepCollectionItem M2M, responses), UserStepResponse (progress, step_collection_item, selected_option) |
UserBlockInteraction (edges: user, content_block, step, selected_option) |
None — no edge links the two; userblockinteraction.go:13 comment: "Independent from guide progress — blocks and guides are separate tracking systems." |
| Service | kontent/services/guide_progress.go (+ api/v1/services/guide_progress.go) |
kontent/services/block_interaction.go (+ api/v1/...) |
None — grep UserBlockInteraction guide_progress.go = 0; grep UserGuideProgress block_interaction.go = 0 (both tiers) |
| Handler | GuideProgressToggleStep / GuideProgressAnswer / GuideProgressReset (kontent/handlers/progress.go) |
BlockInteract / BlockReset / PostBlocksReset (same file, separate methods) |
None — each method calls only its own service; pages.go:434-457 enriches guides and blocks in separate calls |
| Endpoint | /progress/guide/toggle, /progress/guide/answer, /progress/guide/reset |
/progress/block/interact, /progress/block/reset, /progress/block/reset-all |
None |
| Response | GuideProgressJSON / ProgressResult / GuideProgress map |
BlockInteractionsJSON / BlockInteraction / BlockInteractions map |
None — distinct fields (kontent/response.go:134-135, api/v1/response.go:107-119) |
| Hydration | inline #guide-progress-data → static/js/kontent/utils/guide-lightbox.js |
inline #block-interactions-data → static/js/kontent/compilers/molecules/block-interaction-compiler.js (+ blocs/block-interaction-bloc.js) |
None — two JSON blobs, two JS modules; neither calls the other's endpoints |
| Templates | _guide_lightbox.gohtml (lightbox) |
_block_step.gohtml / _block_step_collection.gohtml (post body) |
None |
| Anti-spoiler redaction | RedactGuideAnswers (reads guide responses) |
RedactInteractiveBlockAnswers (reads block interactions) |
None — two separate fns in api/v1/services/guide_redact.go |
| Studio Insights | studio/services/guide_insights.go (summary, per-user list, reset-user/all) — guide progress only |
(no Studio analytics for block interactions) | None — grep UserBlockInteraction guide_insights.go / post_insights.go = 0 |
Conclusion: independence is enforced at schema, service, handler, endpoint, response, hydration, template, redaction, and analytics layers — in both directions. The reconcile question is answered: nothing to reconcile; keep as-is.
Deliverable
Because the systems are already correct, Phase 21 ships documentation, not a migration — the value is making the intentional separation durable so a future dev doesn't "tidy up" by merging them:
- This doc — the architecture record that the split is intentional (D21.1) + the evidence map above.
- The existing schema comment (
userblockinteraction.go:13) already states it; keep it. (Optional: add a mirrored one-line comment onUserGuideProgresspointing back, so the intent is visible from both sides.) - Reconcile the standing memory note [[project_guides_vs_blocks_separate]] — this phase is its closure.
No service/handler/JS/template changes required for the core decision.
Open dispositions (decide before marking Done)
D21.2 — current_step_index (vestigial field)
UserGuideProgress.current_step_index (userguideprogress.go:23) is always written 0 (all create/reset paths: guide_progress.go:67,347; api/.../guide_progress.go:60,324; guide_insights.go:285,355) and never read for any logic — only asserted ==0 in tests (kontent/v1/tests/progress_test.go:453,703). Phase 20 made learn-flow resume client-side (computed from hydrated per-step state), so the server field is genuinely dead.