Phase 21 — Reconciling the Books
05 JUL AT 08:25 PM

Phase 21 — Reconciling the Books

0 LOVES 0 VIEWS
Two tracking systems, one truth.

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 on userguideprogress.go + regenerated ent/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 lightboxUserGuideProgress (per user/guide/attempt) + UserStepResponse (tied to the attempt + a StepCollectionItem) + the completed_steps M2M.
  • Answer it in a post-body blockUserBlockInteraction (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/...) Nonegrep 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-datastatic/js/kontent/utils/guide-lightbox.js inline #block-interactions-datastatic/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) Nonegrep 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:

  1. This doc — the architecture record that the split is intentional (D21.1) + the evidence map above.
  2. The existing schema comment (userblockinteraction.go:13) already states it; keep it. (Optional: add a mirrored one-line comment on UserGuideProgress pointing back, so the intent is visible from both sides.)
  3. 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.

Option Cost Note
(a) Keep + document as reserved (recommended) zero Harmless; plausibly useful later for a server-side / cross-device resume index. Add a Comment noting it's currently unused.
(b) Drop the field schema change + go generate + remove the always-0 writes + the ==0 test asserts; on next startup Schema.Create drops the column ([[gotcha_ent_schema_create_drops_removed_columns]] — lossless here since always 0, no data) Cleaner, but a (small) migration footprint for a harmless column
(c) Start using it new write logic Not recommended — duplicates Phase-20's client-side resume; no current consumer

Recommendation: (a) — lowest risk; the field is harmless and may serve a future cross-device resume.

✅ DECIDED (operator, 2026-06-27): (a) keep + document. Applied: corrected the misleading "resume point" comment on userguideprogress.go:23 to state it's reserved/unused (resume is client-side, UTS Phase 20), and added a mirrored independence note on the UserGuideProgress struct doc-comment (pointing to UserBlockInteraction / D21.1). Regenerated ent/userguideprogress.go (comment-only propagation; ent package builds green). No column drop, no migration.

D21.3 — block-interactions have no Studio analytics (asymmetry)

Studio Insights surfaces guide progress (guide_insights.go) but there is no Studio view/analytics for block interactions. This is a coverage asymmetry, not a conflation. Keep-distinct does not require block insights. Recommendation: out of scope for Phase 21 — note it as a possible future feature ("Block interaction insights") if editors ever want per-block answer/completion analytics; do not build now.

✅ DECIDED (operator, 2026-06-27): out of scope / future. Recorded here as a candidate future feature (Studio "Block interaction insights", parallel to guide insights); not built in Phase 21.


Flutter parity note (Phase 22+)

The Flutter app must also keep the two systems independent (D21.1): the Flutter guide rendering tracks guide progress (its own hydration of guide-progress state); any interactive post-body block tracks block interactions separately. Do not unify them in Flutter either. The web's separation (separate endpoints /progress/guide/* vs /progress/block/*, separate hydration payloads) is the contract to mirror. (Cross-references the Phase-20 behavior contract, which is guide-lightbox-only.)


Acceptance / "Done"

  • [x] D21.1 recorded as the locked architecture decision (this doc + memory + schema comments).
  • [x] D21.2 disposition chosen — (a) keep + document — and applied (schema comments corrected + regenerated; ent builds green).
  • [x] D21.3 acknowledged as out-of-scope/future.
  • [x] Memory note [[project_guides_vs_blocks_separate]] reconciled (Phase 21 = its closure).
  • [x] Overview's Phase 21 bullet updated to the verify-and-document verdict.

D21.2 chose (a), so no behavior change — no tests/smoke needed (the schema edit is comment-only; the column and all logic are unchanged; go build ./ent/... green). Had (b) been chosen, the gate would have been build + go test ./... green + a quick guide-lightbox resume smoke.

← Back to Under the Tuscan Sun