Capablio

Studio app logic

Internal logic map of 360 Insights Studio (feedback360/app_server.py plus the PDP engine). Capablio (capablio.com) is the user-facing SaaS; Studio is the operator back-office for KB corpus management, ingestion…

Updated

The HTTP contract lives in app-api.md; this page explains the state machines, invariants, and gates behind those endpoints, and pairs with the reader-facing wiki page report/wiki/studio.html emitted by the render stage.

State model (AppState)

One process-wide AppState instance owns:

Field Type Lifetime Notes
settings_store SettingsStore disk (settings.json) Merge-on-PUT; reports_root must be absolute; unknown ai.* keys preserved.
credentials CredentialStore memory only Provider API keys are never written to disk; cleared on process exit.
jobs JobRegistry memory Generate jobs by hex id; stage list is fixed.
enrich_progress EnrichmentProgress memory Single in-flight enrichment run; second start returns busy.
active ActiveWorkspace \| None memory, lock-guarded Loaded data.json + metrics.json + pre-rendered app page.

ActiveWorkspace caches parsed JSON and the rendered dynamic page (render_app_page) so legacy routes (/, /api/data, /api/metrics, /report, /wiki/*) serve without re-reading disk. Selecting a workspace fails fast: 404 when sources/thomas.pdf is missing (_is_workspace), 409 when report JSON artifacts are absent.

Workspace invariants

  • A folder is a workspace iff sources/thomas.pdf exists. Everything else (report, verify.json, PDP files) is derived state.
  • Slug format <subject-slug>-<YYYYMMDD-HHMMSS> — one workspace per generate run; workspaces are never mutated in place by later runs.
  • list_workspaces is a pure scan of reports_root: verify_status is read defensively (None on unreadable JSON) and never blocks listing.

Generate job state machine

POST /api/generate creates a JobRecord with four fixed stages — parse → analyze → render → verify — each pending | running | done | failed, then runs AppState.run_generate_job on a background thread:

  1. Copy inputs into <workspace>/sources/ (Thomas required, snapshot optional).
  2. parseparse_360.parsedata.json.
  3. analyzeanalyze.analyzemetrics.json.
  4. renderrender_html.render360-report.html + wiki/*.html (including wiki/studio.html, the reader half of this dual doc).
  5. verifyverify_extraction.run_verification against the copied PDF; writes verify.json with verdict, summary counts, and up to 50 errors.

Failure rules:

  • Verify failure is a soft stop: stage failed, job done with error set, artifacts kept for inspection, workspace not activated, export blocked.
  • Any exception is a hard stop: the currently-running stage flips to failed, the job finishes failed. Nothing is retried automatically.
  • Only a fully verified run auto-activates its workspace (set_active_from_path at the end of stage 5).

Verify-gated export

export_active enforces the single export invariant: verify.json must exist, parse, and carry "verdict": "pass" — otherwise RuntimeError (surfaced as 409). The zip is built by the shared export_snapshot.write_report_zip (same code path as website snapshots) from report/, flat layout, including pdp.json / pdp_checklist.json when present.

PDP engine flow (pdp/engine.py, pdp/compose.py)

Session state is a JSON document (pdp_session.json) in the active workspace's report/ dir; the finalized plan is pdp.json. Core rules:

  • build_question_plan(data, metrics) is pure: same inputs → byte-identical steps. Stages 1–3 (orient, blindspot-*, focus) exist at session start; stages 4–7 (goal-<i>, actions-<i>, support, review) are appended by _append_dynamic_steps after focus is accepted, ordered by CTA rank.
  • apply_answer dispatches per-step validators returning one of five statuses; only accepted, accepted_with_warning, and pushback advance the cursor. pushback (disagree + robust significance) records contested: true but the user's answer stands. Full per-step matrix: pdp.md.
  • Focus selections outside the top-3 CTA require override_reason (needs_override otherwise — cursor does not advance).
  • AI is optional and non-blocking: draft_goal / review_custom_action fall back to deterministic templates on provider error; the fallback is recorded in validation metadata, never raised to HTTP.
  • finalize_session requires the review step confirmed, composes pdp.json via compose_pdp, and derives pdp_checklist.json (goals, actions, training steps, assignments, checkpoints) with per-item todo | in_progress | done.
  • data.json and metrics.json are read-only to all PDP code paths.

Enrichment concurrency

EnrichmentProgress.start is the mutex: it refuses a second run while one is running (409 at the API layer). Progress callbacks update per-section status (pending → writing → ok | rejected | failed), token estimates, and ETA; snapshot() is what GET /api/ai/enrich/progress returns. Enrichment re-renders 360-report.html but never touches the JSON artifacts.

Live controls (/api/metrics)

Dynamic recomputation calls the same compute_metrics(data, params) pure function as the analyze stage — one code path for disk output and API recomputation. Default params reproduce metrics.json byte-identically; invalid importance_source values are a 400, unknown keys are ignored.

Dual wiki pairing

Audience Artifact Source
Engineers this file (docs/app-logic.md) hand-maintained Markdown
Report readers report/wiki/studio.html emitted by render_html._wiki_studio on every render

When Studio behavior changes (stages, gates, PDP validation, export rules), update both: this page for the code-level mechanism, and _wiki_studio in feedback360/render_html.py for the reader-facing explanation. The generated page survives clean regeneration of report/; this page is version-controlled directly.